From e2daf35fcccbb72a439f02a2e4d138d16a45c6fd Mon Sep 17 00:00:00 2001 From: Bryann Valderrama Date: Mon, 27 Oct 2025 19:41:33 -0500 Subject: [PATCH 1/9] feat: load policy in role and permissions views --- openedx_authz/rest_api/v1/views.py | 35 ++++++++++++++++++++---------- 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/openedx_authz/rest_api/v1/views.py b/openedx_authz/rest_api/v1/views.py index a5b9376d..3328db1e 100644 --- a/openedx_authz/rest_api/v1/views.py +++ b/openedx_authz/rest_api/v1/views.py @@ -15,7 +15,10 @@ from rest_framework.views import APIView from openedx_authz import api +from openedx_authz.api.data import ScopeData from openedx_authz.constants import permissions +from openedx_authz.engine.enforcer import AuthzEnforcer +from openedx_authz.engine.filter import Filter from openedx_authz.rest_api.data import RoleOperationError, RoleOperationStatus from openedx_authz.rest_api.decorators import authz_permissions, view_auth_classes from openedx_authz.rest_api.utils import ( @@ -104,13 +107,16 @@ def post(self, request: HttpRequest) -> Response: """Validate one or more permissions for the authenticated user.""" serializer = PermissionValidationSerializer(data=request.data, many=True) serializer.is_valid(raise_exception=True) + data = serializer.validated_data + + AuthzEnforcer.get_enforcer().load_policy() username = request.user.username response_data = [] - for perm in serializer.validated_data: + for permission in data: try: - action = perm["action"] - scope = perm["scope"] + action = permission["action"] + scope = permission["scope"] allowed = api.is_user_allowed(username, action, scope) response_data.append({"action": action, "scope": scope, "allowed": allowed}) except ValueError as e: @@ -258,6 +264,9 @@ def get(self, request: HttpRequest) -> Response: serializer.is_valid(raise_exception=True) query_params = serializer.validated_data + flt = Filter(v2=[ScopeData(external_key=query_params["scope"]).namespaced_key]) + AuthzEnforcer.get_enforcer().load_filtered_policy(flt) + user_role_assignments = api.get_all_user_role_assignments_in_scope(query_params["scope"]) usernames = {assignment.subject.username for assignment in user_role_assignments} context = {"user_map": get_user_map(usernames)} @@ -283,15 +292,16 @@ def put(self, request: HttpRequest) -> Response: """Assign multiple users to a specific role within a scope.""" serializer = AddUsersToRoleWithScopeSerializer(data=request.data) serializer.is_valid(raise_exception=True) + data = serializer.validated_data + + AuthzEnforcer.get_enforcer().load_policy() - role = serializer.validated_data["role"] - scope = serializer.validated_data["scope"] completed, errors = [], [] - for user_identifier in serializer.validated_data["users"]: + for user_identifier in data["users"]: response_dict = {"user_identifier": user_identifier} try: user = get_user_by_username_or_email(user_identifier) - result = api.assign_role_to_user_in_scope(user.username, role, scope) + result = api.assign_role_to_user_in_scope(user.username, data["role"], data["scope"]) if result: response_dict["status"] = RoleOperationStatus.ROLE_ADDED completed.append(response_dict) @@ -328,17 +338,18 @@ def put(self, request: HttpRequest) -> Response: @authz_permissions([permissions.MANAGE_LIBRARY_TEAM.identifier]) def delete(self, request: HttpRequest) -> Response: """Remove multiple users from a specific role within a scope.""" + AuthzEnforcer.get_enforcer().load_policy() + serializer = RemoveUsersFromRoleWithScopeSerializer(data=request.query_params) serializer.is_valid(raise_exception=True) + data = serializer.validated_data - role = serializer.validated_data["role"] - scope = serializer.validated_data["scope"] completed, errors = [], [] - for user_identifier in serializer.validated_data["users"]: + for user_identifier in data["users"]: response_dict = {"user_identifier": user_identifier} try: user = get_user_by_username_or_email(user_identifier) - result = api.unassign_role_from_user(user.username, role, scope) + result = api.unassign_role_from_user(user.username, data["role"], data["scope"]) if result: response_dict["status"] = RoleOperationStatus.ROLE_REMOVED completed.append(response_dict) @@ -435,6 +446,8 @@ def get(self, request: HttpRequest) -> Response: serializer.is_valid(raise_exception=True) query_params = serializer.validated_data + AuthzEnforcer.get_enforcer().load_policy() + generic_scope = get_generic_scope(query_params["scope"]) roles = api.get_role_definitions_in_scope(generic_scope) response_data = [] From e791f853eb3c82489b8a08365c7e4ac77ce6e93a Mon Sep 17 00:00:00 2001 From: Bryann Valderrama Date: Tue, 28 Oct 2025 12:53:47 -0500 Subject: [PATCH 2/9] test: format code and uncomment tests --- openedx_authz/tests/rest_api/test_views.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/openedx_authz/tests/rest_api/test_views.py b/openedx_authz/tests/rest_api/test_views.py index c6b2d332..a196de09 100644 --- a/openedx_authz/tests/rest_api/test_views.py +++ b/openedx_authz/tests/rest_api/test_views.py @@ -39,10 +39,7 @@ class ViewTestMixin(BaseRolesTestCase): """Mixin providing common test utilities for view tests.""" @classmethod - def _assign_roles_to_users( - cls, - assignments: list[dict] | None = None, - ): + def _assign_roles_to_users(cls, assignments: list[dict] | None = None): """Helper method to assign roles to multiple users. This method can be used to assign a role to a single user or multiple users @@ -115,7 +112,7 @@ def setUpClass(cls): }, { "subject_name": "regular_7", - "role_name": "library_collaborator", + "role_name": "library_contributor", "scope_name": "lib:Org3:LIB3", }, { @@ -168,9 +165,9 @@ def setUp(self): ([{"action": permissions.VIEW_LIBRARY.identifier, "scope": "lib:Org1:LIB1"}], [True]), # Single permission - denied (scope not assigned to user) ([{"action": permissions.VIEW_LIBRARY.identifier, "scope": "lib:Org2:LIB2"}], [False]), - # # Single permission - denied (action not assigned to user) + # Single permission - denied (action not assigned to user) ([{"action": "edit_library", "scope": "lib:Org1:LIB1"}], [False]), - # # Multiple permissions - mixed results + # Multiple permissions - mixed results ( [ {"action": permissions.VIEW_LIBRARY.identifier, "scope": "lib:Org1:LIB1"}, @@ -793,9 +790,9 @@ def test_get_roles_pagination(self, query_params: dict, expected_count: int, has # Library Admin user ("regular_5", status.HTTP_200_OK), # Library Author user - # ("regular_6", status.HTTP_200_OK), # TODO: uncomment this when we have the explicit permissions - # Library Collaborator user - # ("regular_7", status.HTTP_200_OK), # TODO: uncomment this when we have the explicit permissions + ("regular_6", status.HTTP_200_OK), # TODO: uncomment this when we have the explicit permissions + # Library Contributor user + ("regular_7", status.HTTP_200_OK), # TODO: uncomment this when we have the explicit permissions # Library User user ("regular_8", status.HTTP_200_OK), # Regular user without permission From 62ce3de245eb3f730efdd2081f9ae4e1d9485db4 Mon Sep 17 00:00:00 2001 From: Bryann Valderrama Date: Tue, 28 Oct 2025 19:08:12 -0500 Subject: [PATCH 3/9] style: format code --- openedx_authz/rest_api/v1/views.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/openedx_authz/rest_api/v1/views.py b/openedx_authz/rest_api/v1/views.py index 3328db1e..7b85962a 100644 --- a/openedx_authz/rest_api/v1/views.py +++ b/openedx_authz/rest_api/v1/views.py @@ -121,10 +121,7 @@ def post(self, request: HttpRequest) -> Response: response_data.append({"action": action, "scope": scope, "allowed": allowed}) except ValueError as e: logger.error(f"Error validating permission for user {username}: {e}") - return Response( - data={"message": "Invalid scope format"}, - status=status.HTTP_400_BAD_REQUEST, - ) + return Response(data={"message": "Invalid scope format"}, status=status.HTTP_400_BAD_REQUEST) except Exception as e: # pylint: disable=broad-exception-caught logger.error(f"Error validating permission for user {username}: {e}") return Response( From dca3da23d0587e178b0edd3f96762395a2d83fcd Mon Sep 17 00:00:00 2001 From: Bryann Valderrama Date: Tue, 28 Oct 2025 19:20:55 -0500 Subject: [PATCH 4/9] chore: remove unnecessary comments --- openedx_authz/tests/rest_api/test_views.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openedx_authz/tests/rest_api/test_views.py b/openedx_authz/tests/rest_api/test_views.py index a196de09..fabd14fa 100644 --- a/openedx_authz/tests/rest_api/test_views.py +++ b/openedx_authz/tests/rest_api/test_views.py @@ -790,9 +790,9 @@ def test_get_roles_pagination(self, query_params: dict, expected_count: int, has # Library Admin user ("regular_5", status.HTTP_200_OK), # Library Author user - ("regular_6", status.HTTP_200_OK), # TODO: uncomment this when we have the explicit permissions + ("regular_6", status.HTTP_200_OK), # Library Contributor user - ("regular_7", status.HTTP_200_OK), # TODO: uncomment this when we have the explicit permissions + ("regular_7", status.HTTP_200_OK), # Library User user ("regular_8", status.HTTP_200_OK), # Regular user without permission From bb07ef0cb0c8ee289b39b58cae624b59efc0ec05 Mon Sep 17 00:00:00 2001 From: Bryann Valderrama Date: Wed, 29 Oct 2025 10:10:22 -0500 Subject: [PATCH 5/9] chore: remove load of policy in permission validation endpoint --- openedx_authz/rest_api/v1/views.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/openedx_authz/rest_api/v1/views.py b/openedx_authz/rest_api/v1/views.py index 7b85962a..787e6215 100644 --- a/openedx_authz/rest_api/v1/views.py +++ b/openedx_authz/rest_api/v1/views.py @@ -109,8 +109,6 @@ def post(self, request: HttpRequest) -> Response: serializer.is_valid(raise_exception=True) data = serializer.validated_data - AuthzEnforcer.get_enforcer().load_policy() - username = request.user.username response_data = [] for permission in data: From 5a39981524d583ffa8297428085a66fd3d866072 Mon Sep 17 00:00:00 2001 From: Bryann Valderrama Date: Wed, 29 Oct 2025 16:36:04 -0500 Subject: [PATCH 6/9] fix: load full policy in get role-user view --- openedx_authz/rest_api/v1/views.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/openedx_authz/rest_api/v1/views.py b/openedx_authz/rest_api/v1/views.py index 787e6215..972a0569 100644 --- a/openedx_authz/rest_api/v1/views.py +++ b/openedx_authz/rest_api/v1/views.py @@ -15,10 +15,8 @@ from rest_framework.views import APIView from openedx_authz import api -from openedx_authz.api.data import ScopeData from openedx_authz.constants import permissions from openedx_authz.engine.enforcer import AuthzEnforcer -from openedx_authz.engine.filter import Filter from openedx_authz.rest_api.data import RoleOperationError, RoleOperationStatus from openedx_authz.rest_api.decorators import authz_permissions, view_auth_classes from openedx_authz.rest_api.utils import ( @@ -259,8 +257,7 @@ def get(self, request: HttpRequest) -> Response: serializer.is_valid(raise_exception=True) query_params = serializer.validated_data - flt = Filter(v2=[ScopeData(external_key=query_params["scope"]).namespaced_key]) - AuthzEnforcer.get_enforcer().load_filtered_policy(flt) + AuthzEnforcer.get_enforcer().load_policy() user_role_assignments = api.get_all_user_role_assignments_in_scope(query_params["scope"]) usernames = {assignment.subject.username for assignment in user_role_assignments} From 6aa980cefef97ff7205d67b19610b89e5f40ea19 Mon Sep 17 00:00:00 2001 From: Bryann Valderrama Date: Wed, 29 Oct 2025 16:45:02 -0500 Subject: [PATCH 7/9] fix: load all policy in permission class and remove from views --- openedx_authz/rest_api/v1/permissions.py | 3 +++ openedx_authz/rest_api/v1/views.py | 9 --------- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/openedx_authz/rest_api/v1/permissions.py b/openedx_authz/rest_api/v1/permissions.py index 9fb0ad33..f4b26a8b 100644 --- a/openedx_authz/rest_api/v1/permissions.py +++ b/openedx_authz/rest_api/v1/permissions.py @@ -5,6 +5,7 @@ from rest_framework.permissions import BasePermission from openedx_authz import api +from openedx_authz.engine.enforcer import AuthzEnforcer class PermissionMeta(type(BasePermission)): @@ -182,6 +183,7 @@ def has_permission(self, request, view) -> bool: """ if request.user.is_superuser or request.user.is_staff: return True + AuthzEnforcer.get_enforcer().load_policy() return self._get_permission_instance(request).has_permission(request, view) def has_object_permission(self, request, view, obj) -> bool: @@ -198,6 +200,7 @@ def has_object_permission(self, request, view, obj) -> bool: """ if request.user.is_superuser or request.user.is_staff: return True + AuthzEnforcer.get_enforcer().load_policy() return self._get_permission_instance(request).has_object_permission(request, view, obj) diff --git a/openedx_authz/rest_api/v1/views.py b/openedx_authz/rest_api/v1/views.py index 972a0569..af7cfb89 100644 --- a/openedx_authz/rest_api/v1/views.py +++ b/openedx_authz/rest_api/v1/views.py @@ -16,7 +16,6 @@ from openedx_authz import api from openedx_authz.constants import permissions -from openedx_authz.engine.enforcer import AuthzEnforcer from openedx_authz.rest_api.data import RoleOperationError, RoleOperationStatus from openedx_authz.rest_api.decorators import authz_permissions, view_auth_classes from openedx_authz.rest_api.utils import ( @@ -257,8 +256,6 @@ def get(self, request: HttpRequest) -> Response: serializer.is_valid(raise_exception=True) query_params = serializer.validated_data - AuthzEnforcer.get_enforcer().load_policy() - user_role_assignments = api.get_all_user_role_assignments_in_scope(query_params["scope"]) usernames = {assignment.subject.username for assignment in user_role_assignments} context = {"user_map": get_user_map(usernames)} @@ -286,8 +283,6 @@ def put(self, request: HttpRequest) -> Response: serializer.is_valid(raise_exception=True) data = serializer.validated_data - AuthzEnforcer.get_enforcer().load_policy() - completed, errors = [], [] for user_identifier in data["users"]: response_dict = {"user_identifier": user_identifier} @@ -330,8 +325,6 @@ def put(self, request: HttpRequest) -> Response: @authz_permissions([permissions.MANAGE_LIBRARY_TEAM.identifier]) def delete(self, request: HttpRequest) -> Response: """Remove multiple users from a specific role within a scope.""" - AuthzEnforcer.get_enforcer().load_policy() - serializer = RemoveUsersFromRoleWithScopeSerializer(data=request.query_params) serializer.is_valid(raise_exception=True) data = serializer.validated_data @@ -438,8 +431,6 @@ def get(self, request: HttpRequest) -> Response: serializer.is_valid(raise_exception=True) query_params = serializer.validated_data - AuthzEnforcer.get_enforcer().load_policy() - generic_scope = get_generic_scope(query_params["scope"]) roles = api.get_role_definitions_in_scope(generic_scope) response_data = [] From d2c70b7a4e950788eabe2132164c55cc304348f9 Mon Sep 17 00:00:00 2001 From: Bryann Valderrama Date: Wed, 29 Oct 2025 18:23:54 -0500 Subject: [PATCH 8/9] chore: bump version to 0.12.0 --- CHANGELOG.rst | 8 +++++++- openedx_authz/__init__.py | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 4e003582..6900783f 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -14,7 +14,13 @@ Change Log Unreleased ********** -* +0.12.0 - 2025-10-30 +******************** + +Changed +======= + +* Load authorization policies in permission class. 0.11.2 - 2025-10-30 ******************** diff --git a/openedx_authz/__init__.py b/openedx_authz/__init__.py index d22c6082..26ecc77c 100644 --- a/openedx_authz/__init__.py +++ b/openedx_authz/__init__.py @@ -4,6 +4,6 @@ import os -__version__ = "0.11.2" +__version__ = "0.12.0" ROOT_DIRECTORY = os.path.dirname(os.path.abspath(__file__)) From c672817be303262faa85bb934abe5b3021762b63 Mon Sep 17 00:00:00 2001 From: Bryann Valderrama Date: Thu, 30 Oct 2025 08:51:47 -0500 Subject: [PATCH 9/9] fix: load policy in permission validation view --- openedx_authz/rest_api/v1/views.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openedx_authz/rest_api/v1/views.py b/openedx_authz/rest_api/v1/views.py index af7cfb89..a79c790d 100644 --- a/openedx_authz/rest_api/v1/views.py +++ b/openedx_authz/rest_api/v1/views.py @@ -16,6 +16,7 @@ from openedx_authz import api from openedx_authz.constants import permissions +from openedx_authz.engine.enforcer import AuthzEnforcer from openedx_authz.rest_api.data import RoleOperationError, RoleOperationStatus from openedx_authz.rest_api.decorators import authz_permissions, view_auth_classes from openedx_authz.rest_api.utils import ( @@ -102,6 +103,8 @@ class PermissionValidationMeView(APIView): ) def post(self, request: HttpRequest) -> Response: """Validate one or more permissions for the authenticated user.""" + AuthzEnforcer.get_enforcer().load_policy() + serializer = PermissionValidationSerializer(data=request.data, many=True) serializer.is_valid(raise_exception=True) data = serializer.validated_data