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 @@ -16,6 +16,14 @@ Unreleased

*

0.9.1 - 2025-10-28
******************

Fixed
=====

* Fix role user count to accurately filter users assigned to roles within specific scopes instead of across all scopes.

0.9.0 - 2025-10-27
******************

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__ = "0.9.0"
__version__ = "0.9.1"

ROOT_DIRECTORY = os.path.dirname(os.path.abspath(__file__))
13 changes: 9 additions & 4 deletions openedx_authz/api/roles.py
Original file line number Diff line number Diff line change
Expand Up @@ -362,18 +362,23 @@ def get_all_subject_role_assignments_in_scope(
return list(role_assignments_per_subject.values())


def get_subjects_for_role(role: RoleData) -> list[SubjectData]:
"""Get all the subjects assigned to a specific role.
def get_subjects_for_role_in_scope(role: RoleData, scope: ScopeData) -> list[SubjectData]:
"""Get all the subjects assigned to a specific role in a specific scope.

Args:
role (RoleData): The role to filter subjects.
scope (ScopeData): The scope to filter subjects.

Returns:
list[SubjectData]: A list of subjects assigned to the specified role.
list[SubjectData]: A list of subjects assigned to the specified role in the specified scope.
"""
enforcer = AuthzEnforcer.get_enforcer()
policies = enforcer.get_filtered_grouping_policy(GroupingPolicyIndex.ROLE.value, role.namespaced_key)
return [SubjectData(namespaced_key=policy[GroupingPolicyIndex.SUBJECT.value]) for policy in policies]
return [
SubjectData(namespaced_key=policy[GroupingPolicyIndex.SUBJECT.value])
for policy in policies
if policy[GroupingPolicyIndex.SCOPE.value] == scope.namespaced_key
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've also had to do this a couple of times in these functions since get_filtered_grouping_policy doesn't support filtering by more than one parameter. It'd be nice if, as a future improvement, we could implement a get_filtered_grouping_policy with N parameters as input. In any case, this looks good to me!

]


def get_scopes_for_role_and_subject(role: RoleData, subject: SubjectData) -> list[ScopeData]:
Expand Down
16 changes: 10 additions & 6 deletions openedx_authz/api/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
get_subject_role_assignments,
get_subject_role_assignments_for_role_in_scope,
get_subject_role_assignments_in_scope,
get_subjects_for_role,
get_subjects_for_role_in_scope,
unassign_role_from_subject_in_scope,
)

Expand All @@ -34,7 +34,7 @@
"get_user_role_assignments_for_role_in_scope",
"get_all_user_role_assignments_in_scope",
"is_user_allowed",
"get_users_for_role",
"get_users_for_role_in_scope",
]


Expand Down Expand Up @@ -188,16 +188,20 @@ def is_user_allowed(
)


def get_users_for_role(role_external_key: str) -> list[UserData]:
"""Get all the users assigned to a specific role.
def get_users_for_role_in_scope(role_external_key: str, scope_external_key: str) -> list[UserData]:
"""Get all the users assigned to a specific role in a specific scope.

Args:
role_external_key (str): The role to filter users (e.g., 'library_admin').
scope_external_key (str): The scope to filter users (e.g., 'lib:DemoX:CSPROB').

Returns:
list[UserData]: A list of users assigned to the specified role.
list[UserData]: A list of users assigned to the specified role in the specified scope.
"""
users = get_subjects_for_role(RoleData(external_key=role_external_key))
users = get_subjects_for_role_in_scope(
RoleData(external_key=role_external_key),
ScopeData(external_key=scope_external_key),
)
return [UserData(namespaced_key=user.namespaced_key) for user in users]


Expand Down
5 changes: 3 additions & 2 deletions openedx_authz/rest_api/v1/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -432,12 +432,13 @@ def get(self, request: HttpRequest) -> Response:
"""Retrieve all roles and their permissions for a specific scope."""
serializer = ListRolesWithScopeSerializer(data=request.query_params)
serializer.is_valid(raise_exception=True)
query_params = serializer.validated_data

generic_scope = get_generic_scope(serializer.validated_data["scope"])
generic_scope = get_generic_scope(query_params["scope"])
roles = api.get_role_definitions_in_scope(generic_scope)
response_data = []
for role in roles:
users = api.get_users_for_role(role.external_key)
users = api.get_users_for_role_in_scope(role.external_key, query_params["scope"].external_key)
response_data.append(
{
"role": role.external_key,
Expand Down
21 changes: 21 additions & 0 deletions openedx_authz/tests/api/test_roles.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
get_subject_role_assignments,
get_subject_role_assignments_for_role_in_scope,
get_subject_role_assignments_in_scope,
get_subjects_for_role_in_scope,
unassign_role_from_subject_in_scope,
)
from openedx_authz.engine.enforcer import AuthzEnforcer
Expand Down Expand Up @@ -524,6 +525,26 @@ def test_get_scopes_for_role_and_subject(self):
scope_names = {scope.external_key for scope in scopes}
self.assertEqual(scope_names, expected_scopes)

@ddt_data(
("library_author", "lib:Org4:art_101", {"liam"}),
("library_author", "lib:Org4:art_201", {"liam"}),
("library_author", "lib:Org4:art_301", {"liam"}),
("non_existent_role", "lib:Org4:art_101", set()),
("library_author", "sc:non_existent_scope", set()),
("non_existent_role", "sc:non_existent_scope", set()),
)
@unpack
def test_get_subjects_for_role_in_scope(self, role_name: str, scope_name: str, expected_subjects: set[str]):
"""Test retrieving subjects for a given role in a specific scope.

Expected result:
- The subjects associated with the specified role in the given scope are correctly retrieved.
"""
subjects = get_subjects_for_role_in_scope(RoleData(external_key=role_name), ScopeData(external_key=scope_name))

subject_names = {subject.external_key for subject in subjects}
self.assertEqual(subject_names, expected_subjects)


@ddt
class TestRoleAssignmentAPI(RolesTestSetupMixin):
Expand Down