Skip to content

Commit 6c7d069

Browse files
refactor: completely abstract consumers of internal naming conventions
1 parent 637cbdd commit 6c7d069

12 files changed

Lines changed: 1010 additions & 684 deletions

File tree

openedx_authz/api/data.py

Lines changed: 112 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -26,95 +26,163 @@ class PolicyIndex(Enum):
2626

2727

2828
@define
29-
class ScopeData:
29+
class AuthZData:
30+
"""Base class for all authz data classes.
31+
32+
Attributes:
33+
NAMESPACE: The namespace prefix for the data type (e.g., 'user', 'role').
34+
SEPARATOR: The separator between the namespace and the identifier (e.g., ':', '@').
35+
"""
36+
37+
SEPARATOR: str = "@"
38+
NAMESPACE: str = None # To be defined in subclasses
39+
40+
41+
@define
42+
class ScopeData(AuthZData):
3043
"""A scope is a context in which roles and permissions are assigned.
3144
3245
Attributes:
33-
scope_id: The scope identifier (e.g., 'org:Demo').
46+
scope_id: The scope identifier (e.g., 'org@Demo').
3447
3548
This class assumes that the scope is already namespaced appropriately
3649
before being passed in, as scopes can vary widely (e.g., courses, organizations).
3750
"""
38-
scope_id: str
51+
52+
NAMESPACE: str = "sc" # Generic scope namespace, should be overridden by specific scope types
53+
scope_id: str = ""
54+
name: str = "" # Optional human-readable name
55+
56+
def __attrs_post_init__(self):
57+
"""Ensure scope ID has appropriate namespace prefix."""
58+
if not self.scope_id:
59+
self.scope_id = f"{self.NAMESPACE}{self.SEPARATOR}{self.name}".lower()
60+
61+
# Allow reverse lookup of name from scope_id
62+
if not self.name and self.scope_id and self.NAMESPACE and self.scope_id.startswith(f"{self.NAMESPACE}{self.SEPARATOR}"):
63+
self.name = self.scope_id.split(self.SEPARATOR, 1)[1].lower()
64+
3965

4066
@define
4167
class ContentLibraryData(ScopeData):
4268
"""A content library is a collection of content items.
4369
4470
Attributes:
4571
library_id: The content library identifier (e.g., 'library-v1:edX+DemoX+2021_T1').
72+
scope_id: Inherited from ScopeData, auto-generated from library_id if not provided.
4673
"""
4774

48-
library_id: str
75+
NAMESPACE: str = "lib"
76+
library_id: str = ""
4977

5078
def __attrs_post_init__(self):
51-
"""Ensure scope ID has 'lib:' namespace prefix."""
52-
if not self.scope_id.startswith("lib:"):
53-
self.scope_id = f"lib:{self.library_id}"
79+
"""Ensure scope ID has 'lib@' namespace prefix."""
80+
if not self.scope_id:
81+
self.scope_id = f"{self.NAMESPACE}{self.SEPARATOR}{self.library_id}".lower()
82+
83+
# Allow reverse lookup of library_id from scope_id
84+
if not self.library_id and self.scope_id.startswith(f"{self.NAMESPACE}{self.SEPARATOR}"):
85+
self.library_id = self.scope_id.split(self.SEPARATOR, 1)[1].lower()
86+
5487

5588
@define
56-
class SubjectData:
89+
class SubjectData(AuthZData):
5790
"""A subject is an entity that can be assigned roles and permissions.
5891
5992
Attributes:
60-
subject_id: The subject identifier namespaced (e.g., 'user:john_doe').
93+
subject_id: The subject identifier namespaced (e.g., 'user@john_doe').
6194
6295
This class assumes that the subject was already namespaced by their own
63-
type (e.g., 'user:', 'group:') before being passed in since subjects can be
96+
type (e.g., 'user@', 'group@') before being passed in since subjects can be
6497
users, groups, or other entities.
6598
"""
6699

100+
NAMESPACE: str = (
101+
"sub" # Generic subject namespace, should be overridden by specific subject types
102+
)
67103
subject_id: str = ""
104+
name: str = "" # Optional human-readable name
105+
106+
def __attrs_post_init__(self):
107+
"""Ensure subject ID has appropriate namespace prefix.
108+
109+
This allows initialization with either name= or subject_id= parameter.
110+
"""
111+
if not self.subject_id:
112+
self.subject_id = f"{self.NAMESPACE}{self.SEPARATOR}{self.name}".lower()
113+
114+
# Allow reverse lookup of name from subject_id
115+
if not self.name and self.subject_id.startswith(f"{self.NAMESPACE}{self.SEPARATOR}"):
116+
self.name = self.subject_id.split(self.SEPARATOR, 1)[1].lower()
117+
68118

69119
@define
70120
class UserData(SubjectData):
71121
"""A user is a subject that can be assigned roles and permissions.
72122
73123
Attributes:
74124
username: The username for the user (e.g., 'john_doe').
125+
subject_id: Inherited from SubjectData, auto-generated from username if not provided.
75126
76-
This class automatically adds the 'user:' namespace prefix to the subject ID.
127+
This class automatically adds the 'user@' namespace prefix to the subject ID.
77128
Can be initialized with either username= or subject_id= parameter.
78129
"""
79130

131+
NAMESPACE: str = "user"
80132
username: str = ""
81133

