From c5262fb31598a48e1ee08643ead89d009e8f25ea Mon Sep 17 00:00:00 2001 From: Bryann Valderrama Date: Fri, 31 Oct 2025 16:55:53 -0500 Subject: [PATCH 01/11] feat: add custom matcher in casbin model --- openedx_authz/engine/config/model.conf | 2 +- openedx_authz/engine/enforcer.py | 4 +++ openedx_authz/engine/matcher.py | 37 +++++++++++++++++++++++++ openedx_authz/tests/test_enforcement.py | 4 +++ tests/test_models.py | 9 ------ 5 files changed, 46 insertions(+), 10 deletions(-) create mode 100644 openedx_authz/engine/matcher.py diff --git a/openedx_authz/engine/config/model.conf b/openedx_authz/engine/config/model.conf index 89bb2bd8..8d6acf7a 100644 --- a/openedx_authz/engine/config/model.conf +++ b/openedx_authz/engine/config/model.conf @@ -92,4 +92,4 @@ e = some(where (p.eft == allow)) && !some(where (p.eft == deny)) # 1. Subject must have role in scope OR global role # 2. Scope must match pattern # 3. Action must match OR inherit via action grouping -m = (g(r.sub, p.sub, r.scope) || g(r.sub, p.sub, "*")) && keyMatch(r.scope, p.scope) && (r.act == p.act || g2(p.act, r.act)) +m = custom_check(r.sub, r.act, r.scope) || (g(r.sub, p.sub, r.scope) || g(r.sub, p.sub, "*")) && keyMatch(r.scope, p.scope) && (r.act == p.act || g2(p.act, r.act)) diff --git a/openedx_authz/engine/enforcer.py b/openedx_authz/engine/enforcer.py index 866842a2..378aa42f 100644 --- a/openedx_authz/engine/enforcer.py +++ b/openedx_authz/engine/enforcer.py @@ -182,6 +182,9 @@ def _initialize_enforcer(cls) -> SyncedEnforcer: Returns: SyncedEnforcer: Configured Casbin enforcer with adapter and auto-sync """ + # Avoid circular import + from openedx_authz.engine.matcher import check_custom_conditions # pylint: disable=import-outside-toplevel + db_alias = getattr(settings, "CASBIN_DB_ALIAS", "default") try: @@ -195,5 +198,6 @@ def _initialize_enforcer(cls) -> SyncedEnforcer: adapter = ExtendedAdapter() enforcer = SyncedEnforcer(settings.CASBIN_MODEL, adapter) + enforcer.add_function("custom_check", check_custom_conditions) return enforcer diff --git a/openedx_authz/engine/matcher.py b/openedx_authz/engine/matcher.py new file mode 100644 index 00000000..aeb87a43 --- /dev/null +++ b/openedx_authz/engine/matcher.py @@ -0,0 +1,37 @@ +"""Custom condition checker. Note only used for data_library scope""" + +from django.contrib.auth import get_user_model + +from openedx_authz.api.data import UserData +from openedx_authz.rest_api.utils import get_user_by_username_or_email + +User = get_user_model() + + +def check_custom_conditions(request_user: str, request_action: str, request_scope: str) -> bool: # pylint: disable=unused-argument + """ + Evaluates custom, non-role-based conditions for library actions. + + Checks attribute-based conditions that don't rely on role assignments: + - Staff and superusers have full access + - create_library: requires granted course creator status + - view_library: allowed if library has public read enabled + + Args: + request_user (str): Namespaced user key + request_action (str): Namespaced action key + request_scope (str): Namespaced scope key + + Returns: + bool: True if the condition is satisfied, False otherwise + """ + try: + username = UserData(namespaced_key=request_user).external_key + user = get_user_by_username_or_email(username) + except User.DoesNotExist: + return False + + if user.is_staff or user.is_superuser: + return True + + return False diff --git a/openedx_authz/tests/test_enforcement.py b/openedx_authz/tests/test_enforcement.py index ccb2144e..94f4b560 100644 --- a/openedx_authz/tests/test_enforcement.py +++ b/openedx_authz/tests/test_enforcement.py @@ -10,10 +10,12 @@ from unittest import TestCase import casbin +import pytest from ddt import data, ddt, unpack from openedx_authz import ROOT_DIRECTORY from openedx_authz.constants import roles +from openedx_authz.engine.matcher import check_custom_conditions from openedx_authz.tests.test_utils import ( make_action_key, make_library_key, @@ -44,6 +46,7 @@ class AuthRequest(TypedDict): ] +@pytest.mark.django_db @ddt class CasbinEnforcementTestCase(TestCase): """ @@ -65,6 +68,7 @@ def setUpClass(cls) -> None: raise FileNotFoundError(f"Model file not found: {model_file}") cls.enforcer = casbin.Enforcer(model_file) + cls.enforcer.add_function("custom_check", check_custom_conditions) def _load_policy(self, policy: list[str]) -> None: """ diff --git a/tests/test_models.py b/tests/test_models.py index 38c29ce8..4239fd72 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -2,12 +2,3 @@ """ Tests for the `openedx-authz` models module. """ - -import pytest - - -@pytest.mark.skip(reason="Placeholder to allow pytest to succeed before real tests are in place.") -def test_placeholder(): - """ - TODO: Delete this test once there are real tests. - """ From 8d401e711e765bdac9b497833a3788e89947d4be Mon Sep 17 00:00:00 2001 From: Bryann Valderrama Date: Fri, 31 Oct 2025 17:04:20 -0500 Subject: [PATCH 02/11] refactor: enhance custom condition checks for authorization in matcher.py --- openedx_authz/engine/matcher.py | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/openedx_authz/engine/matcher.py b/openedx_authz/engine/matcher.py index aeb87a43..be4f7b68 100644 --- a/openedx_authz/engine/matcher.py +++ b/openedx_authz/engine/matcher.py @@ -2,7 +2,7 @@ from django.contrib.auth import get_user_model -from openedx_authz.api.data import UserData +from openedx_authz.api.data import ContentLibraryData, ScopeData, UserData from openedx_authz.rest_api.utils import get_user_by_username_or_email User = get_user_model() @@ -10,20 +10,21 @@ def check_custom_conditions(request_user: str, request_action: str, request_scope: str) -> bool: # pylint: disable=unused-argument """ - Evaluates custom, non-role-based conditions for library actions. + Evaluates custom, non-role-based conditions for authorization checks. - Checks attribute-based conditions that don't rely on role assignments: - - Staff and superusers have full access - - create_library: requires granted course creator status - - view_library: allowed if library has public read enabled + Checks attribute-based conditions that don't rely on role assignments. + Currently handles ContentLibraryData scopes by granting access to staff + and superusers. Args: - request_user (str): Namespaced user key - request_action (str): Namespaced action key - request_scope (str): Namespaced scope key + request_user (str): Namespaced user key (format: "user::") + request_action (str): Namespaced action key (format: "action::") + request_scope (str): Namespaced scope key (format: "scope_type::") Returns: - bool: True if the condition is satisfied, False otherwise + bool: True if the condition is satisfied (user is staff/superuser for + ContentLibraryData scopes), False otherwise (including when user + doesn't exist or scope type is not supported) """ try: username = UserData(namespaced_key=request_user).external_key @@ -31,7 +32,9 @@ def check_custom_conditions(request_user: str, request_action: str, request_scop except User.DoesNotExist: return False - if user.is_staff or user.is_superuser: - return True + scope = ScopeData(namespaced_key=request_scope) + + if isinstance(scope, ContentLibraryData): + return user.is_staff or user.is_superuser return False From 6458b59757f793d410c11fdba15b48ca796bbab2 Mon Sep 17 00:00:00 2001 From: Bryann Valderrama Date: Fri, 31 Oct 2025 17:34:07 -0500 Subject: [PATCH 03/11] feat: add tests for staff and superuser automatic permission grants --- openedx_authz/tests/rest_api/test_views.py | 15 +++++ openedx_authz/tests/test_enforcement.py | 77 ++++++++++++++++++++++ 2 files changed, 92 insertions(+) diff --git a/openedx_authz/tests/rest_api/test_views.py b/openedx_authz/tests/rest_api/test_views.py index fabd14fa..88982107 100644 --- a/openedx_authz/tests/rest_api/test_views.py +++ b/openedx_authz/tests/rest_api/test_views.py @@ -195,6 +195,21 @@ def test_permission_validation_success(self, request_data: list[dict], permissio self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data, expected_response) + def test_permission_validation_staff_superuser_access(self): + """Test permission validation for staff and superuser users.""" + self.client.force_authenticate(user=self.admin_user) + request_data = [ + {"action": perm.identifier, "scope": "lib:AnyOrg1:ANYLIB1"} for perm in roles.LIBRARY_ADMIN_PERMISSIONS + ] + expected_response = request_data.copy() + for item in expected_response: + item["allowed"] = True + + response = self.client.post(self.url, data=request_data, format="json") + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data, expected_response) + @data( # Single permission [{"action": "edit_library"}], diff --git a/openedx_authz/tests/test_enforcement.py b/openedx_authz/tests/test_enforcement.py index 94f4b560..c3c6f500 100644 --- a/openedx_authz/tests/test_enforcement.py +++ b/openedx_authz/tests/test_enforcement.py @@ -12,6 +12,7 @@ import casbin import pytest from ddt import data, ddt, unpack +from django.contrib.auth import get_user_model from openedx_authz import ROOT_DIRECTORY from openedx_authz.constants import roles @@ -24,6 +25,8 @@ make_user_key, ) +User = get_user_model() + class AuthRequest(TypedDict): """ @@ -577,3 +580,77 @@ def test_wildcard_library_access(self, scope: str, expected_result: bool): "expected_result": expected_result, } self._test_enforcement(self.POLICY, request) + + +@pytest.mark.django_db +@ddt +class StaffSuperuserAccessTests(CasbinEnforcementTestCase): + """ + Tests for staff and superuser automatic permission grants via custom_check. + + This test class verifies that staff members and superusers are automatically + granted access to ContentLibrary scopes through the check_custom_conditions function, + without requiring explicit role assignments. + """ + + # Empty policy - no role assignments for staff/superuser users + POLICY = [] + + def setUp(self) -> None: + """Set up the test environment.""" + super().setUp() + User.objects.create_user(username="staff_user", email="staff@example.com", password="test", is_staff=True) + User.objects.create_superuser(username="superuser", email="super@example.com", password="test") + User.objects.create_user(username="regular_user", email="regular@example.com", password="test") + + @data( + # Staff user has automatic access to any library scope + ( + make_user_key("staff_user"), + make_action_key("view_library"), + make_library_key("lib:TestOrg:TestLib"), + True, + ), + ( + make_user_key("staff_user"), + make_action_key("edit_library"), + make_library_key("lib:AnyOrg:AnyLib"), + True, + ), + # Superuser has automatic access to any library scope + ( + make_user_key("superuser"), + make_action_key("view_library"), + make_library_key("lib:TestOrg:TestLib"), + True, + ), + ( + make_user_key("superuser"), + make_action_key("delete_library"), + make_library_key("lib:AnyOrg:AnyLib"), + True, + ), + # Regular user without role assignment has no access + ( + make_user_key("regular_user"), + make_action_key("view_library"), + make_library_key("lib:TestOrg:TestLib"), + False, + ), + ) + @unpack + def test_staff_superuser_guaranteed_permissions(self, subject: str, action: str, scope: str, expected_result: bool): + """Test that staff and superusers have guaranteed permissions for ContentLibrary scopes. + + This test validates that: + - Staff users automatically have access to all library scopes without role assignments + - Superusers automatically have access to all library scopes without role assignments + - Regular users require explicit role assignments to access libraries + - Access is granted through the custom_check matcher function + + Expected result: + - Staff and superusers can perform any action on any ContentLibrary scope + - Regular users are denied access without role assignments + """ + request = {"subject": subject, "action": action, "scope": scope, "expected_result": expected_result} + self._test_enforcement(self.POLICY, request) From 1d7087ac0ec1e5bacd8caa19c2737fbdf67fe1b6 Mon Sep 17 00:00:00 2001 From: Bryann Valderrama Date: Fri, 31 Oct 2025 17:43:05 -0500 Subject: [PATCH 04/11] test: validate permissions for not library scopes --- openedx_authz/tests/rest_api/test_views.py | 27 +++++++++++++++++----- 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/openedx_authz/tests/rest_api/test_views.py b/openedx_authz/tests/rest_api/test_views.py index 88982107..ab135fa2 100644 --- a/openedx_authz/tests/rest_api/test_views.py +++ b/openedx_authz/tests/rest_api/test_views.py @@ -195,15 +195,30 @@ def test_permission_validation_success(self, request_data: list[dict], permissio self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data, expected_response) - def test_permission_validation_staff_superuser_access(self): - """Test permission validation for staff and superuser users.""" + @data( + ("lib:AnyOrg1:ANYLIB1", True), + ("lib:AnyOrg2:ANYLIB2", True), + ("lib:AnyOrg3:ANYLIB3", True), + ("sc:AnyScope1", False), + ) + @unpack + def test_permission_validation_staff_superuser_access(self, scope: str, expected_result: bool): + """Test that staff/superuser users have guaranteed permissions for ContentLibrary scopes. + + Test cases: + - ContentLibrary scopes (lib:*): Staff/superuser automatically allowed + - Generic scopes (sc:*): No automatic access granted + + Expected result: + - Returns 200 OK status + - For library scopes: All permissions are allowed (True) + - For non-library scopes: Permissions follow normal authorization (False) + """ self.client.force_authenticate(user=self.admin_user) - request_data = [ - {"action": perm.identifier, "scope": "lib:AnyOrg1:ANYLIB1"} for perm in roles.LIBRARY_ADMIN_PERMISSIONS - ] + request_data = [{"action": perm.identifier, "scope": scope} for perm in roles.LIBRARY_ADMIN_PERMISSIONS] expected_response = request_data.copy() for item in expected_response: - item["allowed"] = True + item["allowed"] = expected_result response = self.client.post(self.url, data=request_data, format="json") From 4bd8f2d0edfe581c5445b0fc22815f0c0d8b71e3 Mon Sep 17 00:00:00 2001 From: Maria Grimaldi Date: Wed, 5 Nov 2025 13:50:44 +0100 Subject: [PATCH 05/11] refactor: drop /tests package in favor of openedx_authz/tests --- tox.ini | 1 - 1 file changed, 1 deletion(-) diff --git a/tox.ini b/tox.ini index 6326bb9d..bb46961f 100644 --- a/tox.ini +++ b/tox.ini @@ -73,7 +73,6 @@ deps = setuptools -r{toxinidir}/requirements/quality.txt commands = - touch tests/__init__.py pylint openedx_authz tests test_utils manage.py setup.py rm tests/__init__.py ruff check openedx_authz tests test_utils manage.py setup.py From c3f0d69977bf83a283d6944ce7a248570cee5064 Mon Sep 17 00:00:00 2001 From: Maria Grimaldi Date: Wed, 5 Nov 2025 18:20:15 +0100 Subject: [PATCH 06/11] refactor: address PR reviews --- openedx_authz/engine/config/model.conf | 2 +- openedx_authz/engine/enforcer.py | 7 ++----- openedx_authz/engine/matcher.py | 2 +- openedx_authz/tests/test_enforcement.py | 10 +++++----- 4 files changed, 9 insertions(+), 12 deletions(-) diff --git a/openedx_authz/engine/config/model.conf b/openedx_authz/engine/config/model.conf index 8d6acf7a..b4707590 100644 --- a/openedx_authz/engine/config/model.conf +++ b/openedx_authz/engine/config/model.conf @@ -92,4 +92,4 @@ e = some(where (p.eft == allow)) && !some(where (p.eft == deny)) # 1. Subject must have role in scope OR global role # 2. Scope must match pattern # 3. Action must match OR inherit via action grouping -m = custom_check(r.sub, r.act, r.scope) || (g(r.sub, p.sub, r.scope) || g(r.sub, p.sub, "*")) && keyMatch(r.scope, p.scope) && (r.act == p.act || g2(p.act, r.act)) +m = is_staff_or_superuser(r.sub, r.act, r.scope) || (g(r.sub, p.sub, r.scope) || g(r.sub, p.sub, "*")) && keyMatch(r.scope, p.scope) && (r.act == p.act || g2(p.act, r.act)) diff --git a/openedx_authz/engine/enforcer.py b/openedx_authz/engine/enforcer.py index 378aa42f..11702214 100644 --- a/openedx_authz/engine/enforcer.py +++ b/openedx_authz/engine/enforcer.py @@ -22,7 +22,7 @@ from django.conf import settings from openedx_authz.engine.adapter import ExtendedAdapter - +from openedx_authz.engine.matcher import is_admin_or_superuser_check def libraries_v2_enabled() -> bool: """Dummy toggle that is always enabled.""" @@ -182,9 +182,6 @@ def _initialize_enforcer(cls) -> SyncedEnforcer: Returns: SyncedEnforcer: Configured Casbin enforcer with adapter and auto-sync """ - # Avoid circular import - from openedx_authz.engine.matcher import check_custom_conditions # pylint: disable=import-outside-toplevel - db_alias = getattr(settings, "CASBIN_DB_ALIAS", "default") try: @@ -198,6 +195,6 @@ def _initialize_enforcer(cls) -> SyncedEnforcer: adapter = ExtendedAdapter() enforcer = SyncedEnforcer(settings.CASBIN_MODEL, adapter) - enforcer.add_function("custom_check", check_custom_conditions) + enforcer.add_function("is_staff_or_superuser", is_admin_or_superuser_check) return enforcer diff --git a/openedx_authz/engine/matcher.py b/openedx_authz/engine/matcher.py index be4f7b68..dea6373d 100644 --- a/openedx_authz/engine/matcher.py +++ b/openedx_authz/engine/matcher.py @@ -8,7 +8,7 @@ User = get_user_model() -def check_custom_conditions(request_user: str, request_action: str, request_scope: str) -> bool: # pylint: disable=unused-argument +def is_admin_or_superuser_check(request_user: str, request_action: str, request_scope: str) -> bool: # pylint: disable=unused-argument """ Evaluates custom, non-role-based conditions for authorization checks. diff --git a/openedx_authz/tests/test_enforcement.py b/openedx_authz/tests/test_enforcement.py index c3c6f500..3496fe3a 100644 --- a/openedx_authz/tests/test_enforcement.py +++ b/openedx_authz/tests/test_enforcement.py @@ -16,7 +16,7 @@ from openedx_authz import ROOT_DIRECTORY from openedx_authz.constants import roles -from openedx_authz.engine.matcher import check_custom_conditions +from openedx_authz.engine.matcher import is_admin_or_superuser_check from openedx_authz.tests.test_utils import ( make_action_key, make_library_key, @@ -71,7 +71,7 @@ def setUpClass(cls) -> None: raise FileNotFoundError(f"Model file not found: {model_file}") cls.enforcer = casbin.Enforcer(model_file) - cls.enforcer.add_function("custom_check", check_custom_conditions) + cls.enforcer.add_function("is_staff_or_superuser", is_admin_or_superuser_check) def _load_policy(self, policy: list[str]) -> None: """ @@ -586,10 +586,10 @@ def test_wildcard_library_access(self, scope: str, expected_result: bool): @ddt class StaffSuperuserAccessTests(CasbinEnforcementTestCase): """ - Tests for staff and superuser automatic permission grants via custom_check. + Tests for staff and superuser automatic permission grants via is_staff_or_superuser. This test class verifies that staff members and superusers are automatically - granted access to ContentLibrary scopes through the check_custom_conditions function, + granted access to ContentLibrary scopes through the is_admin_or_superuser_check function, without requiring explicit role assignments. """ @@ -646,7 +646,7 @@ def test_staff_superuser_guaranteed_permissions(self, subject: str, action: str, - Staff users automatically have access to all library scopes without role assignments - Superusers automatically have access to all library scopes without role assignments - Regular users require explicit role assignments to access libraries - - Access is granted through the custom_check matcher function + - Access is granted through the is_staff_or_superuser matcher function Expected result: - Staff and superusers can perform any action on any ContentLibrary scope From 4b15aeb62ab549d629a797bd80e19f3bdd3fe0c6 Mon Sep 17 00:00:00 2001 From: Maria Grimaldi Date: Thu, 6 Nov 2025 11:30:20 +0100 Subject: [PATCH 07/11] refactor: drop sc: references --- 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 ab135fa2..4a962d3a 100644 --- a/openedx_authz/tests/rest_api/test_views.py +++ b/openedx_authz/tests/rest_api/test_views.py @@ -199,7 +199,7 @@ def test_permission_validation_success(self, request_data: list[dict], permissio ("lib:AnyOrg1:ANYLIB1", True), ("lib:AnyOrg2:ANYLIB2", True), ("lib:AnyOrg3:ANYLIB3", True), - ("sc:AnyScope1", False), + ("global:AnyScope1", False), ) @unpack def test_permission_validation_staff_superuser_access(self, scope: str, expected_result: bool): @@ -207,7 +207,7 @@ def test_permission_validation_staff_superuser_access(self, scope: str, expected Test cases: - ContentLibrary scopes (lib:*): Staff/superuser automatically allowed - - Generic scopes (sc:*): No automatic access granted + - Generic scopes (global:*): No automatic access granted Expected result: - Returns 200 OK status From 6d039c97774a57668f33253a07355b6d2bb2346d Mon Sep 17 00:00:00 2001 From: Maria Grimaldi Date: Thu, 6 Nov 2025 11:40:35 +0100 Subject: [PATCH 08/11] refactor: add test case for non-existent lib scope --- openedx_authz/tests/test_enforcement.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/openedx_authz/tests/test_enforcement.py b/openedx_authz/tests/test_enforcement.py index 3496fe3a..13adfe57 100644 --- a/openedx_authz/tests/test_enforcement.py +++ b/openedx_authz/tests/test_enforcement.py @@ -637,6 +637,13 @@ def setUp(self) -> None: make_library_key("lib:TestOrg:TestLib"), False, ), + # Non existent library scope access denied + ( + make_user_key("regular_user"), + make_action_key("view_library"), + make_library_key("lib:NonExistent:NoLib"), + False, + ), ) @unpack def test_staff_superuser_guaranteed_permissions(self, subject: str, action: str, scope: str, expected_result: bool): From fd8dca3ab3848126d9131f51eb08f8f833a78e7c Mon Sep 17 00:00:00 2001 From: Maria Grimaldi Date: Thu, 6 Nov 2025 11:48:30 +0100 Subject: [PATCH 09/11] refactor: go back to previous configuration for tox.ini --- tox.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/tox.ini b/tox.ini index bb46961f..6326bb9d 100644 --- a/tox.ini +++ b/tox.ini @@ -73,6 +73,7 @@ deps = setuptools -r{toxinidir}/requirements/quality.txt commands = + touch tests/__init__.py pylint openedx_authz tests test_utils manage.py setup.py rm tests/__init__.py ruff check openedx_authz tests test_utils manage.py setup.py From f423be1a41cfa91724c4d718929e383bfdc5575f Mon Sep 17 00:00:00 2001 From: Maria Grimaldi Date: Thu, 6 Nov 2025 11:51:56 +0100 Subject: [PATCH 10/11] refactor: address quality issues --- openedx_authz/engine/enforcer.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openedx_authz/engine/enforcer.py b/openedx_authz/engine/enforcer.py index 11702214..ee58b81f 100644 --- a/openedx_authz/engine/enforcer.py +++ b/openedx_authz/engine/enforcer.py @@ -24,6 +24,7 @@ from openedx_authz.engine.adapter import ExtendedAdapter from openedx_authz.engine.matcher import is_admin_or_superuser_check + def libraries_v2_enabled() -> bool: """Dummy toggle that is always enabled.""" return True From b5de0c8b6ec1d83b2d1fea27bd0b93510554f83e Mon Sep 17 00:00:00 2001 From: Maria Grimaldi Date: Tue, 11 Nov 2025 11:25:33 +0100 Subject: [PATCH 11/11] chore: update docs for release --- CHANGELOG.rst | 10 +++++++++- openedx_authz/__init__.py | 2 +- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 1c269dbd..d45e9cf2 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -14,7 +14,15 @@ Change Log Unreleased ********** -0.13.1 - 2025-11-06 +0.14.0 - 2025-11-10 +******************** + +Added +===== + +* Implement custom matcher to check for staff and superuser status. + +0.13.1 - 2025-11-10 ******************** Fixed diff --git a/openedx_authz/__init__.py b/openedx_authz/__init__.py index 6438c962..7a509bf1 100644 --- a/openedx_authz/__init__.py +++ b/openedx_authz/__init__.py @@ -4,6 +4,6 @@ import os -__version__ = "0.13.1" +__version__ = "0.14.0" ROOT_DIRECTORY = os.path.dirname(os.path.abspath(__file__))