1515from opaque_keys .edx .keys import CourseKey
1616from opaque_keys .edx .locator import CourseLocator
1717from openedx_authz .api import users as authz_api
18- from openedx_authz .api .data import CourseOverviewData , RoleAssignmentData
18+ from openedx_authz .api .data import CourseOverviewData , OrgCourseOverviewGlobData , RoleAssignmentData
1919from openedx_authz .constants import roles as authz_roles
2020
2121from common .djangoapps .student .models import CourseAccessRole
@@ -74,17 +74,6 @@ def authz_add_role(user: User, authz_role: str, course_key: str):
7474 legacy_role = get_legacy_role_from_authz_role (authz_role )
7575 emit_course_access_role_added (user , course_locator , course_locator .org , legacy_role )
7676
77- def authz_get_all_course_assignments_for_user (user : User ) -> list [RoleAssignmentData ]:
78- """
79- Get all course assignments for a user.
80- """
81- assignments = authz_api .get_user_role_assignments (user_external_key = user .username )
82- # filter courses only
83- filtered_assignments = [
84- assignment for assignment in assignments
85- if isinstance (assignment .scope , CourseOverviewData )
86- ]
87- return filtered_assignments
8877
8978def get_org_from_key (key : str ) -> str :
9079 """
@@ -93,6 +82,7 @@ def get_org_from_key(key: str) -> str:
9382 parsed_key = CourseKey .from_string (key )
9483 return parsed_key .org
9584
85+
9686def register_access_role (cls ):
9787 """
9888 Decorator that allows access roles to be registered within the roles module and referenced by their
@@ -141,34 +131,120 @@ class AuthzCompatCourseAccessRole:
141131 """
142132 Generic data class for storing CourseAccessRole-compatible data
143133 to be used inside BulkRoleCache and RoleCache.
134+
144135 This allows the cache to store both legacy and openedx-authz compatible roles
145136 """
146137 user_id : int
147138 username : str
148139 org : str
149- course_id : str # Course key
140+ course_id : str | None
150141 role : str
151142
152143
153- def get_authz_compat_course_access_roles_for_user (user : User ) -> set [AuthzCompatCourseAccessRole ]:
144+ def _get_org_and_course_id_from_authz_scope (
145+ scope : CourseOverviewData | OrgCourseOverviewGlobData ,
146+ ) -> tuple [str , str | None ] | None :
154147 """
155- Retrieve all CourseAccessRole objects for a given user and convert them to AuthzCompatCourseAccessRole objects.
148+ Extract the org and course key from an AuthZ course assignment scope.
149+
150+ Course-scoped assignments return ``(org, course_external_key)``.
151+ Org-wide assignments return ``(org, None)``.
152+
153+ Returns ``None`` when the org cannot be determined. For org-wide scopes,
154+ ``OrgGlobData.org`` is typed as ``str | None`` because it is parsed from
155+ ``external_key`` and returns ``None`` for malformed glob patterns.
156156 """
157- compat_role_assignments = set ()
158- assignments = authz_get_all_course_assignments_for_user (user )
159- for assignment in assignments :
160- for role in assignment .roles :
161- legacy_role = get_legacy_role_from_authz_role (authz_role = role .external_key )
162- course_key = assignment .scope .external_key
163- org = get_org_from_key (course_key )
164- compat_role = AuthzCompatCourseAccessRole (
157+ if isinstance (scope , CourseOverviewData ):
158+ course_id = scope .external_key
159+ return get_org_from_key (course_id ), course_id
160+ if isinstance (scope , OrgCourseOverviewGlobData ):
161+ return scope .org , None
162+ return None
163+
164+
165+ def authz_get_all_course_assignments_for_user (user : User ) -> list [RoleAssignmentData ]:
166+ """
167+ Return AuthZ role assignments for a user that apply to courses.
168+
169+ Includes assignments scoped to a specific course (``CourseOverviewData``) and
170+ assignments scoped to all courses in an organization (``OrgCourseOverviewGlobData``).
171+ Assignments for other resource types, such as content libraries, are excluded.
172+
173+ Args:
174+ user (User): The user whose AuthZ role assignments should be retrieved.
175+
176+ Returns:
177+ list[RoleAssignmentData]: Role assignments whose scope is course-level or org-wide
178+ """
179+ return authz_api .get_user_role_assignments_per_scope_type (
180+ user_external_key = user .username ,
181+ scope_types = (CourseOverviewData , OrgCourseOverviewGlobData ),
182+ )
183+
184+
185+ def _compat_roles_from_authz_assignment (
186+ user : User ,
187+ assignment : RoleAssignmentData ,
188+ ) -> set [AuthzCompatCourseAccessRole ]:
189+ """
190+ Convert an AuthZ role assignment into legacy-compatible course access roles.
191+
192+ Course-scoped assignments produce roles tied to a specific course key.
193+ Org-wide assignments produce org-level roles with no course key (``course_id``
194+ is ``None``), matching legacy ``OrgStaffRole`` / ``OrgInstructorRole`` behavior.
195+ AuthZ roles without a legacy mapping are skipped.
196+
197+ Args:
198+ user (User): The user associated with the assignment.
199+ assignment (RoleAssignmentData): A single AuthZ role assignment, including
200+ its scope and assigned roles.
201+
202+ Returns:
203+ set[AuthzCompatCourseAccessRole]: Legacy-compatible role records for the
204+ assignment. Returns an empty set if the org cannot be determined from
205+ the scope or no roles could be mapped.
206+ """
207+ org_and_course_id = _get_org_and_course_id_from_authz_scope (assignment .scope )
208+ if org_and_course_id is None :
209+ return set ()
210+ org , course_id = org_and_course_id
211+
212+ compat_roles = set ()
213+ for role in assignment .roles :
214+ legacy_role = get_legacy_role_from_authz_role (authz_role = role .external_key )
215+ if legacy_role is None :
216+ continue
217+ compat_roles .add (
218+ AuthzCompatCourseAccessRole (
165219 user_id = user .id ,
166220 username = user .username ,
167221 org = org ,
168- course_id = course_key ,
169- role = legacy_role
222+ course_id = course_id ,
223+ role = legacy_role ,
170224 )
171- compat_role_assignments .add (compat_role )
225+ )
226+ return compat_roles
227+
228+
229+ def get_authz_compat_course_access_roles_for_user (user : User ) -> set [AuthzCompatCourseAccessRole ]:
230+ """
231+ Retrieve AuthZ course and org role assignments for a user in legacy format.
232+
233+ Fetches all course-level and org-wide AuthZ assignments for the user and
234+ converts each one into ``AuthzCompatCourseAccessRole`` records suitable for
235+ ``RoleCache`` and other legacy permission checks.
236+
237+ Args:
238+ user (User): The user whose AuthZ role assignments should be converted.
239+
240+ Returns:
241+ set[AuthzCompatCourseAccessRole]: Legacy-compatible role records derived
242+ from the user's AuthZ assignments. Returns an empty set if the user
243+ has no applicable assignments.
244+ """
245+ compat_role_assignments = set ()
246+ for assignment in authz_get_all_course_assignments_for_user (user ):
247+ compat_role_assignments .update (_compat_roles_from_authz_assignment (user , assignment ))
172248 return compat_role_assignments
173249
174250
@@ -843,11 +919,9 @@ def courses_with_role(self) -> set[AuthzCompatCourseAccessRole]:
843919 """
844920 Return a set of AuthzCompatCourseAccessRole for all of the courses with this user x (or derived from x) role.
845921 """
922+ # Get all assignments for a user to a role
846923 roles = RoleCache .get_roles (self .role )
847924 legacy_assignments = CourseAccessRole .objects .filter (role__in = roles , user = self .user )
848-
849- # Get all assignments for a user to a role
850- new_authz_roles = [get_authz_role_from_legacy_role (role ) for role in roles ]
851925 all_authz_user_assignments = authz_get_all_course_assignments_for_user (self .user )
852926
853927 all_assignments = set ()
@@ -863,19 +937,9 @@ def courses_with_role(self) -> set[AuthzCompatCourseAccessRole]:
863937 ))
864938
865939 for assignment in all_authz_user_assignments :
866- for role in assignment .roles :
867- if role .external_key not in new_authz_roles :
868- continue
869- legacy_role = get_legacy_role_from_authz_role (authz_role = role .external_key )
870- course_key = assignment .scope .external_key
871- org = get_org_from_key (course_key )
872- all_assignments .add (AuthzCompatCourseAccessRole (
873- user_id = self .user .id ,
874- username = self .user .username ,
875- org = org ,
876- course_id = course_key ,
877- role = legacy_role
878- ))
940+ for compat_role in _compat_roles_from_authz_assignment (self .user , assignment ):
941+ if compat_role .role in roles :
942+ all_assignments .add (compat_role )
879943
880944 return all_assignments
881945
@@ -899,18 +963,12 @@ def has_courses_with_role(self, org: str | None = None) -> bool:
899963 return True
900964
901965 # Then check for authz assignments
902- new_authz_roles = [get_authz_role_from_legacy_role (role ) for role in roles ]
903966 all_authz_user_assignments = authz_get_all_course_assignments_for_user (self .user )
904967
905968 for assignment in all_authz_user_assignments :
906- for role in assignment . roles :
907- if role . external_key not in new_authz_roles :
969+ for compat_role in _compat_roles_from_authz_assignment ( self . user , assignment ) :
970+ if compat_role . role not in roles :
908971 continue
909- if org is None :
910- # There is at least one assignment, short circuit
911- return True
912- course_key = assignment .scope .external_key
913- parsed_org = get_org_from_key (course_key )
914- if org == parsed_org :
972+ if org is None or org == compat_role .org :
915973 return True
916974 return False
0 commit comments