82134
def __attrs_post_init__(self):
83-
"""Ensure subject ID has 'user:' namespace prefix."""
84-
# If username was provided, use it to set subject_id
85-
if not self.subject_id.startswith("user:"):
86-
self.subject_id = f"user:{self.username}"
135+
"""Ensure subject ID has 'user@' namespace prefix.
136+
137+
This allows initialization with either username or subject_id.
138+
"""
139+
if not self.subject_id:
140+
self.subject_id = f"{self.NAMESPACE}{self.SEPARATOR}{self.username}".lower()
141+
142+
# Allow reverse lookup of username from subject_id
143+
if not self.username and self.subject_id.startswith(f"{self.NAMESPACE}{self.SEPARATOR}"):
144+
self.username = self.subject_id.split(self.SEPARATOR, 1)[1].lower()
145+
87146

88147
@define
89-
class ActionData:
148+
class ActionData(AuthZData):
90149
"""An action is an operation that can be performed in a specific scope.
91150
92151
Attributes:
93-
action: The action name. Automatically prefixed with 'act:' if not present.
152+
action: The action name. Automatically prefixed with 'act@' if not present.
94153
"""
95154

96-
action_id: str
155+
NAMESPACE: str = "act"
156+
name: str = ""
157+
action_id: str = ""
97158

98159
def __attrs_post_init__(self):
99-
"""Ensure action name has 'act:' namespace prefix."""
100-
if not self.action_id.startswith("act:"):
101-
self.action_id = f"act:{self.action_id}"
160+
"""Ensure action name has 'act@' namespace prefix.
161+
162+
This allows initialization with either name= or action_id= parameter.
163+
"""
164+
if not self.action_id:
165+
self.action_id = f"{self.NAMESPACE}{self.SEPARATOR}{self.name}".lower()
166+
167+
# Allow reverse lookup of name from action_id
168+
if not self.name and self.action_id.startswith(f"{self.NAMESPACE}{self.SEPARATOR}"):
169+
self.name = self.action_id.split(self.SEPARATOR, 1)[1].lower()
102170

103171

104172
@define
105-
class PermissionData: # TODO: change to policy?
173+
class PermissionData(AuthZData):
106174
"""A permission is an action that can be performed under certain conditions.
107175
108176
Attributes:
109177
name: The name of the permission.
110178
"""
111179

112-
action: ActionData
180+
action: ActionData = None
113181
effect: Literal["allow", "deny"] = "allow"
114182

115183

116184
@define
117-
class RoleMetadataData:
185+
class RoleMetadataData(AuthZData):
118186
"""Metadata for a role.
119187
120188
Attributes:
@@ -129,38 +197,46 @@ class RoleMetadataData:
129197

130198

131199
@define
132-
class RoleData:
200+
class RoleData(AuthZData):
133201
"""A role is a named group of permissions.
134202
135203
Attributes:
136-
name: The name of the role. Must have 'role:' namespace prefix.
204+
name: The name of the role. Must have 'role@' namespace prefix.
205+
role_id: The role identifier namespaced (e.g., 'role@instructor').
137206
permissions: A list of permissions assigned to the role.
138-
scopes: A list of scopes assigned to the role.
139207
metadata: A dictionary of metadata assigned to the role. This can include
140208
information such as the description of the role, creation date, etc.
141209
"""
142210

143-
name: str
211+
NAMESPACE: str = "role"
212+
name: str = ""
213+
role_id: str = ""
144214
permissions: list[PermissionData] = None
145215
metadata: RoleMetadataData = None
146216

147217
def __attrs_post_init__(self):
148-
"""Ensure role name has 'role:' namespace prefix."""
149-
if not self.name.startswith("role:"):
150-
self.name = f"role:{self.name}"
218+
"""Ensure role id has 'role@' namespace prefix.
219+
220+
This allows initialization with either name= or role_id= parameter.
221+
"""
222+
if not self.role_id or not self.role_id.startswith(f"{self.NAMESPACE}{self.SEPARATOR}"):
223+
self.role_id = f"{self.NAMESPACE}{self.SEPARATOR}{self.name}".lower()
151224

225+
# Allow reverse lookup of name from role_id
226+
if not self.name and self.role_id.startswith(f"{self.NAMESPACE}{self.SEPARATOR}"):
227+
self.name = self.role_id.split(self.SEPARATOR, 1)[1].lower()
152228

153229
@define
154-
class RoleAssignmentData:
230+
class RoleAssignmentData(AuthZData):
155231
"""A role assignment is the assignment of a role to a subject in a specific scope.
156232
157233
Attributes:
158-
subject: The ID of the user namespaced (e.g., 'user:john_doe').
234+
subject: The ID of the user namespaced (e.g., 'user@john_doe').
159235
email: The email of the user.
160236
role_name: The name of the role.
161237
scope: The scope in which the role is assigned.
162238
"""
163239

164-
subject: SubjectData
165-
role: RoleData
166-
scope: ScopeData
240+
subject: SubjectData = None
241+
role: RoleData = None
242+
scope: ScopeData = None

openedx_authz/api/permissions.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,13 @@
55
are not explicitly defined, but are inferred from the policy rules.
66
"""
77

8-
from openedx_authz.api.data import ActionData, PermissionData, PolicyIndex, ScopeData, SubjectData
8+
from openedx_authz.api.data import (
9+
ActionData,
10+
PermissionData,
11+
PolicyIndex,
12+
ScopeData,
13+
SubjectData,
14+
)
915
from openedx_authz.engine.enforcer import enforcer
1016

1117
__all__ = [

0 commit comments

Comments
 (0)