|
32 | 32 | "AuthzBaseClass", |
33 | 33 | "ContentLibraryData", |
34 | 34 | "CourseOverviewData", |
| 35 | + "GlobalWildcardScopeData", |
35 | 36 | "GroupingPolicyIndex", |
36 | 37 | "OrgCourseOverviewGlobData", |
37 | 38 | "OrgGlobData", |
@@ -154,11 +155,11 @@ def __call__(cls, *args, **kwargs): |
154 | 155 |
|
155 | 156 | # When working with global scopes, we can't determine subclass with an external_key since |
156 | 157 | # a global scope it's not attached to a specific resource type. So we only use * as |
157 | | - # an external_key to mean generic scope which maps to base ScopeData class. |
| 158 | + # an external_key to mean the global wildcard scope which maps to GlobalWildcardScopeData. |
158 | 159 | # The only remaining issue is that internally the namespace key used in policies will be |
159 | 160 | # The global scope namespace (global^*), so we need to handle that case here. |
160 | 161 | if kwargs.get("external_key") == GLOBAL_SCOPE_WILDCARD: |
161 | | - return super().__call__(*args, **kwargs) |
| 162 | + return super(ScopeMeta, GlobalWildcardScopeData).__call__(*args, **kwargs) |
162 | 163 |
|
163 | 164 | if "namespaced_key" in kwargs: |
164 | 165 | scope_cls = cls.get_subclass_by_namespaced_key(kwargs["namespaced_key"]) |
@@ -249,6 +250,11 @@ def get_subclass_by_external_key(mcs, external_key: str) -> Type["ScopeData"]: |
249 | 250 | - This won't work for org scopes that don't have explicit namespace prefixes. |
250 | 251 | TODO: Handle org scopes differently. |
251 | 252 | """ |
| 253 | + |
| 254 | + # Special case: handle the global wildcard scope: |
| 255 | + if external_key == "*": |
| 256 | + return mcs.glob_registry.get("global") |
| 257 | + |
252 | 258 | if EXTERNAL_KEY_SEPARATOR not in external_key: |
253 | 259 | raise ValueError(f"Invalid external_key format: {external_key}") |
254 | 260 |
|
@@ -288,7 +294,7 @@ def get_all_namespaces(mcs) -> dict[str, Type["ScopeData"]]: |
288 | 294 |
|
289 | 295 | Examples: |
290 | 296 | >>> ScopeMeta.get_all_namespaces() |
291 | | - {'global': ScopeData, 'lib': ContentLibraryData, 'org': OrganizationData} |
| 297 | + {'global': GlobalWildcardScopeData, 'lib': ContentLibraryData, 'org': OrganizationData} |
292 | 298 | """ |
293 | 299 | return mcs.scope_registry |
294 | 300 |
|
@@ -326,8 +332,7 @@ class ScopeData(AuthZData, metaclass=ScopeMeta): |
326 | 332 |
|
327 | 333 | # The 'global' namespace is used for scopes that aren't tied to a specific resource type. |
328 | 334 | # This base class supports: |
329 | | - # 1. Global wildcard scopes (external_key='*') that apply across all resource types |
330 | | - # 2. Custom global scopes that don't map to specific domain objects (e.g., 'global:some_scope') |
| 335 | + # - Custom scopes that don't map to specific domain objects (e.g., 'global:some_scope') |
331 | 336 | # Subclasses like ContentLibraryData ('lib') represent concrete resource types with their own namespaces. |
332 | 337 | NAMESPACE: ClassVar[str] = "global" |
333 | 338 | IS_GLOB: ClassVar[bool] = False |
@@ -400,6 +405,93 @@ def exists(self) -> bool: |
400 | 405 | raise NotImplementedError("Subclasses must implement exists method.") |
401 | 406 |
|
402 | 407 |
|
| 408 | +@define |
| 409 | +class GlobalWildcardScopeData(ScopeData): |
| 410 | + """The global wildcard scope representing access across all scopes. |
| 411 | +
|
| 412 | + This scope is used when a role assignment should apply globally (i.e., not |
| 413 | + tied to any specific resource). It corresponds to the ``global^*`` namespaced |
| 414 | + key in Casbin policies. |
| 415 | +
|
| 416 | + The global wildcard scope always exists and does not map to a concrete domain |
| 417 | + object. It is automatically instantiated by the ``ScopeMeta`` metaclass when |
| 418 | + ``ScopeData(external_key='*')`` is called. |
| 419 | +
|
| 420 | + Attributes: |
| 421 | + NAMESPACE (str): 'global'. |
| 422 | + external_key (str): Always ``'*'``. |
| 423 | + namespaced_key (str): Always ``'global^*'``. |
| 424 | +
|
| 425 | + Examples: |
| 426 | + >>> scope = ScopeData(external_key='*') |
| 427 | + >>> isinstance(scope, GlobalWildcardScopeData) |
| 428 | + True |
| 429 | + >>> scope.exists() |
| 430 | + True |
| 431 | + >>> scope.namespaced_key |
| 432 | + 'global^*' |
| 433 | + """ |
| 434 | + |
| 435 | + # The 'global' namespace is used for scopes that aren't tied to a specific resource type. |
| 436 | + # This class supports Global wildcard scopes (external_key='*') that apply across all resource types |
| 437 | + NAMESPACE: ClassVar[str] = "global" |
| 438 | + IS_GLOB: ClassVar[bool] = True |
| 439 | + |
| 440 | + @classmethod |
| 441 | + def validate_external_key(cls, external_key: str) -> bool: |
| 442 | + """Validate the external_key format for GlobalWildcardScopeData. |
| 443 | +
|
| 444 | + Only the wildcard ``'*'`` is accepted. |
| 445 | +
|
| 446 | + Args: |
| 447 | + external_key: The external key to validate. |
| 448 | +
|
| 449 | + Returns: |
| 450 | + bool: True if the key is ``'*'``, False otherwise. |
| 451 | + """ |
| 452 | + return external_key == GLOBAL_SCOPE_WILDCARD |
| 453 | + |
| 454 | + @classmethod |
| 455 | + def get_admin_view_permission(cls) -> PermissionData: |
| 456 | + """No admin view permission for the global scope. |
| 457 | +
|
| 458 | + Global scope assignments are managed exclusively by superadmins |
| 459 | + at the REST API layer, so no granular permission is needed. |
| 460 | +
|
| 461 | + Returns: |
| 462 | + None |
| 463 | + """ |
| 464 | + return VIEW_LIBRARY_TEAM |
| 465 | + |
| 466 | + @classmethod |
| 467 | + def get_admin_manage_permission(cls) -> PermissionData: |
| 468 | + """No admin manage permission for the global scope. |
| 469 | +
|
| 470 | + Global scope assignments are managed exclusively by superadmins |
| 471 | + at the REST API layer, so no granular permission is needed. |
| 472 | +
|
| 473 | + Returns: |
| 474 | + None |
| 475 | + """ |
| 476 | + return None |
| 477 | + |
| 478 | + def get_object(self) -> None: |
| 479 | + """The global wildcard scope does not map to a concrete domain object. |
| 480 | +
|
| 481 | + Returns: |
| 482 | + None: Always returns None. |
| 483 | + """ |
| 484 | + return None |
| 485 | + |
| 486 | + def exists(self) -> bool: |
| 487 | + """The global wildcard scope always exists. |
| 488 | +
|
| 489 | + Returns: |
| 490 | + bool: Always True. |
| 491 | + """ |
| 492 | + return True |
| 493 | + |
| 494 | + |
403 | 495 | @define |
404 | 496 | class ContentLibraryData(ScopeData): |
405 | 497 | """A content library scope for authorization in the Open edX platform. |
@@ -690,6 +782,9 @@ class OrgGlobData(ScopeData): |
690 | 782 | - ``course-v1:DemoX+*`` (all courses in org ``DemoX``) |
691 | 783 | """ |
692 | 784 |
|
| 785 | + # This NAMESPACE should be overriden by specific scope subclasses, |
| 786 | + # Setting this here so it doesn't conflict with GlobalWildcardScopeData's 'global' namespace |
| 787 | + NAMESPACE: ClassVar[str] = "orgglob" |
693 | 788 | IS_GLOB: ClassVar[bool] = True |
694 | 789 | ID_SEPARATOR: ClassVar[str] |
695 | 790 | ORG_NAME_VALID_PATTERN: ClassVar[re.Pattern] = r"^[a-zA-Z0-9._-]*$" |
|
0 commit comments