From ea5d7c28c6e0164cf05b269c36a38c4737135989 Mon Sep 17 00:00:00 2001 From: Maria Grimaldi Date: Fri, 7 Nov 2025 15:21:00 +0100 Subject: [PATCH 1/3] refactor: avoid duplicates when getting role scopes --- openedx_authz/api/data.py | 4 +++ openedx_authz/api/roles.py | 2 +- openedx_authz/tests/api/test_roles.py | 35 +++++++++++++++++++++++++++ 3 files changed, 40 insertions(+), 1 deletion(-) diff --git a/openedx_authz/api/data.py b/openedx_authz/api/data.py index 01a67826..2e250d71 100644 --- a/openedx_authz/api/data.py +++ b/openedx_authz/api/data.py @@ -318,6 +318,10 @@ class ScopeData(AuthZData, metaclass=ScopeMeta): # Subclasses like ContentLibraryData ('lib') represent concrete resource types with their own namespaces. NAMESPACE: ClassVar[str] = "global" + def __eq__(self, other: "ScopeData") -> bool: + """Compare scopes based on their external_key.""" + return self.external_key == other.external_key + @classmethod def validate_external_key(cls, _: str) -> bool: """Validate the external_key format for ScopeData. diff --git a/openedx_authz/api/roles.py b/openedx_authz/api/roles.py index 2c0f02a7..c4e27e6c 100644 --- a/openedx_authz/api/roles.py +++ b/openedx_authz/api/roles.py @@ -398,6 +398,6 @@ def get_scopes_for_subject_and_permission( scopes = [] for role_assignment in roles_for_subject: for role in role_assignment.roles: - if permission in role.permissions: + if permission in role.permissions and role_assignment.scope not in scopes: scopes.append(role_assignment.scope) return scopes diff --git a/openedx_authz/tests/api/test_roles.py b/openedx_authz/tests/api/test_roles.py index d19497eb..3dec484a 100644 --- a/openedx_authz/tests/api/test_roles.py +++ b/openedx_authz/tests/api/test_roles.py @@ -620,6 +620,41 @@ def test_get_scopes_for_subject_and_permission(self, subject_name, action_name, for expected_scope in expected_scope_names: self.assertIn(expected_scope, actual_scope_names) + def test_get_scopes_for_subject_and_permission_no_duplicates(self): + """Test that get_scopes_for_subject_and_permission returns no duplicate scopes. + + This test verifies that when a subject has multiple roles in the same scope + that grant the same permission, the scope appears only once in the result. + + Expected result: + - Each scope appears exactly once in the returned list + - No duplicate scopes even when multiple roles grant the same permission + """ + test_scope = "lib:TestOrg:duplicate_test" + test_subject = "test_user_duplicates" + + assign_role_to_subject_in_scope( + SubjectData(external_key=test_subject), + RoleData(external_key=roles.LIBRARY_ADMIN.external_key), + ScopeData(external_key=test_scope), + ) + + assign_role_to_subject_in_scope( + SubjectData(external_key=test_subject), + RoleData(external_key=roles.LIBRARY_AUTHOR.external_key), + ScopeData(external_key=test_scope), + ) + + subject = SubjectData(external_key=test_subject) + permission = PermissionData(action=ActionData(external_key="view_library")) + + scopes = get_scopes_for_subject_and_permission(subject, permission) + scope_external_keys = [scope.external_key for scope in scopes] + + self.assertEqual(len(scope_external_keys), 1) + self.assertEqual(scope_external_keys[0], test_scope) + self.assertEqual(len(scope_external_keys), len(set(scope_external_keys))) + @ddt_data( (roles.LIBRARY_AUTHOR.external_key, "lib:Org4:art_101", {"liam"}), (roles.LIBRARY_AUTHOR.external_key, "lib:Org4:art_201", {"liam"}), From ac9cdf9380c5b11c4af3d776b1bfdd77883bac25 Mon Sep 17 00:00:00 2001 From: Maria Grimaldi Date: Tue, 11 Nov 2025 11:06:01 +0100 Subject: [PATCH 2/3] refactor: drop unnecessary eq method for parent --- openedx_authz/api/data.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/openedx_authz/api/data.py b/openedx_authz/api/data.py index 2e250d71..01a67826 100644 --- a/openedx_authz/api/data.py +++ b/openedx_authz/api/data.py @@ -318,10 +318,6 @@ class ScopeData(AuthZData, metaclass=ScopeMeta): # Subclasses like ContentLibraryData ('lib') represent concrete resource types with their own namespaces. NAMESPACE: ClassVar[str] = "global" - def __eq__(self, other: "ScopeData") -> bool: - """Compare scopes based on their external_key.""" - return self.external_key == other.external_key - @classmethod def validate_external_key(cls, _: str) -> bool: """Validate the external_key format for ScopeData. From 51ddd6287908cd0459e2c6faab20144d9a132e08 Mon Sep 17 00:00:00 2001 From: Maria Grimaldi Date: Tue, 11 Nov 2025 11:09:16 +0100 Subject: [PATCH 3/3] chore: update docs for release --- CHANGELOG.rst | 8 ++++++++ openedx_authz/__init__.py | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index e064d4f8..1c269dbd 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -14,6 +14,14 @@ Change Log Unreleased ********** +0.13.1 - 2025-11-06 +******************** + +Fixed +===== + +* Avoid duplicates when getting scopes for given user and permissions. + 0.13.0 - 2025-11-05 ******************** diff --git a/openedx_authz/__init__.py b/openedx_authz/__init__.py index c39af842..6438c962 100644 --- a/openedx_authz/__init__.py +++ b/openedx_authz/__init__.py @@ -4,6 +4,6 @@ import os -__version__ = "0.13.0" +__version__ = "0.13.1" ROOT_DIRECTORY = os.path.dirname(os.path.abspath(__file__))