@@ -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
4167class 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
70120class 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
0 commit comments