Skip to content
Merged
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
8 changes: 8 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,14 @@ Change Log
Unreleased
**********

1.19.0 - 2026-06-17
*******************

Added
=====

* Add ``get_user_role_assignments_per_scope_type`` API function to fetch a user's role assignments filtered by scope type.

1.18.0 - 2026-06-09
*******************

Expand Down
2 changes: 1 addition & 1 deletion openedx_authz/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@

import os

__version__ = "1.18.0"
__version__ = "1.19.0"

ROOT_DIRECTORY = os.path.dirname(os.path.abspath(__file__))
24 changes: 24 additions & 0 deletions openedx_authz/api/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
"unassign_role_from_user",
"batch_unassign_role_from_users",
"get_user_role_assignments",
"get_user_role_assignments_per_scope_type",
"get_user_role_assignments_in_scope",
"get_user_role_assignments_for_role_in_scope",
"get_user_role_assignments_filtered",
Expand Down Expand Up @@ -149,6 +150,29 @@ def get_user_role_assignments(user_external_key: str) -> list[RoleAssignmentData
return get_subject_role_assignments(UserData(external_key=user_external_key))


def get_user_role_assignments_per_scope_type(
user_external_key: str,
scope_types: tuple[type[ScopeData], ...],
) -> list[RoleAssignmentData]:
"""Get role assignments for a user matching any of the given scope types.

Casbin policies store full scope keys (e.g., 'course-v1^course-v1:Org+Course+Run'), so there is no
way to query by scope type directly; filtering happens here after fetching the user's assignments.

Args:
user_external_key: ID of the user (e.g., 'john_doe').
scope_types: ScopeData subclasses (not instances). Assignments matching any of the given types are returned.

Returns:
list[RoleAssignmentData]: The user's assignments whose scope is an instance of any of the given scope types.
"""
return [
assignment
for assignment in get_user_role_assignments(user_external_key=user_external_key)
if isinstance(assignment.scope, scope_types)
]


def get_user_role_assignments_in_scope(user_external_key: str, scope_external_key: str) -> list[RoleAssignmentData]:
"""Get the roles assigned to a user in a specific scope.

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 @@ -5,7 +5,17 @@
from ddt import data, ddt, unpack
from django.contrib.auth import get_user_model

from openedx_authz.api.data import ContentLibraryData, RoleAssignmentData, RoleData, UserData
from openedx_authz.api.data import (
ContentLibraryData,
CourseOverviewData,
OrgContentLibraryGlobData,
OrgCourseOverviewGlobData,
PlatformContentLibraryGlobData,
PlatformCourseOverviewGlobData,
RoleAssignmentData,
RoleData,
UserData,
)
from openedx_authz.api.users import (
_filter_allowed_assignments,
_filter_candidate_assignments_by_params,
Expand All @@ -17,6 +27,7 @@
get_user_role_assignments_filtered,
get_user_role_assignments_for_role_in_scope,
get_user_role_assignments_in_scope,
get_user_role_assignments_per_scope_type,
get_visible_role_assignments_for_user,
get_visible_user_role_assignments_filtered_by_current_user,
is_user_allowed,
Expand Down Expand Up @@ -57,6 +68,76 @@ def _assign_roles_to_users(
)


@ddt
class TestUserRoleAssignmentsPerScopeType(UserAssignmentsSetupMixin):
"""Tests for get_user_role_assignments_per_scope_type including glob scope types."""

GLOB_SCOPE_TEST_ASSIGNMENTS = [
{
"subject_name": "nina",
"role_name": roles.LIBRARY_ADMIN.external_key,
"scope_name": OrgContentLibraryGlobData.build_external_key("GlobTest"),
},
{
"subject_name": "nina",
"role_name": roles.COURSE_STAFF.external_key,
"scope_name": OrgCourseOverviewGlobData.build_external_key("GlobTest"),
},
{
"subject_name": "nina",
"role_name": roles.LIBRARY_ADMIN.external_key,
"scope_name": PlatformContentLibraryGlobData.build_external_key(),
},
{
"subject_name": "nina",
"role_name": roles.COURSE_STAFF.external_key,
"scope_name": PlatformCourseOverviewGlobData.build_external_key(),
},
]

@classmethod
def setUpClass(cls):
super().setUpClass()
cls._assign_roles_to_users(assignments=cls.GLOB_SCOPE_TEST_ASSIGNMENTS)

@data(
("eve", (ContentLibraryData,), 3),
("eve", (CourseOverviewData,), 0),
("carlos", (CourseOverviewData,), 3),
("carlos", (ContentLibraryData,), 0),
("carlos", (CourseOverviewData, ContentLibraryData), 3),
("nina", (OrgContentLibraryGlobData,), 1),
("nina", (OrgCourseOverviewGlobData,), 1),
("nina", (PlatformContentLibraryGlobData,), 1),
("nina", (PlatformCourseOverviewGlobData,), 1),
("nina", (OrgContentLibraryGlobData, OrgCourseOverviewGlobData), 2),
("nina", (PlatformContentLibraryGlobData, PlatformCourseOverviewGlobData), 2),
(
"nina",
(
OrgContentLibraryGlobData,
OrgCourseOverviewGlobData,
PlatformContentLibraryGlobData,
PlatformCourseOverviewGlobData,
),
4,
),
("nina", (ContentLibraryData,), 0),
("nina", (CourseOverviewData,), 0),
)
@unpack
def test_get_user_role_assignments_per_scope_type(self, username, scope_types, expected_count):
"""Test retrieving role assignments for a user filtered by scope type."""
role_assignments = get_user_role_assignments_per_scope_type(
user_external_key=username,
scope_types=scope_types,
)

self.assertEqual(len(role_assignments), expected_count)
for assignment in role_assignments:
self.assertIsInstance(assignment.scope, scope_types)


@ddt
class TestUserRoleAssignments(UserAssignmentsSetupMixin):
"""Test suite for user-role assignment API functions."""
Expand Down