From 38a0573687f58faeb84bc946326bf69d7737a178 Mon Sep 17 00:00:00 2001 From: Maria Grimaldi Date: Fri, 31 Oct 2025 17:25:16 +0100 Subject: [PATCH 1/8] refactor: consider global scope wildcard when instantiating scope --- openedx_authz/api/data.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/openedx_authz/api/data.py b/openedx_authz/api/data.py index 5fa39c05..95d69ee5 100644 --- a/openedx_authz/api/data.py +++ b/openedx_authz/api/data.py @@ -181,6 +181,11 @@ def __call__(cls, *args, **kwargs): if cls is not ScopeData: return super().__call__(*args, **kwargs) + # When working with global scopes, we can't determine subclass with an external_key since + # a global scope it's not attached to a specific resource type. So we only use namespaced_key + if kwargs.get("external_key") == GENERIC_SCOPE_WILDCARD: + return super().__call__(*args, **kwargs) + if "namespaced_key" in kwargs: scope_cls = cls.get_subclass_by_namespaced_key(kwargs["namespaced_key"]) return super(ScopeMeta, scope_cls).__call__(*args, **kwargs) From 5bc85e1405462e10c9a454b5c4c929036ddf8bf6 Mon Sep 17 00:00:00 2001 From: Maria Grimaldi Date: Fri, 31 Oct 2025 17:27:37 +0100 Subject: [PATCH 2/8] docs: update docstring with latest changes --- openedx_authz/api/data.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openedx_authz/api/data.py b/openedx_authz/api/data.py index 95d69ee5..21f2fad9 100644 --- a/openedx_authz/api/data.py +++ b/openedx_authz/api/data.py @@ -182,7 +182,8 @@ def __call__(cls, *args, **kwargs): return super().__call__(*args, **kwargs) # When working with global scopes, we can't determine subclass with an external_key since - # a global scope it's not attached to a specific resource type. So we only use namespaced_key + # a global scope it's not attached to a specific resource type. So we only use * as an + # an external_key to mean generic scope which maps to base ScopeData class. if kwargs.get("external_key") == GENERIC_SCOPE_WILDCARD: return super().__call__(*args, **kwargs) From 5e8453dfb438861fd0c6bbe9094a436400e2ff99 Mon Sep 17 00:00:00 2001 From: Maria Grimaldi Date: Fri, 31 Oct 2025 17:53:19 +0100 Subject: [PATCH 3/8] test: add a test case for wildcard scope --- openedx_authz/tests/api/test_data.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/openedx_authz/tests/api/test_data.py b/openedx_authz/tests/api/test_data.py index 614d6cb3..221bf5eb 100644 --- a/openedx_authz/tests/api/test_data.py +++ b/openedx_authz/tests/api/test_data.py @@ -345,6 +345,27 @@ def test_empty_external_key_raises_value_error(self): with self.assertRaises(ValueError): SubjectData(external_key="") + def test_scope_data_with_wildcard_external_key(self): + """Test that ScopeData instantiated with wildcard (*) returns base ScopeData. + + When using the global scope wildcard '*', the metaclass should return a base + ScopeData instance rather than attempting subclass determination. + + Expected Result: + - ScopeData(external_key='*') creates base ScopeData instance + - namespaced_key is 'sc^*' + - No subclass determination occurs + """ + scope = ScopeData(external_key="*") + + expected_namespaced = f"{ScopeData.NAMESPACE}{ScopeData.SEPARATOR}*" + + self.assertIsInstance(scope, ScopeData) + # Ensure it's exactly ScopeData, not a subclass + self.assertEqual(type(scope), ScopeData) + self.assertEqual(scope.external_key, "*") + self.assertEqual(scope.namespaced_key, expected_namespaced) + @ddt class TestDataRepresentation(TestCase): From 4c1577aa38ac96e2cce2907c80fee71fea8d5ab5 Mon Sep 17 00:00:00 2001 From: Maria Grimaldi Date: Fri, 31 Oct 2025 18:17:59 +0100 Subject: [PATCH 4/8] docs: update scope docstring --- openedx_authz/api/data.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/openedx_authz/api/data.py b/openedx_authz/api/data.py index 21f2fad9..87cda28c 100644 --- a/openedx_authz/api/data.py +++ b/openedx_authz/api/data.py @@ -184,6 +184,8 @@ def __call__(cls, *args, **kwargs): # When working with global scopes, we can't determine subclass with an external_key since # a global scope it's not attached to a specific resource type. So we only use * as an # an external_key to mean generic scope which maps to base ScopeData class. + # The only remaining issue is that internally the namespace key used in policies will be + # The generic scope namespace (sc^*), so we need to handle that case here. if kwargs.get("external_key") == GENERIC_SCOPE_WILDCARD: return super().__call__(*args, **kwargs) @@ -309,6 +311,11 @@ class ScopeData(AuthZData, metaclass=ScopeMeta): 'sc^generic_scope' """ + # The 'sc' namespace is used for generic scopes that aren't tied to a specific resource type. + # This base class supports: + # 1. Global scopes (external_key='*') that apply across all resource types + # 2. Custom generic scopes that don't map to specific domain objects + # Subclasses like ContentLibraryData ('lib') represent concrete resource types. NAMESPACE: ClassVar[str] = "sc" @classmethod From eb540c5f6af4b7003662ff7c3efd14f106c2655b Mon Sep 17 00:00:00 2001 From: "Maria Grimaldi (Majo)" Date: Mon, 3 Nov 2025 12:45:29 +0100 Subject: [PATCH 5/8] docs: Apply suggestion from @rodmgwgu Co-authored-by: Rodrigo Mendez <117670175+rodmgwgu@users.noreply.github.com> --- openedx_authz/api/data.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openedx_authz/api/data.py b/openedx_authz/api/data.py index 87cda28c..3228609d 100644 --- a/openedx_authz/api/data.py +++ b/openedx_authz/api/data.py @@ -182,7 +182,7 @@ def __call__(cls, *args, **kwargs): return super().__call__(*args, **kwargs) # When working with global scopes, we can't determine subclass with an external_key since - # a global scope it's not attached to a specific resource type. So we only use * as an + # a global scope it's not attached to a specific resource type. So we only use * as # an external_key to mean generic scope which maps to base ScopeData class. # The only remaining issue is that internally the namespace key used in policies will be # The generic scope namespace (sc^*), so we need to handle that case here. From 685ebb1a4eac0e0509a72b5c5207c7e18fe62632 Mon Sep 17 00:00:00 2001 From: Maria Grimaldi Date: Tue, 4 Nov 2025 12:30:13 +0100 Subject: [PATCH 6/8] refactor: use global namespace instead of sc --- openedx_authz/api/data.py | 28 ++++++++++++------------ openedx_authz/rest_api/v1/permissions.py | 12 +++++----- openedx_authz/rest_api/v1/serializers.py | 2 +- openedx_authz/tests/api/test_data.py | 28 ++++++++++++------------ openedx_authz/tests/api/test_roles.py | 16 +++++++------- 5 files changed, 43 insertions(+), 43 deletions(-) diff --git a/openedx_authz/api/data.py b/openedx_authz/api/data.py index 3228609d..4b9b00c7 100644 --- a/openedx_authz/api/data.py +++ b/openedx_authz/api/data.py @@ -185,7 +185,7 @@ def __call__(cls, *args, **kwargs): # a global scope it's not attached to a specific resource type. So we only use * as # an external_key to mean generic scope which maps to base ScopeData class. # The only remaining issue is that internally the namespace key used in policies will be - # The generic scope namespace (sc^*), so we need to handle that case here. + # The global scope namespace (global^*), so we need to handle that case here. if kwargs.get("external_key") == GENERIC_SCOPE_WILDCARD: return super().__call__(*args, **kwargs) @@ -206,7 +206,7 @@ def get_subclass_by_namespaced_key(mcs, namespaced_key: str) -> Type["ScopeData" Extracts the namespace prefix (before '^') and returns the registered subclass. Args: - namespaced_key: The namespaced key (e.g., 'lib^lib:DemoX:CSPROB', 'sc^generic'). + namespaced_key: The namespaced key (e.g., 'lib^lib:DemoX:CSPROB', 'global^generic'). Returns: The ScopeData subclass for the namespace, or ScopeData if namespace not recognized. @@ -214,7 +214,7 @@ def get_subclass_by_namespaced_key(mcs, namespaced_key: str) -> Type["ScopeData" Examples: >>> ScopeMeta.get_subclass_by_namespaced_key('lib^lib:DemoX:CSPROB') - >>> ScopeMeta.get_subclass_by_namespaced_key('sc^generic') + >>> ScopeMeta.get_subclass_by_namespaced_key('global^generic') """ # TODO: Default separator, can't access directly from class so made it a constant @@ -232,7 +232,7 @@ def get_subclass_by_external_key(mcs, external_key: str) -> Type["ScopeData"]: the key format using the subclass's validate_external_key method. Args: - external_key: The external key (e.g., 'lib:DemoX:CSPROB', 'sc:generic'). + external_key: The external key (e.g., 'lib:DemoX:CSPROB', 'global:generic'). Returns: The ScopeData subclass corresponding to the namespace. @@ -271,11 +271,11 @@ def get_all_namespaces(mcs) -> dict[str, Type["ScopeData"]]: Returns: dict[str, Type["ScopeData"]]: A dictionary of all namespace prefixes registered in the scope registry. - Each namespace corresponds to a ScopeData subclass (e.g., 'lib', 'sc'). + Each namespace corresponds to a ScopeData subclass (e.g., 'lib', 'global'). Examples: >>> ScopeMeta.get_all_namespaces() - {'sc': ScopeData, 'lib': ContentLibraryData, 'org': OrganizationData} + {'global': ScopeData, 'lib': ContentLibraryData, 'org': OrganizationData} """ return mcs.scope_registry @@ -301,22 +301,22 @@ class ScopeData(AuthZData, metaclass=ScopeMeta): and not tied to any specific scope type, holding attributes common to all scopes. Attributes: - NAMESPACE: 'sc' for generic scopes. + NAMESPACE: 'global' for generic scopes. external_key: The scope identifier without namespace (e.g., 'generic_scope'). - namespaced_key: The scope identifier with namespace (e.g., 'sc^generic_scope'). + namespaced_key: The scope identifier with namespace (e.g., 'global^generic_scope'). Examples: >>> scope = ScopeData(external_key='generic_scope') >>> scope.namespaced_key - 'sc^generic_scope' + 'global^generic_scope' """ - # The 'sc' namespace is used for generic scopes that aren't tied to a specific resource type. + # The 'global' namespace is used for scopes that aren't tied to a specific resource type. # This base class supports: - # 1. Global scopes (external_key='*') that apply across all resource types - # 2. Custom generic scopes that don't map to specific domain objects - # Subclasses like ContentLibraryData ('lib') represent concrete resource types. - NAMESPACE: ClassVar[str] = "sc" + # 1. Global wildcard scopes (external_key='*') that apply across all resource types + # 2. Custom global scopes that don't map to specific domain objects (e.g., 'global:some_scope') + # Subclasses like ContentLibraryData ('lib') represent concrete resource types with their own namespaces. + NAMESPACE: ClassVar[str] = "global" @classmethod def validate_external_key(cls, _: str) -> bool: diff --git a/openedx_authz/rest_api/v1/permissions.py b/openedx_authz/rest_api/v1/permissions.py index f4b26a8b..5193fb46 100644 --- a/openedx_authz/rest_api/v1/permissions.py +++ b/openedx_authz/rest_api/v1/permissions.py @@ -30,7 +30,7 @@ def get_permission_class(mcs, namespace: str) -> type["BaseScopePermission"]: """Retrieve the permission class for the given namespace. Args: - namespace: The namespace identifier (e.g., 'lib', 'sc'). + namespace: The namespace identifier (e.g., 'lib', 'global'). Returns: type["BaseScopePermission"]: The permission class for the namespace, @@ -54,8 +54,8 @@ class BaseScopePermission(BasePermission, metaclass=PermissionMeta): specific authorization logic for their scope types. """ - NAMESPACE: ClassVar[str] = "sc" - """The namespace identifier for this permission class. Default ``sc`` for generic scopes.""" + NAMESPACE: ClassVar[str] = "global" + """The namespace identifier for this permission class. Default ``global`` for generic scopes.""" def get_scope_value(self, request) -> str | None: """Extract the scope value from the request. @@ -78,7 +78,7 @@ def get_scope_namespace(self, request) -> str: request: The Django REST framework request object. Returns: - str: The scope namespace (e.g., 'lib', 'sc'). + str: The scope namespace (e.g., 'lib', 'global'). Examples: >>> request.data = {"scope": "lib:DemoX:CSPROB"} @@ -86,7 +86,7 @@ def get_scope_namespace(self, request) -> str: 'lib' >>> request.data = {} >>> permission.get_scope_namespace(request) - 'sc' + 'global' """ scope_value = self.get_scope_value(request) if not scope_value: @@ -137,7 +137,7 @@ class DynamicScopePermission(BaseScopePermission): >>> request.data = {"scope": "lib:DemoX:CSPROB"} >>> ContentLibraryPermission.has_permission(request, view) >>> # For a generic scope request, this will delegate to BaseScopePermission - >>> request.data = {"scope": "sc:generic"} + >>> request.data = {"scope": "global:generic"} >>> BaseScopePermission.has_permission(request, view) Note: diff --git a/openedx_authz/rest_api/v1/serializers.py b/openedx_authz/rest_api/v1/serializers.py index 5c1d3e88..df920368 100644 --- a/openedx_authz/rest_api/v1/serializers.py +++ b/openedx_authz/rest_api/v1/serializers.py @@ -135,7 +135,7 @@ def validate_scope(self, value: str) -> api.ScopeData: returns an instance of the appropriate ScopeData subclass. Args: - value: The scope string to validate (e.g., 'lib', 'sc', 'org'). + value: The scope string to validate (e.g., 'lib', 'global', 'org'). Returns: ScopeData: An instance of the appropriate ScopeData subclass for the scope. diff --git a/openedx_authz/tests/api/test_data.py b/openedx_authz/tests/api/test_data.py index 221bf5eb..7f44e75e 100644 --- a/openedx_authz/tests/api/test_data.py +++ b/openedx_authz/tests/api/test_data.py @@ -155,7 +155,7 @@ def test_scope_data_direct_instantiation_with_namespaced_key(self): """Test that ScopeData can be instantiated with namespaced_key. Expected Result: - - ScopeData(namespaced_key='sc^generic') creates ScopeData instance + - ScopeData(namespaced_key='global^generic') creates ScopeData instance """ namespaced_key = f"{ScopeData.NAMESPACE}{ScopeData.SEPARATOR}generic" @@ -222,17 +222,17 @@ def test_scope_data_registration(self): """Test that ScopeData and its subclasses are registered correctly. Expected Result: - - 'sc' namespace maps to ScopeData class + - 'global' namespace maps to ScopeData class - 'lib' namespace maps to ContentLibraryData class """ - self.assertIn("sc", ScopeData.scope_registry) - self.assertIs(ScopeData.scope_registry["sc"], ScopeData) + self.assertIn("global", ScopeData.scope_registry) + self.assertIs(ScopeData.scope_registry["global"], ScopeData) self.assertIn("lib", ScopeData.scope_registry) self.assertIs(ScopeData.scope_registry["lib"], ContentLibraryData) @data( ("lib^lib:DemoX:CSPROB", ContentLibraryData), - ("sc^generic_scope", ScopeData), + ("global^generic_scope", ScopeData), ) @unpack def test_dynamic_instantiation_via_namespaced_key(self, namespaced_key, expected_class): @@ -240,7 +240,7 @@ def test_dynamic_instantiation_via_namespaced_key(self, namespaced_key, expected Expected Result: - ScopeData(namespaced_key='lib^...') returns ContentLibraryData instance - - ScopeData(namespaced_key='sc^...') returns ScopeData instance + - ScopeData(namespaced_key='global^...') returns ScopeData instance """ instance = ScopeData(namespaced_key=namespaced_key) @@ -249,7 +249,7 @@ def test_dynamic_instantiation_via_namespaced_key(self, namespaced_key, expected @data( ("lib^lib:DemoX:CSPROB", ContentLibraryData), - ("sc^generic", ScopeData), + ("global^generic", ScopeData), ("unknown^something", ScopeData), ) @unpack @@ -258,7 +258,7 @@ def test_get_subclass_by_namespaced_key(self, namespaced_key, expected_class): Expected Result: - 'lib^...' returns ContentLibraryData - - 'sc^...' returns ScopeData + - 'global^...' returns ScopeData - 'unknown^...' returns ScopeData (fallback) """ subclass = ScopeMeta.get_subclass_by_namespaced_key(namespaced_key) @@ -268,7 +268,7 @@ def test_get_subclass_by_namespaced_key(self, namespaced_key, expected_class): @data( ("lib:DemoX:CSPROB", ContentLibraryData), ("lib:edX:Demo", ContentLibraryData), - ("sc:generic_scope", ScopeData), + ("global:generic_scope", ScopeData), ) @unpack def test_get_subclass_by_external_key(self, external_key, expected_class): @@ -276,7 +276,7 @@ def test_get_subclass_by_external_key(self, external_key, expected_class): Expected Result: - 'lib:...' returns ContentLibraryData - - 'sc:...' returns ScopeData + - 'global:...' returns ScopeData """ subclass = ScopeMeta.get_subclass_by_external_key(external_key) @@ -319,12 +319,12 @@ def test_base_scope_data_with_external_key(self): - ScopeData(external_key='...') creates ScopeData instance - No dynamic subclass selection occurs """ - scope = ScopeData(external_key="sc:generic_scope") + scope = ScopeData(external_key="global:generic_scope") - expected_namespaced = f"{ScopeData.NAMESPACE}{ScopeData.SEPARATOR}sc:generic_scope" + expected_namespaced = f"{ScopeData.NAMESPACE}{ScopeData.SEPARATOR}global:generic_scope" self.assertIsInstance(scope, ScopeData) - self.assertEqual(scope.external_key, "sc:generic_scope") + self.assertEqual(scope.external_key, "global:generic_scope") self.assertEqual(scope.namespaced_key, expected_namespaced) def test_empty_namespaced_key_raises_value_error(self): @@ -353,7 +353,7 @@ def test_scope_data_with_wildcard_external_key(self): Expected Result: - ScopeData(external_key='*') creates base ScopeData instance - - namespaced_key is 'sc^*' + - namespaced_key is 'global^*' - No subclass determination occurs """ scope = ScopeData(external_key="*") diff --git a/openedx_authz/tests/api/test_roles.py b/openedx_authz/tests/api/test_roles.py index 2c4193d6..d19497eb 100644 --- a/openedx_authz/tests/api/test_roles.py +++ b/openedx_authz/tests/api/test_roles.py @@ -500,9 +500,9 @@ def test_get_all_role_assignments_scopes(self, subject_name, expected_roles): (roles.LIBRARY_AUTHOR.external_key, "lib:Org6:project_beta", 1), (roles.LIBRARY_CONTRIBUTOR.external_key, "lib:Org6:project_gamma", 1), (roles.LIBRARY_USER.external_key, "lib:Org6:project_delta", 1), - ("non_existent_role", "sc:any_library", 0), - (roles.LIBRARY_ADMIN.external_key, "sc:non_existent_scope", 0), - ("non_existent_role", "sc:non_existent_scope", 0), + ("non_existent_role", "global:any_library", 0), + (roles.LIBRARY_ADMIN.external_key, "global:non_existent_scope", 0), + ("non_existent_role", "global:non_existent_scope", 0), ) @unpack def test_get_role_assignments_in_scope(self, role_name, scope_name, expected_count): @@ -625,8 +625,8 @@ def test_get_scopes_for_subject_and_permission(self, subject_name, action_name, (roles.LIBRARY_AUTHOR.external_key, "lib:Org4:art_201", {"liam"}), (roles.LIBRARY_AUTHOR.external_key, "lib:Org4:art_301", {"liam"}), ("non_existent_role", "lib:Org4:art_101", set()), - (roles.LIBRARY_AUTHOR.external_key, "sc:non_existent_scope", set()), - ("non_existent_role", "sc:non_existent_scope", set()), + (roles.LIBRARY_AUTHOR.external_key, "global:non_existent_scope", set()), + ("non_existent_role", "global:non_existent_scope", set()), ) @unpack def test_get_subjects_for_role_in_scope(self, role_name: str, scope_name: str, expected_subjects: set[str]): @@ -654,7 +654,7 @@ class TestRoleAssignmentAPI(RolesTestSetupMixin): """ @ddt_data( - (["mary", "john"], roles.LIBRARY_USER.external_key, "sc:batch_test", True), + (["mary", "john"], roles.LIBRARY_USER.external_key, "global:batch_test", True), ( ["paul", "diana", "lila"], roles.LIBRARY_CONTRIBUTOR.external_key, @@ -712,7 +712,7 @@ def test_batch_assign_role_to_subjects_in_scope(self, subject_names, role, scope self.assertIn(role, role_names) @ddt_data( - (["mary", "john"], roles.LIBRARY_USER.external_key, "sc:batch_test", True), + (["mary", "john"], roles.LIBRARY_USER.external_key, "global:batch_test", True), ( ["paul", "diana", "lila"], roles.LIBRARY_CONTRIBUTOR.external_key, @@ -827,7 +827,7 @@ def test_unassign_role_from_subject_in_scope(self, subject_names, role, scope_na ) ], ), - ("sc:non_existent_scope", []), + ("global:non_existent_scope", []), ) @unpack def test_get_all_role_assignments_in_scope(self, scope_name, expected_assignments): From cf2fa620ee528da2af1a648b7907ce747f2318fb Mon Sep 17 00:00:00 2001 From: Maria Grimaldi Date: Wed, 5 Nov 2025 13:00:43 +0100 Subject: [PATCH 7/8] refactor: change generic to global wildecard constant --- openedx_authz/api/data.py | 4 ++-- openedx_authz/rest_api/utils.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/openedx_authz/api/data.py b/openedx_authz/api/data.py index 4b9b00c7..01a67826 100644 --- a/openedx_authz/api/data.py +++ b/openedx_authz/api/data.py @@ -31,7 +31,7 @@ AUTHZ_POLICY_ATTRIBUTES_SEPARATOR = "^" EXTERNAL_KEY_SEPARATOR = ":" -GENERIC_SCOPE_WILDCARD = "*" +GLOBAL_SCOPE_WILDCARD = "*" NAMESPACED_KEY_PATTERN = rf"^.+{re.escape(AUTHZ_POLICY_ATTRIBUTES_SEPARATOR)}.+$" @@ -186,7 +186,7 @@ def __call__(cls, *args, **kwargs): # an external_key to mean generic scope which maps to base ScopeData class. # The only remaining issue is that internally the namespace key used in policies will be # The global scope namespace (global^*), so we need to handle that case here. - if kwargs.get("external_key") == GENERIC_SCOPE_WILDCARD: + if kwargs.get("external_key") == GLOBAL_SCOPE_WILDCARD: return super().__call__(*args, **kwargs) if "namespaced_key" in kwargs: diff --git a/openedx_authz/rest_api/utils.py b/openedx_authz/rest_api/utils.py index 4af6aaa1..cbeaaa82 100644 --- a/openedx_authz/rest_api/utils.py +++ b/openedx_authz/rest_api/utils.py @@ -3,7 +3,7 @@ from django.contrib.auth import get_user_model from django.db.models import Q -from openedx_authz.api.data import GENERIC_SCOPE_WILDCARD, ScopeData +from openedx_authz.api.data import GLOBAL_SCOPE_WILDCARD, ScopeData from openedx_authz.rest_api.data import SearchField, SortField, SortOrder User = get_user_model() @@ -28,7 +28,7 @@ def get_generic_scope(scope: ScopeData) -> ScopeData: >>> get_generic_scope(scope) ScopeData(namespaced_key="lib^*") """ - return ScopeData(namespaced_key=f"{scope.NAMESPACE}{ScopeData.SEPARATOR}{GENERIC_SCOPE_WILDCARD}") + return ScopeData(namespaced_key=f"{scope.NAMESPACE}{ScopeData.SEPARATOR}{GLOBAL_SCOPE_WILDCARD}") def get_user_map(usernames: list[str]) -> dict[str, User]: From e802390b95e64386714a19acbf65d3831b1f297d Mon Sep 17 00:00:00 2001 From: Maria Grimaldi Date: Wed, 5 Nov 2025 13:08:25 +0100 Subject: [PATCH 8/8] docs: update docs for next 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 6900783f..e064d4f8 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -14,6 +14,14 @@ Change Log Unreleased ********** +0.13.0 - 2025-11-05 +******************** + +Added +===== + +* Add support for global scopes instead of generic `sc` scope to support instance-level permissions. + 0.12.0 - 2025-10-30 ******************** diff --git a/openedx_authz/__init__.py b/openedx_authz/__init__.py index 26ecc77c..c39af842 100644 --- a/openedx_authz/__init__.py +++ b/openedx_authz/__init__.py @@ -4,6 +4,6 @@ import os -__version__ = "0.12.0" +__version__ = "0.13.0" ROOT_DIRECTORY = os.path.dirname(os.path.abspath(__file__))