From 74905f92d61509118639067f741a4477ee57c780 Mon Sep 17 00:00:00 2001 From: aiolibsbot Date: Wed, 1 Jul 2026 14:38:11 +0000 Subject: [PATCH] perf: cache parsed permissions per request permits() ran permissions_as_dict() on every call, re-splitting each permission string and json.loads-ing every filter value. Since permits() is invoked O(records * fields) times per list request, this parsing was repeated for every record/field even though the underlying permissions are identical for the whole request. Cache the parsed dict (not the raw collection) under the existing aiohttpadmin_permissions request key so the strings are parsed once. Update the list-filter path in abc.py to read the cached dict directly. --- aiohttp_admin/backends/abc.py | 6 +++--- aiohttp_admin/security.py | 14 +++++++++----- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/aiohttp_admin/backends/abc.py b/aiohttp_admin/backends/abc.py index d83cce79..0f9ee01f 100644 --- a/aiohttp_admin/backends/abc.py +++ b/aiohttp_admin/backends/abc.py @@ -13,7 +13,7 @@ from aiohttp_security import check_permission, permits from pydantic import Json -from ..security import check, permissions_as_dict +from ..security import check from ..types import ComponentState, InputState, fk, resources_key if sys.version_info >= (3, 10): @@ -435,8 +435,8 @@ def _process_list_query(self, query: _ListQuery, request: web.Request) -> None: query["filter"] = merged_filter # Add filters from advanced permissions. - # The permissions will be cached on the request from a previous permissions check. - permissions = permissions_as_dict(request["aiohttpadmin_permissions"]) + # The parsed permissions are cached on the request from a previous check. + permissions = request["aiohttpadmin_permissions"] filters = permissions.get(f"admin.{self.name}.view", permissions.get(f"admin.{self.name}.*", {})) for k, v in filters.items(): diff --git a/aiohttp_admin/security.py b/aiohttp_admin/security.py index 5355a088..23edb5ab 100644 --- a/aiohttp_admin/security.py +++ b/aiohttp_admin/security.py @@ -94,16 +94,20 @@ async def permits( except (TypeError, ValueError): raise TypeError("Context must be `(request, record)` or `(request, None)`") - permissions: Optional[Collection[str]] = request.get("aiohttpadmin_permissions") + permissions = request.get("aiohttpadmin_permissions") if permissions is None: + raw: Collection[str] if self._identity_callback is None: - permissions = (Permissions.all,) + raw = (Permissions.all,) else: user = await self._identity_callback(identity) - permissions = user["permissions"] - # Cache permissions per request to avoid potentially dozens of DB calls. + raw = user["permissions"] + # Cache the parsed permissions per request to avoid both repeat DB calls + # and re-parsing the permission strings on every check (permits() runs + # O(records * fields) times per request). + permissions = permissions_as_dict(raw) request["aiohttpadmin_permissions"] = permissions - return has_permission(permission, permissions_as_dict(permissions), record) + return has_permission(permission, permissions, record) class TokenIdentityPolicy(SessionIdentityPolicy):