1111
1212from django .contrib .auth import get_user_model
1313from django .db .models import Q
14+ from edx_django_utils .cache import RequestCache
1415
1516from openedx_authz .api .data import (
1617 ActionData ,
4344
4445User = get_user_model ()
4546
47+ # Cache namespace used by is_user_allowed. Cleared by role mutation functions so
48+ # that permission checks within the same request reflect the latest assignments.
49+ _IS_USER_ALLOWED_CACHE_NS = "rbac_is_user_allowed"
50+
4651
4752__all__ = [
4853 "assign_role_to_user_in_scope" ,
@@ -76,6 +81,7 @@ def assign_role_to_user_in_scope(user_external_key: str, role_external_key: str,
7681 Returns:
7782 bool: True if the role was assigned successfully, False otherwise.
7883 """
84+ RequestCache (_IS_USER_ALLOWED_CACHE_NS ).clear ()
7985 return assign_role_to_subject_in_scope (
8086 UserData (external_key = user_external_key ),
8187 RoleData (external_key = role_external_key ),
@@ -91,6 +97,7 @@ def batch_assign_role_to_users_in_scope(users: list[str], role_external_key: str
9197 role_external_key (str): Name of the role to assign.
9298 scope (str): Scope in which to assign the role.
9399 """
100+ RequestCache (_IS_USER_ALLOWED_CACHE_NS ).clear ()
94101 namespaced_users = [UserData (external_key = username ) for username in users ]
95102 batch_assign_role_to_subjects_in_scope (
96103 namespaced_users ,
@@ -110,6 +117,7 @@ def unassign_role_from_user(user_external_key: str, role_external_key: str, scop
110117 Returns:
111118 bool: True if the role was unassigned successfully, False otherwise.
112119 """
120+ RequestCache (_IS_USER_ALLOWED_CACHE_NS ).clear ()
113121 return unassign_role_from_subject_in_scope (
114122 UserData (external_key = user_external_key ),
115123 RoleData (external_key = role_external_key ),
@@ -125,6 +133,7 @@ def batch_unassign_role_from_users(users: list[str], role_external_key: str, sco
125133 role_external_key (str): Name of the role to unassign.
126134 scope (str): Scope in which to unassign the role.
127135 """
136+ RequestCache (_IS_USER_ALLOWED_CACHE_NS ).clear ()
128137 namespaced_users = [UserData (external_key = user ) for user in users ]
129138 batch_unassign_role_from_subjects_in_scope (
130139 namespaced_users ,
@@ -345,6 +354,11 @@ def is_user_allowed(
345354) -> bool :
346355 """Check if a user has a specific permission in a given scope.
347356
357+ Results are cached per-request (keyed by user, action, and scope) to avoid
358+ repeated enforcement calls for the same arguments within a single request,
359+ e.g. when permission checks are performed once per object-tag during
360+ serialization.
361+
348362 Args:
349363 user_external_key (str): ID of the user (e.g., 'john_doe').
350364 action_external_key (str): The action to check (e.g., 'view_course').
@@ -353,11 +367,20 @@ def is_user_allowed(
353367 Returns:
354368 bool: True if the user has the specified permission in the scope, False otherwise.
355369 """
356- return is_subject_allowed (
370+ request_cache = RequestCache (_IS_USER_ALLOWED_CACHE_NS )
371+ cache_key = f"{ user_external_key } :{ action_external_key } :{ scope_external_key } "
372+
373+ cached_response = request_cache .get_cached_response (cache_key )
374+ if cached_response .is_found :
375+ return cached_response .value
376+
377+ result = is_subject_allowed (
357378 UserData (external_key = user_external_key ),
358379 ActionData (external_key = action_external_key ),
359380 ScopeData (external_key = scope_external_key ),
360381 )
382+ request_cache .set (cache_key , result )
383+ return result
361384
362385
363386def get_users_for_role_in_scope (role_external_key : str , scope_external_key : str ) -> list [UserData ]:
@@ -405,6 +428,7 @@ def unassign_all_roles_from_user(user_external_key: str) -> bool:
405428 Returns:
406429 bool: True if any roles were removed, False otherwise.
407430 """
431+ RequestCache (_IS_USER_ALLOWED_CACHE_NS ).clear ()
408432 return unassign_subject_from_all_roles (UserData (external_key = user_external_key ))
409433
410434
0 commit comments