1010from casbin import Enforcer
1111
1212from openedx_authz .api .data import CourseOverviewData , OrgCourseOverviewGlobData
13+ from openedx_authz .api .roles import get_all_role_assignments_per_scope_type
1314from openedx_authz .api .users import (
1415 assign_role_to_user_in_scope ,
1516 batch_assign_role_to_users_in_scope ,
1617 batch_unassign_role_from_users ,
17- get_user_role_assignments ,
1818)
1919from openedx_authz .constants .roles import (
2020 LEGACY_COURSE_ROLE_EQUIVALENCES ,
@@ -203,6 +203,8 @@ def migrate_legacy_course_roles_to_authz(course_access_role_model, course_id_lis
203203 raise ValueError (
204204 "At least one of course_id_list or org_id must be provided to limit the scope of the migration."
205205 )
206+
207+ # TODO: not sure if we should keep the startswith here
206208 course_access_role_filter = {}
207209
208210 if org_id :
@@ -286,6 +288,12 @@ def migrate_authz_to_legacy_course_roles(
286288 This is essentially the reverse of migrate_legacy_course_roles_to_authz and is intended
287289 for rollback purposes in case of migration issues.
288290
291+ To build each CourseAccessRole entry, the function needs:
292+ - A user: resolved from role assignments in scopes linked to courses.
293+ - A scope: either a CourseOverviewData (course-level) or OrgCourseOverviewGlobData (org-level glob),
294+ filtered by course_id or org_id if provided.
295+ - A role: a role external key that maps to a legacy role in COURSE_ROLE_EQUIVALENCES.
296+
289297 param course_access_role_model: It should be the CourseAccessRole model. This is passed in because the function
290298 is intended to run within a Django migration context, where direct model imports can cause issues.
291299 param user_subject_model: It should be the UserSubject model. This is passed in because the function
@@ -300,70 +308,77 @@ def migrate_authz_to_legacy_course_roles(
300308 "At least one of course_id_list or org_id must be provided to limit the scope of the rollback migration."
301309 )
302310
303- # 1. Get all users with course-related permissions in the new model by filtering
304- # UserSubjects that are linked to CourseScopes with a valid course overview.
305- course_subject_filter = {
306- "casbin_rules__scope__coursescope__course_overview__isnull" : False ,
307- }
311+ # CourseOverviewData and OrgCourseOverviewGlobData share the same namespace,
312+ # so filtering by CourseOverviewData captures both course-level and org-level glob assignments.
313+ role_assignments = get_all_role_assignments_per_scope_type (scope_type = CourseOverviewData )
308314
315+ # Two cases here:
316+ # 1. If org_id is provided, we filter by org_id which will include both org-level glob scopes and course-level scopes linked to that org
317+ # 2. If only course_id_list is provided, we filter by course_id which will include only course-level scopes linked to those course_ids since
318+ # org-level glob scopes don't have course_id in their scope object
309319 if org_id :
310- course_subject_filter ["casbin_rules__scope__coursescope__course_overview__org" ] = org_id
320+ role_assignments = [
321+ role_assignment
322+ for role_assignment in role_assignments
323+ if role_assignment .scope .org == org_id
324+ ]
311325
312326 if course_id_list and not org_id :
313- # Only filter by course_id if org_id is not provided,
314- # otherwise we will filter by org_id which is more efficient
315- course_subject_filter ["casbin_rules__scope__coursescope__course_overview__id__in" ] = course_id_list
316-
317- course_subjects = user_subject_model .objects .filter (** course_subject_filter ).select_related ("user" ).distinct ()
327+ role_assignments = [
328+ role_assignment
329+ for role_assignment in role_assignments
330+ if isinstance (role_assignment .scope , CourseOverviewData )
331+ and role_assignment .scope .external_key in course_id_list
332+ ]
318333
319334 roles_with_errors = []
320335 roles_with_no_errors = []
321336 unassignments = defaultdict (list )
322337
323- for course_subject in course_subjects :
324- user = course_subject .user
325- user_external_key = user .username
338+ for role_assignment in role_assignments :
339+
340+ # Per valid role assignment, create corresponding CourseAccessRole entry
341+ try :
342+ user_external_key = role_assignment .subject .external_key
343+ role_external_key = role_assignment .roles [0 ].external_key
344+ scope_external_key = role_assignment .scope .external_key
345+
346+ course_access_role_kwargs = {
347+ "user" : user_subject_model .objects .get (user__username = user_external_key ).user ,
348+ "role" : COURSE_ROLE_EQUIVALENCES [role_external_key ],
349+ }
350+
351+ # Here we prioritize course_id over org for scope since course-level scope is more specific
352+ # and also both are not needed to create a valid CourseAccessRole entry
353+ if isinstance (role_assignment .scope , CourseOverviewData ):
354+ course_access_role_kwargs ["course_id" ] = scope_external_key
355+ elif isinstance (role_assignment .scope , OrgCourseOverviewGlobData ):
356+ course_access_role_kwargs ["org" ] = role_assignment .scope .org
357+ else :
358+ logger .error (
359+ f"Unexpected scope type: { type (role_assignment .scope )} for RoleAssignment with scope: { scope_external_key } "
360+ )
361+ roles_with_errors .append (role_assignment )
362+ continue
363+
364+ course_access_role_model .objects .create (** course_access_role_kwargs )
365+ roles_with_no_errors .append (role_assignment )
326366
327- # 2. Get all role assignments for the user
328- role_assignments = get_user_role_assignments (user_external_key = user_external_key )
367+ logger .info (
368+ f"Successfully rolled back RoleAssignment for User: { user_external_key } "
369+ f"in Role: { role_external_key } and Scope: { scope_external_key } "
370+ f"to legacy CourseAccessRole entry."
371+ )
329372
330- for assignment in role_assignments :
331- if not isinstance (assignment .scope , CourseOverviewData ):
332- logger .error (f"Skipping role assignment for User: { user_external_key } due to missing course scope." )
333- continue
373+ if delete_after_migration :
374+ unassignments [(role_external_key , scope_external_key )].append (user_external_key )
334375
335- scope = assignment .scope .external_key
336-
337- course_overview = assignment .scope .get_object ()
338-
339- for role in assignment .roles :
340- legacy_role = COURSE_ROLE_EQUIVALENCES .get (role .external_key )
341- if legacy_role is None :
342- logger .error (f"Unknown role: { role } for User: { user_external_key } " )
343- roles_with_errors .append ((user_external_key , role .external_key , scope ))
344- continue
345-
346- try :
347- # Create legacy CourseAccessRole entry
348- course_access_role_model .objects .get_or_create (
349- user = user ,
350- org = course_overview .org ,
351- course_id = scope ,
352- role = legacy_role ,
353- )
354- roles_with_no_errors .append ((user_external_key , role .external_key , scope ))
355- except Exception as e : # pylint: disable=broad-exception-caught
356- logger .error (
357- f"Error creating CourseAccessRole for User: "
358- f"{ user_external_key } , Role: { legacy_role } , Course: { scope } : { e } "
359- )
360- roles_with_errors .append ((user_external_key , role .external_key , scope ))
361- continue
362-
363- # If we successfully created the legacy role, we can add this role assignment
364- # to the unassignment list if delete_after_migration is True
365- if delete_after_migration :
366- unassignments [(role .external_key , scope )].append (user_external_key )
376+ except Exception as e :
377+ logger .error (
378+ f"Error rolling back RoleAssignment for User: { role_assignment .subject .external_key } "
379+ f"in Role: { role_assignment .roles [0 ].external_key } and Scope: { role_assignment .scope .external_key } : { e } "
380+ )
381+ roles_with_errors .append (role_assignment )
367382
368383 # Once the loop is done, we can log summary of unassignments
369384 # and perform batch unassignment if delete_after_migration is True
0 commit comments