Skip to content

Commit a154226

Browse files
committed
feat: add ID_SEPARATOR attribute
1 parent 8b584e2 commit a154226

1 file changed

Lines changed: 51 additions & 30 deletions

File tree

openedx_authz/api/data.py

Lines changed: 51 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -425,11 +425,12 @@ class ContentLibraryData(ScopeData):
425425
Content libraries use the LibraryLocatorV2 format for identification.
426426
427427
Attributes:
428-
NAMESPACE: 'lib' for content library scopes.
429-
external_key: The content library identifier (e.g., 'lib:DemoX:CSPROB').
428+
NAMESPACE (str): 'lib' for content library scopes.
429+
ID_SEPARATOR (str): ':' for content library scopes.
430+
external_key (str): The content library identifier (e.g., 'lib:DemoX:CSPROB').
430431
Must be a valid LibraryLocatorV2 format.
431-
namespaced_key: The library identifier with namespace (e.g., 'lib^lib:DemoX:CSPROB').
432-
library_id: Property alias for external_key.
432+
namespaced_key (str): The library identifier with namespace (e.g., 'lib^lib:DemoX:CSPROB').
433+
library_id (str): Property alias for external_key.
433434
434435
Examples:
435436
>>> library = ContentLibraryData(external_key='lib:DemoX:CSPROB')
@@ -443,6 +444,7 @@ class ContentLibraryData(ScopeData):
443444
"""
444445

445446
NAMESPACE: ClassVar[str] = "lib"
447+
ID_SEPARATOR: ClassVar[str] = ":"
446448

447449
@property
448450
def library_id(self) -> str:
@@ -527,25 +529,27 @@ class OrgLibraryGlobData(ContentLibraryData):
527529
"""Organization-level glob pattern for content libraries.
528530
529531
This class represents glob patterns that match multiple libraries within an organization.
530-
Format: 'lib:ORG*' where ORG is a valid organization identifier.
532+
Format: 'lib:ORG:*' where ORG is a valid organization identifier.
531533
532534
The glob pattern allows granting permissions to all libraries within a specific organization
533535
without needing to specify each library individually.
534536
535537
Attributes:
536538
NAMESPACE (str): Inherited 'lib' from ContentLibraryData.
537-
external_key (str): The glob pattern (e.g., 'lib:DemoX*').
538-
namespaced_key (str): The pattern with namespace (e.g., 'lib^lib:DemoX*').
539+
ID_SEPARATOR (str): ':' for content library scopes.
540+
IS_GLOB (bool): True for organization-level glob patterns.
541+
external_key (str): The glob pattern (e.g., 'lib:DemoX:*').
542+
namespaced_key (str): The pattern with namespace (e.g., 'lib^lib:DemoX:*').
539543
540544
Validation Rules:
541545
- Must end with GLOBAL_SCOPE_WILDCARD (*)
542-
- Must have format 'lib:ORG*' (exactly one organization identifier)
546+
- Must have format 'lib:ORG:*' (exactly one organization identifier)
543547
- The organization must exist in at least one ContentLibrary
544548
- Wildcard can only appear at the end after org identifier
545549
- Cannot have wildcards at slug level (lib:ORG:SLUG* is invalid)
546550
547551
Examples:
548-
>>> glob = OrgLibraryGlobData(external_key='lib:DemoX*')
552+
>>> glob = OrgLibraryGlobData(external_key='lib:DemoX:*')
549553
>>> glob.org
550554
'DemoX'
551555
@@ -561,7 +565,7 @@ def org(self) -> str | None:
561565
"""Get the organization identifier from the glob pattern.
562566
563567
Returns:
564-
str: The organization identifier (e.g., 'DemoX' from 'lib:DemoX*'), None otherwise.
568+
str: The organization identifier (e.g., 'DemoX' from 'lib:DemoX:*'), None otherwise.
565569
"""
566570
return self.get_org(self.external_key)
567571

