@@ -100,18 +100,24 @@ class ScopeMeta(type):
100100 """Metaclass for ScopeData to handle dynamic subclass instantiation based on namespace."""
101101
102102 scope_registry : ClassVar [dict [str , Type ["ScopeData" ]]] = {}
103- glob_registry : ClassVar [dict [str , Type ["ScopeData" ]]] = {}
103+ org_glob_registry : ClassVar [dict [str , Type ["ScopeData" ]]] = {}
104+ platform_glob_registry : ClassVar [dict [str , Type ["ScopeData" ]]] = {}
104105
105106 def __init__ (cls , name , bases , attrs ):
106- """Initialize the metaclass and register subclasses."""
107107 super ().__init__ (name , bases , attrs )
108108 if not hasattr (cls , "scope_registry" ):
109109 cls .scope_registry = {}
110- if not hasattr (cls , "glob_registry" ):
111- cls .glob_registry = {}
110+ if not hasattr (cls , "org_glob_registry" ):
111+ cls .org_glob_registry = {}
112+ if not hasattr (cls , "platform_glob_registry" ):
113+ cls .platform_glob_registry = {}
114+
115+ cls .IS_GLOB = cls .IS_ORG_GLOB or cls .IS_PLATFORM_GLOB
112116
113- if cls .IS_GLOB and cls .NAMESPACE :
114- cls .glob_registry [cls .NAMESPACE ] = cls
117+ if cls .IS_PLATFORM_GLOB and cls .NAMESPACE :
118+ cls .platform_glob_registry [cls .NAMESPACE ] = cls
119+ elif cls .IS_ORG_GLOB and cls .NAMESPACE :
120+ cls .org_glob_registry [cls .NAMESPACE ] = cls
115121 else :
116122 cls .scope_registry [cls .NAMESPACE ] = cls
117123
@@ -170,6 +176,11 @@ def __call__(cls, *args, **kwargs):
170176
171177 return super ().__call__ (* args , ** kwargs )
172178
179+ @staticmethod
180+ def _is_platform_glob (external_key : str , namespace : str ) -> bool :
181+ """Validate if the external key is a platform glob."""
182+ return external_key == f"{ namespace } { EXTERNAL_KEY_SEPARATOR } { GLOBAL_SCOPE_WILDCARD } "
183+
173184 @classmethod
174185 def get_subclass_by_namespaced_key (mcs , namespaced_key : str ) -> Type ["ScopeData" ]:
175186 """Get the appropriate ScopeData subclass from the namespaced key.
@@ -205,10 +216,13 @@ def get_subclass_by_namespaced_key(mcs, namespaced_key: str) -> Type["ScopeData"
205216 is_glob = GLOBAL_SCOPE_WILDCARD in external_key
206217
207218 if is_glob :
208- # Try to get glob-specific class first
209- return mcs .glob_registry .get (namespace , ScopeData )
219+ # Check if this is a platform glob pattern first
220+ if mcs ._is_platform_glob (external_key , namespace ):
221+ return mcs .platform_glob_registry .get (namespace , ScopeData )
222+ # If not a platform glob, check if it's an org glob
223+ return mcs .org_glob_registry .get (namespace , ScopeData )
210224
211- # Fall back to standard scope class
225+ # If not a glob, return the standard scope class
212226 return mcs .scope_registry .get (namespace , ScopeData )
213227
214228 @classmethod
@@ -258,11 +272,20 @@ def get_subclass_by_external_key(mcs, external_key: str) -> Type["ScopeData"]:
258272 is_glob = GLOBAL_SCOPE_WILDCARD in external_key
259273
260274 if is_glob :
261- glob_subclass = mcs .glob_registry .get (namespace )
275+ if mcs ._is_platform_glob (external_key , namespace ):
276+ platform_subclass = mcs .platform_glob_registry .get (namespace )
277+
278+ if not platform_subclass :
279+ raise ValueError (f"Unknown platform glob scope: { namespace } for external_key: { external_key } " )
280+ if not platform_subclass .validate_external_key (external_key ):
281+ raise ValueError (f"Invalid external_key format for platform glob scope: { external_key } " )
282+
283+ return platform_subclass
284+
285+ glob_subclass = mcs .org_glob_registry .get (namespace )
262286
263287 if not glob_subclass :
264288 raise ValueError (f"Unknown glob scope: { namespace } for external_key: { external_key } " )
265-
266289 if not glob_subclass .validate_external_key (external_key ):
267290 raise ValueError (f"Invalid external_key format for glob scope: { external_key } " )
268291
@@ -272,7 +295,6 @@ def get_subclass_by_external_key(mcs, external_key: str) -> Type["ScopeData"]:
272295
273296 if not scope_subclass :
274297 raise ValueError (f"Unknown scope: { namespace } for external_key: { external_key } " )
275-
276298 if not scope_subclass .validate_external_key (external_key ):
277299 raise ValueError (f"Invalid external_key format: { external_key } " )
278300
@@ -283,15 +305,66 @@ def get_all_namespaces(mcs) -> dict[str, Type["ScopeData"]]:
283305 """Get all registered scope namespaces.
284306
285307 Returns:
286- dict[str, Type["ScopeData"]]: A dictionary of all namespace prefixes registered in the scope registry.
287- Each namespace corresponds to a ScopeData subclass (e.g., 'lib', 'global').
308+ dict[str, Type["ScopeData"]]: A dictionary of all namespace prefixes
309+ registered in the scope registry. Each namespace corresponds to
310+ a ScopeData subclass (e.g., 'lib', 'global').
288311
289312 Examples:
290313 >>> ScopeMeta.get_all_namespaces()
291314 {'global': ScopeData, 'lib': ContentLibraryData, 'org': OrganizationData}
292315 """
293316 return mcs .scope_registry
294317
318+ @classmethod
319+ def get_all_org_glob_namespaces (mcs ) -> dict [str , Type ["ScopeData" ]]:
320+ """Get all registered organization-level glob scope namespaces.
321+
322+ Returns:
323+ dict[str, Type["ScopeData"]]: A dictionary of all namespace prefixes
324+ registered in the organization glob registry. Each namespace corresponds
325+ to an org-level glob ScopeData subclass (e.g., 'course-v1', 'lib').
326+
327+ Examples:
328+ >>> ScopeMeta.get_all_org_glob_namespaces()
329+ {'course-v1': OrgCourseOverviewGlobData, 'lib': OrgContentLibraryGlobData}
330+ """
331+ return mcs .org_glob_registry
332+
333+ @classmethod
334+ def get_all_platform_glob_namespaces (mcs ) -> dict [str , Type ["ScopeData" ]]:
335+ """Get all registered platform-level glob scope namespaces.
336+
337+ Returns:
338+ dict[str, Type["ScopeData"]]: A dictionary of all namespace prefixes
339+ registered in the platform glob registry. Each namespace corresponds
340+ to a platform-level glob ScopeData subclass (e.g., 'course-v1', 'lib').
341+
342+ Examples:
343+ >>> ScopeMeta.get_all_platform_glob_namespaces()
344+ {'course-v1': PlatformCourseOverviewGlobData, 'lib': PlatformContentLibraryGlobData}
345+ """
346+ return mcs .platform_glob_registry
347+
348+ @classmethod
349+ def get_all_registered_scopes (mcs ) -> list [Type ["ScopeData" ]]:
350+ """Get all registered scope subclasses across all registries.
351+
352+ Returns:
353+ list[Type["ScopeData"]]: A unique list of all ScopeData subclasses registered in the standard,
354+ organization glob, and platform glob registries.
355+
356+ Examples:
357+ >>> ScopeMeta.get_all_registered_scopes()
358+ [ScopeData, ContentLibraryData, OrgCourseOverviewGlobData, PlatformCourseOverviewGlobData]
359+ """
360+ return list (
361+ {
362+ * mcs .scope_registry .values (),
363+ * mcs .org_glob_registry .values (),
364+ * mcs .platform_glob_registry .values (),
365+ }
366+ )
367+
295368 @classmethod
296369 def validate_external_key (mcs , external_key : str ) -> bool :
297370 """Validate the external_key format for the subclass.
@@ -330,7 +403,8 @@ class ScopeData(AuthZData, metaclass=ScopeMeta):
330403 # 2. Custom global scopes that don't map to specific domain objects (e.g., 'global:some_scope')
331404 # Subclasses like ContentLibraryData ('lib') represent concrete resource types with their own namespaces.
332405 NAMESPACE : ClassVar [str ] = "global"
333- IS_GLOB : ClassVar [bool ] = False
406+ IS_ORG_GLOB : ClassVar [bool ] = False
407+ IS_PLATFORM_GLOB : ClassVar [bool ] = False
334408
335409 @classmethod
336410 def validate_external_key (cls , _ : str ) -> bool :
@@ -680,7 +754,8 @@ class OrgGlobData(ScopeData):
680754 namespace-specific (subclasses must define it to match their key format).
681755
682756 Attributes:
683- IS_GLOB (bool): Always True for organization-level glob patterns.
757+ NAMESPACE (str): The namespace prefix for organization-level glob patterns (e.g., ``org``).
758+ IS_ORG_GLOB (bool): Always True for organization-level glob patterns.
684759 ID_SEPARATOR (str): Separator used right before the wildcard (e.g., ``:`` or ``+``).
685760 ORG_NAME_VALID_PATTERN (re.Pattern | str): Regex used to validate the organization
686761 identifier extracted from :attr:`external_key`.
@@ -690,7 +765,8 @@ class OrgGlobData(ScopeData):
690765 - ``course-v1:DemoX+*`` (all courses in org ``DemoX``)
691766 """
692767
693- IS_GLOB : ClassVar [bool ] = True
768+ NAMESPACE : ClassVar [str ] = "org"
769+ IS_ORG_GLOB : ClassVar [bool ] = True
694770 ID_SEPARATOR : ClassVar [str ]
695771 ORG_NAME_VALID_PATTERN : ClassVar [re .Pattern ] = r"^[a-zA-Z0-9._-]*$"
696772
@@ -824,7 +900,7 @@ class OrgContentLibraryGlobData(OrgGlobData):
824900 Attributes:
825901 NAMESPACE (str): 'lib' for content library scopes.
826902 ID_SEPARATOR (str): ':' for content library scopes.
827- IS_GLOB (bool): True for scope data that represents a glob pattern.
903+ IS_ORG_GLOB (bool): True for scope data that represents an organization-level glob pattern.
828904 external_key (str): The glob pattern (e.g., ``lib:DemoX:*``).
829905 namespaced_key (str): The pattern with namespace (e.g., ``lib^lib:DemoX:*``).
830906
@@ -882,7 +958,7 @@ class OrgCourseOverviewGlobData(OrgGlobData):
882958 Attributes:
883959 NAMESPACE (str): 'course-v1' for course scopes.
884960 ID_SEPARATOR (str): '+' for course scopes.
885- IS_GLOB (bool): True for scope data that represents a glob pattern.
961+ IS_ORG_GLOB (bool): True for scope data that represents an organization-level glob pattern.
886962 external_key (str): The glob pattern (e.g., 'course-v1:OpenedX+*').
887963 namespaced_key (str): The pattern with namespace (e.g., 'course-v1^course-v1:OpenedX+*').
888964
@@ -927,6 +1003,152 @@ def get_admin_manage_permission(cls) -> PermissionData:
9271003 return COURSES_MANAGE_COURSE_TEAM
9281004
9291005
1006+ @define
1007+ class PlatformGlobData (ScopeData ):
1008+ """Base class for platform-level glob scope keys.
1009+
1010+ This represents a platform-wide pattern: it matches "all resources in the platform"
1011+ for a given namespace, rather than being limited to a specific organization or concrete
1012+ object. The pattern is stored in :attr:`external_key` and **must** consist only of
1013+ the namespace followed by the global wildcard (``*``).
1014+
1015+ The expected shape is::
1016+
1017+ {NAMESPACE}{EXTERNAL_KEY_SEPARATOR}*
1018+
1019+ where ``{NAMESPACE}`` is the resource type namespace (e.g., ``course-v1`` or ``lib``).
1020+
1021+ Attributes:
1022+ IS_PLATFORM_GLOB (bool): Always True for platform-level glob patterns.
1023+ NAMESPACE (str): Must be defined by subclasses (e.g., 'course-v1', 'lib').
1024+
1025+ Examples:
1026+ - ``course-v1:*`` (all courses in the platform)
1027+ - ``lib:*`` (all libraries in the platform)
1028+
1029+ Note:
1030+ Subclasses must override NAMESPACE and implement the required abstract methods.
1031+ """
1032+
1033+ NAMESPACE : ClassVar [str ] = "platform"
1034+ IS_PLATFORM_GLOB : ClassVar [bool ] = True
1035+
1036+ @classmethod
1037+ def validate_external_key (cls , external_key : str ) -> bool :
1038+ """Validate the external_key format for platform-level glob patterns.
1039+
1040+ Args:
1041+ external_key (str): The external key to validate (e.g., ``course-v1:*`` or ``lib:*``).
1042+
1043+ Returns:
1044+ bool: True if the format is valid, False otherwise.
1045+ """
1046+ return cls .build_external_key () == external_key
1047+
1048+ @classmethod
1049+ @abstractmethod
1050+ def get_admin_view_permission (cls ) -> PermissionData :
1051+ """Get the permission required to view this scope.
1052+
1053+ Returns:
1054+ PermissionData: The permission required to view this scope in the admin console.
1055+ """
1056+ raise NotImplementedError ("Subclasses must implement get_admin_view_permission method." )
1057+
1058+ @classmethod
1059+ @abstractmethod
1060+ def get_admin_manage_permission (cls ) -> PermissionData :
1061+ """Get the permission required to manage this scope.
1062+
1063+ Returns:
1064+ PermissionData: The permission required to manage this scope in the admin console.
1065+ """
1066+ raise NotImplementedError ("Subclasses must implement get_admin_manage_permission method." )
1067+
1068+ @classmethod
1069+ def build_external_key (cls ) -> str :
1070+ """Build the external key for all resources in the platform.
1071+
1072+ Returns:
1073+ str: The external key for the platform-level glob (e.g., ``course-v1:*``).
1074+
1075+ Examples:
1076+ >>> PlatformCourseOverviewGlobData.build_external_key()
1077+ 'course-v1:*'
1078+ """
1079+ return f"{ cls .NAMESPACE } { EXTERNAL_KEY_SEPARATOR } { GLOBAL_SCOPE_WILDCARD } "
1080+
1081+ def get_object (self ) -> None :
1082+ """Platform-level glob scopes do not map to a concrete domain object.
1083+
1084+ Returns:
1085+ None: Always returns None.
1086+ """
1087+ return None
1088+
1089+ def exists (self ) -> bool :
1090+ """Platform-level glob scopes always exist.
1091+
1092+ Returns:
1093+ bool: Always True.
1094+ """
1095+ return True
1096+
1097+
1098+ @define
1099+ class PlatformCourseOverviewGlobData (PlatformGlobData ):
1100+ """Platform-level glob pattern for courses.
1101+
1102+ This class represents glob patterns that match all courses in the platform,
1103+ Format: ``course-v1:*``
1104+
1105+ The glob pattern allows granting permissions to all courses across the entire
1106+ platform without needing to specify organizations or individual courses.
1107+
1108+ Attributes:
1109+ NAMESPACE (str): 'course-v1' for course scopes.
1110+ IS_PLATFORM_GLOB (bool): True for scope data that represents a platform-level glob pattern.
1111+ external_key (str): The glob pattern (always ``course-v1:*``).
1112+ namespaced_key (str): The pattern with namespace (``course-v1^course-v1:*``).
1113+
1114+ Validation Rules:
1115+ - Must be exactly ``course-v1:*``
1116+ - Applies to all existing and future courses in the platform
1117+ - Does not grant access to other resource types
1118+
1119+ Examples:
1120+ >>> glob = PlatformCourseOverviewGlobData(external_key='course-v1:*')
1121+ >>> glob.exists()
1122+ True
1123+ >>> glob.namespaced_key
1124+ 'course-v1^course-v1:*'
1125+
1126+ Note:
1127+ This class is automatically instantiated by the ScopeMeta metaclass when
1128+ a course scope with the platform wildcard is created.
1129+ """
1130+
1131+ NAMESPACE : ClassVar [str ] = "course-v1"
1132+
1133+ @classmethod
1134+ def get_admin_view_permission (cls ) -> PermissionData :
1135+ """Get the permission required to view this scope.
1136+
1137+ Returns:
1138+ PermissionData: The permission required to view this scope in the admin console.
1139+ """
1140+ return COURSES_VIEW_COURSE_TEAM
1141+
1142+ @classmethod
1143+ def get_admin_manage_permission (cls ) -> PermissionData :
1144+ """Get the permission required to manage this scope.
1145+
1146+ Returns:
1147+ PermissionData: The permission required to manage this scope in the admin console.
1148+ """
1149+ return COURSES_MANAGE_COURSE_TEAM
1150+
1151+
9301152class CCXCourseOverviewData (CourseOverviewData ):
9311153 """CCX course scope for authorization in the Open edX platform.
9321154
0 commit comments