Skip to content

Commit 60a1a9e

Browse files
committed
feat: add scope filter to get_scopes_for_user_and_permission
1 parent 218e294 commit 60a1a9e

3 files changed

Lines changed: 104 additions & 5 deletions

File tree

openedx_authz/api/users.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -311,22 +311,24 @@ def get_users_for_role_in_scope(role_external_key: str, scope_external_key: str)
311311

312312

313313
def get_scopes_for_user_and_permission(
314-
user_external_key: str,
315-
action_external_key: str,
314+
user_external_key: str, action_external_key: str, scope_classes_filter: tuple[type[ScopeData], ...] | None = None
316315
) -> list[ScopeData]:
317316
"""Get all scopes where a specific user is assigned a specific permission.
318317
319318
Args:
320319
user_external_key (str): ID of the user (e.g., 'john_doe').
321320
action_external_key (str): The action to filter scopes (e.g., 'view', 'edit').
322-
321+
scope_classes_filter (tuple[type[ScopeData], ...] | None): Optional tuple of scope types to filter by.
323322
Returns:
324323
list[ScopeData]: A list of scopes where the user is assigned the specified permission.
325324
"""
326-
return get_scopes_for_subject_and_permission(
325+
scopes_list = get_scopes_for_subject_and_permission(
327326
UserData(external_key=user_external_key),
328327
PermissionData(action=ActionData(external_key=action_external_key)),
329328
)
329+
if scope_classes_filter:
330+
scopes_list = [scope for scope in scopes_list if isinstance(scope, scope_classes_filter)]
331+
return scopes_list
330332

331333

332334
def unassign_all_roles_from_user(user_external_key: str) -> bool:

openedx_authz/tests/api/test_roles.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,22 @@ def setUpClass(cls):
296296
"role_name": roles.COURSE_STAFF.external_key,
297297
"scope_name": "course-v1:TestOrg+TestCourse+2024_T3",
298298
},
299+
# Edge case: same user, different role, different scopes using Org instead of course scope
300+
{
301+
"subject_name": "eduardo",
302+
"role_name": roles.COURSE_STAFF.external_key,
303+
"scope_name": "course-v1:TestOrg+TestCourse+2024_T1",
304+
},
305+
{
306+
"subject_name": "eduardo",
307+
"role_name": roles.COURSE_STAFF.external_key,
308+
"scope_name": "course-v1:TestOrg+*",
309+
},
310+
{
311+
"subject_name": "eduardo",
312+
"role_name": roles.LIBRARY_AUTHOR.external_key,
313+
"scope_name": "lib:Org4:art_301",
314+
},
299315
# Mixed permission levels across libraries for comprehensive testing
300316
{
301317
"subject_name": "maya",

openedx_authz/tests/api/test_users.py

Lines changed: 82 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,20 @@
22

33
from ddt import data, ddt, unpack
44

5-
from openedx_authz.api.data import ContentLibraryData, RoleAssignmentData, RoleData, UserData
5+
from openedx_authz.api.data import (
6+
ContentLibraryData,
7+
CourseOverviewData,
8+
OrgCourseOverviewGlobData,
9+
RoleAssignmentData,
10+
RoleData,
11+
UserData,
12+
)
613
from openedx_authz.api.users import (
714
assign_role_to_user_in_scope,
815
batch_assign_role_to_users_in_scope,
916
batch_unassign_role_from_users,
1017
get_all_user_role_assignments_in_scope,
18+
get_scopes_for_user_and_permission,
1119
get_user_role_assignments,
1220
get_user_role_assignments_for_role_in_scope,
1321
get_user_role_assignments_in_scope,
@@ -424,6 +432,79 @@ def test_unassign_all_roles_impacts_permissions(self):
424432
)
425433
self.assertFalse(has_permission_after)
426434

435+
@data(
436+
# No filter → should return all scopes where user has permission
437+
(
438+
"alice",
439+
permissions.DELETE_LIBRARY.identifier,
440+
None,
441+
{"lib:Org1:math_101"},
442+
),
443+
# Filter only ContentLibraryData → should include library scopes only
444+
(
445+
"alice",
446+
permissions.DELETE_LIBRARY.identifier,
447+
(ContentLibraryData,),
448+
{"lib:Org1:math_101"},
449+
),
450+
# Filter excludes the scope type → should return empty
451+
(
452+
"alice",
453+
permissions.COURSES_VIEW_COURSE.identifier,
454+
(CourseOverviewData,),
455+
set(),
456+
),
457+
# Multiple scopes (same type)
458+
(
459+
"eve",
460+
permissions.MANAGE_LIBRARY_TEAM.identifier,
461+
(ContentLibraryData,),
462+
{"lib:Org2:physics_401"},
463+
),
464+
# Multiple scopes (different types) - filter to only one type
465+
(
466+
"eduardo",
467+
permissions.COURSES_VIEW_COURSE.identifier,
468+
(CourseOverviewData,),
469+
{"course-v1:TestOrg+TestCourse+2024_T1"},
470+
),
471+
(
472+
"eduardo",
473+
permissions.COURSES_VIEW_COURSE.identifier,
474+
None,
475+
{"course-v1:TestOrg+TestCourse+2024_T1", "course-v1:TestOrg+*"},
476+
),
477+
(
478+
"eduardo",
479+
permissions.COURSES_VIEW_COURSE.identifier,
480+
(OrgCourseOverviewGlobData,),
481+
{"course-v1:TestOrg+*"},
482+
),
483+
)
484+
@unpack
485+
def test_get_scopes_for_user_and_permission_with_filter(
486+
self,
487+
username,
488+
action,
489+
scope_filter,
490+
expected_scopes,
491+
):
492+
"""Test filtering scopes by scope_classes_filter.
493+
494+
Expected result:
495+
- When no filter is provided, all scopes are returned
496+
- When a filter is provided, only matching scope types are returned
497+
- When filter excludes scope types, result is empty
498+
"""
499+
scopes = get_scopes_for_user_and_permission(
500+
user_external_key=username,
501+
action_external_key=action,
502+
scope_classes_filter=scope_filter,
503+
)
504+
505+
scope_keys = {scope.external_key for scope in scopes}
506+
self.assertEqual(scope_keys, expected_scopes)
507+
427508

428509
@ddt
429510
class TestUserPermissions(UserAssignmentsSetupMixin):

0 commit comments

Comments
 (0)