@@ -570,15 +574,17 @@ def validate_external_key(cls, external_key: str) -> bool:
570574
"""Validate the external_key format for organization-level library globs.
571575
572576
Args:
573-
external_key (str): The external key to validate (e.g., 'lib:DemoX*').
577+
external_key (str): The external key to validate (e.g., 'lib:DemoX:*').
574578
575579
Returns:
576580
bool: True if the format is valid, False otherwise.
577581
"""
578582
if not external_key.startswith(cls.NAMESPACE + EXTERNAL_KEY_SEPARATOR):
579583
return False
580584

581-
if not external_key.endswith(GLOBAL_SCOPE_WILDCARD):
585+
# Enforce explicit org-level separator: 'lib:ORG:*'
586+
suffix = cls.ID_SEPARATOR + GLOBAL_SCOPE_WILDCARD
587+
if not external_key.endswith(suffix):
582588
return False
583589

584590
org = cls.get_org(external_key)
@@ -598,9 +604,13 @@ def get_org(cls, external_key: str) -> str | None:
598604
external_key (str): The external key to extract the organization identifier from.
599605
600606
Returns:
601-
str: The organization identifier (e.g., 'DemoX' from 'lib:DemoX*'), None otherwise.
607+
str: The organization identifier (e.g., 'DemoX' from 'lib:DemoX:*'), None otherwise.
602608
"""
603-
scope_prefix = external_key[: -len(GLOBAL_SCOPE_WILDCARD)]
609+
suffix = cls.ID_SEPARATOR + GLOBAL_SCOPE_WILDCARD
610+
if not external_key.endswith(suffix):
611+
return None
612+
613+
scope_prefix = external_key[: -len(suffix)]
604614
parts = scope_prefix.split(EXTERNAL_KEY_SEPARATOR)
605615

606616
if len(parts) != 2 or not parts[1]:
@@ -650,22 +660,25 @@ class CourseOverviewData(ScopeData):
650660
Courses uses the CourseKey format for identification.
651661
652662
Attributes:
653-
NAMESPACE: 'course-v1' for course scopes.
654-
external_key: The course identifier (e.g., 'course-v1:TestOrg+TestCourse+2024_T1').
663+
NAMESPACE (str): 'course-v1' for course scopes.
664+
ID_SEPARATOR (str): '+' for course scopes.
665+
IS_GLOB (bool): False for course scopes.
666+
external_key (str): The course identifier (e.g., 'course-v1:TestOrg+TestCourse+2024_T1').
655667
Must be a valid CourseKey format.
656-
namespaced_key: The course identifier with namespace (e.g., 'course-v1^course-v1:TestOrg+TestCourse+2024_T1').
657-
course_id: Property alias for external_key.
668+
namespaced_key (str): The course identifier with namespace
669+
(e.g., 'course-v1^course-v1:TestOrg+TestCourse+2024_T1').
670+
course_id (str): Property alias for external_key.
658671
659672
Examples:
660673
>>> course = CourseOverviewData(external_key='course-v1:TestOrg+TestCourse+2024_T1')
661674
>>> course.namespaced_key
662675
'course-v1^course-v1:TestOrg+TestCourse+2024_T1'
663676
>>> course.course_id
664677
'course-v1:TestOrg+TestCourse+2024_T1'
665-
666678
"""
667679

668680
NAMESPACE: ClassVar[str] = "course-v1"
681+
ID_SEPARATOR: ClassVar[str] = "+"
669682

