Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions openedx_authz/api/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -311,22 +311,24 @@ def get_users_for_role_in_scope(role_external_key: str, scope_external_key: str)


def get_scopes_for_user_and_permission(
user_external_key: str,
action_external_key: str,
user_external_key: str, action_external_key: str, scope_classes_filter: tuple[type[ScopeData], ...] | None = None
) -> list[ScopeData]:
"""Get all scopes where a specific user is assigned a specific permission.

Args:
user_external_key (str): ID of the user (e.g., 'john_doe').
action_external_key (str): The action to filter scopes (e.g., 'view', 'edit').

scope_classes_filter (tuple[type[ScopeData], ...] | None): Optional tuple of scope types to filter by.
Returns:
list[ScopeData]: A list of scopes where the user is assigned the specified permission.
"""
return get_scopes_for_subject_and_permission(
scopes_list = get_scopes_for_subject_and_permission(
UserData(external_key=user_external_key),
PermissionData(action=ActionData(external_key=action_external_key)),
)
if scope_classes_filter:
scopes_list = [scope for scope in scopes_list if isinstance(scope, scope_classes_filter)]
return scopes_list


def unassign_all_roles_from_user(user_external_key: str) -> bool:
Expand Down
16 changes: 16 additions & 0 deletions openedx_authz/tests/api/test_roles.py
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,22 @@ def setUpClass(cls):
"role_name": roles.COURSE_STAFF.external_key,
"scope_name": "course-v1:TestOrg+TestCourse+2024_T3",
},
# Edge case: same user, different role, different scopes using Org instead of course scope
{
"subject_name": "eduardo",
"role_name": roles.COURSE_STAFF.external_key,
"scope_name": "course-v1:TestOrg+TestCourse+2024_T1",
},
{
"subject_name": "eduardo",
"role_name": roles.COURSE_STAFF.external_key,
"scope_name": "course-v1:TestOrg+*",
},
{
"subject_name": "eduardo",
"role_name": roles.LIBRARY_AUTHOR.external_key,
"scope_name": "lib:Org4:art_301",
},
# Mixed permission levels across libraries for comprehensive testing
{
"subject_name": "maya",
Expand Down
83 changes: 82 additions & 1 deletion openedx_authz/tests/api/test_users.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,20 @@

from ddt import data, ddt, unpack

from openedx_authz.api.data import ContentLibraryData, RoleAssignmentData, RoleData, UserData
from openedx_authz.api.data import (
ContentLibraryData,
CourseOverviewData,
OrgCourseOverviewGlobData,
RoleAssignmentData,
RoleData,
UserData,
)
from openedx_authz.api.users import (
assign_role_to_user_in_scope,
batch_assign_role_to_users_in_scope,
batch_unassign_role_from_users,
get_all_user_role_assignments_in_scope,
get_scopes_for_user_and_permission,
get_user_role_assignments,
get_user_role_assignments_for_role_in_scope,
get_user_role_assignments_in_scope,
Expand Down Expand Up @@ -424,6 +432,79 @@ def test_unassign_all_roles_impacts_permissions(self):
)
self.assertFalse(has_permission_after)

@data(
# No filter → should return all scopes where user has permission
(
"alice",
permissions.DELETE_LIBRARY.identifier,
None,
{"lib:Org1:math_101"},
),
# Filter only ContentLibraryData → should include library scopes only
(
"alice",
permissions.DELETE_LIBRARY.identifier,
(ContentLibraryData,),
{"lib:Org1:math_101"},
),
# Filter excludes the scope type → should return empty
(
"alice",
permissions.COURSES_VIEW_COURSE.identifier,
(CourseOverviewData,),
set(),
),
# Multiple scopes (same type)
(
"eve",
permissions.MANAGE_LIBRARY_TEAM.identifier,
(ContentLibraryData,),
{"lib:Org2:physics_401"},
),
# Multiple scopes (different types) - filter to only one type
(
"eduardo",
permissions.COURSES_VIEW_COURSE.identifier,
(CourseOverviewData,),
{"course-v1:TestOrg+TestCourse+2024_T1"},
),
(
"eduardo",
permissions.COURSES_VIEW_COURSE.identifier,
None,
{"course-v1:TestOrg+TestCourse+2024_T1", "course-v1:TestOrg+*"},
),
(
"eduardo",
permissions.COURSES_VIEW_COURSE.identifier,
(OrgCourseOverviewGlobData,),
{"course-v1:TestOrg+*"},
),
)
@unpack
def test_get_scopes_for_user_and_permission_with_filter(
self,
username,
action,
scope_filter,
expected_scopes,
):
"""Test filtering scopes by scope_classes_filter.

Expected result:
- When no filter is provided, all scopes are returned
- When a filter is provided, only matching scope types are returned
- When filter excludes scope types, result is empty
"""
scopes = get_scopes_for_user_and_permission(
user_external_key=username,
action_external_key=action,
scope_classes_filter=scope_filter,
)

scope_keys = {scope.external_key for scope in scopes}
self.assertEqual(scope_keys, expected_scopes)


@ddt
class TestUserPermissions(UserAssignmentsSetupMixin):
Expand Down
Loading