670683
@property
671684
def course_id(self) -> str:
@@ -750,25 +763,27 @@ class OrgCourseGlobData(CourseOverviewData):
750763
"""Organization-level glob pattern for courses.
751764
752765
This class represents glob patterns that match multiple courses within an organization.
753-
Format: 'course-v1:ORG*' where ORG is a valid organization identifier.
766+
Format: 'course-v1:ORG+*' where ORG is a valid organization identifier.
754767
755768
The glob pattern allows granting permissions to all courses within a specific organization
756769
without needing to specify each course individually.
757770
758771
Attributes:
759-
NAMESPACE: Inherited 'course-v1' from CourseOverviewData.
760-
external_key: The glob pattern (e.g., 'course-v1:OpenedX*').
761-
namespaced_key: The pattern with namespace (e.g., 'course-v1^course-v1:OpenedX*').
772+
NAMESPACE (str): Inherited 'course-v1' from CourseOverviewData.
773+
ID_SEPARATOR (str): '+' for course scopes.
774+
IS_GLOB (bool): True for organization-level glob patterns.
775+
external_key (str): The glob pattern (e.g., 'course-v1:OpenedX+*').
776+
namespaced_key (str): The pattern with namespace (e.g., 'course-v1^course-v1:OpenedX+*').
762777
763778
Validation Rules:
764779
- Must end with GLOBAL_SCOPE_WILDCARD (*)
765-
- Must have format 'course-v1:ORG*' (exactly one organization identifier)
780+
- Must have format 'course-v1:ORG+*' (exactly one organization identifier)
766781
- The organization must exist in at least one CourseOverview
767782
- Wildcard can only appear at the end after org identifier
768783
- Cannot have wildcards at course or run level (course-v1:ORG+COURSE* is invalid)
769784
770785
Examples:
771-
>>> glob = OrgCourseGlobData(external_key='course-v1:OpenedX*')
786+
>>> glob = OrgCourseGlobData(external_key='course-v1:OpenedX+*')
772787
>>> glob.org
773788
'OpenedX'
774789
@@ -784,7 +799,7 @@ def org(self) -> str | None:
784799
"""Get the organization identifier from the glob pattern.
785800
786801
Returns:
787-
str | None: The organization identifier (e.g., 'OpenedX' from 'course-v1:OpenedX*'), None otherwise.
802+
str | None: The organization identifier (e.g., 'OpenedX' from 'course-v1:OpenedX+*'), None otherwise.
788803
"""
789804
return self.get_org(self.external_key)
790805

@@ -793,15 +808,17 @@ def validate_external_key(cls, external_key: str) -> bool:
793808
"""Validate the external_key format for organization-level course globs.
794809
795810
Args:
796-
external_key (str): The external key to validate (e.g., 'course-v1:OpenedX*').
811+
external_key (str): The external key to validate (e.g., 'course-v1:OpenedX+*').
797812
798813
Returns:
799814
bool: True if the format is valid, False otherwise.
800815
"""
801816
if not external_key.startswith(cls.NAMESPACE + EXTERNAL_KEY_SEPARATOR):
802817
return False
803818

804-
if not external_key.endswith(GLOBAL_SCOPE_WILDCARD):
819+
# Enforce explicit org-level separator: 'course-v1:ORG+*'
820+
glob_suffix = cls.ID_SEPARATOR + GLOBAL_SCOPE_WILDCARD
821+
if not external_key.endswith(glob_suffix):
805822
return False
806823

807824
org = cls.get_org(external_key)
@@ -821,9 +838,13 @@ def get_org(cls, external_key: str) -> str | None:
821838
external_key (str): The external key to extract the organization identifier from.
822839
823840
Returns:
824-
str | None: The organization identifier (e.g., 'OpenedX' from 'course-v1:OpenedX*'), None otherwise.
841+
str | None: The organization identifier (e.g., 'OpenedX' from 'course-v1:OpenedX+*'), None otherwise.
825842
"""
826-
scope_prefix = external_key[: -len(GLOBAL_SCOPE_WILDCARD)]
843+
suffix = cls.ID_SEPARATOR + GLOBAL_SCOPE_WILDCARD
844+
if not external_key.endswith(suffix):
845+
return None
846+
847+
scope_prefix = external_key[: -len(suffix)]
827848
parts = scope_prefix.split(EXTERNAL_KEY_SEPARATOR)
828849

829850
if len(parts) != 2 or not parts[1]:

0 commit comments

Comments
 (0)