Skip to content

Commit 21cd3a5

Browse files
committed
refactor: update authorization model and enforcement command for scope-based permissions
1 parent 65d9af9 commit 21cd3a5

3 files changed

Lines changed: 115 additions & 98 deletions

File tree

openedx_authz/engine/config/model.conf

Lines changed: 58 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,16 @@
55
# - Scoped role assignments (user roles tied to specific contexts)
66
# - Action grouping (manage → read/write/delete to reduce duplication)
77
# - System-wide roles (global scope "*" applies everywhere)
8-
# - Granular permissions (exact resources) and broad permissions (org-level)
8+
# - Scope-based permissions (authorization based on context, not specific objects)
99
# - Negative rules (deny overrides allow for exceptions)
10-
# - Namespace support (course:*, lib:*, org:*, etc.)
11-
# - Extensibility (new resource types just need new namespaces)
10+
# - Namespace support (course-v1:*, lib:*, org:*, etc.)
11+
# - Extensibility (new resource types only require new scope namespaces)
12+
#
13+
# DESIGN PRINCIPLE:
14+
# - Authorization is scope-based, not object-based
15+
# - Each request is explicitly scoped (course, org, global, etc.)
16+
# - Permissions are granted within scopes, eliminating need for object matching
17+
# - Containment relationships are handled by the application layer
1218
#
1319
# NOT handled here (deferred to application):
1420
# - Resource grouping/containment (app resolves parent-child relationships)
@@ -17,59 +23,58 @@
1723
############################################
1824

1925
[request_definition]
20-
# Request format: subject, action, object, scope
26+
# Request format: subject, action, scope
2127
#
2228
# sub = subject/principal with namespace (e.g., "user:alice", "service:lms")
2329
# act = action with namespace (e.g., "act:read", "act:manage", "act:edit-courses")
24-
# obj = object/resource with namespace (e.g., "course:course-v1:OpenedX+Demo+2024", "lib:math-basics", "org:OpenedX")
25-
# scope = authorization scope context (e.g., "org:OpenedX", "lib:math-basics", "*" for global)
30+
# scope = authorization scope context (e.g., "org:OpenedX", "course-v1:...", "*" for global)
2631
#
2732
# SCOPE SEMANTICS:
28-
# - Scope determines which role assignments are considered
29-
# - "*" = global scope (system-wide roles)
30-
# - "org:OpenedX" = organization-scoped roles
31-
# - "course:course-v1:..." = course-scoped roles
33+
# - Scope determines the authorization context and which role assignments apply
34+
# - "*" = global scope (system-wide roles apply everywhere)
35+
# - "org:OpenedX" = organization-scoped roles (apply within OpenedX org)
36+
# - "course-v1:..." = course-scoped roles (apply within specific course)
3237
#
33-
# - Application must provide appropriate scope based on business logic
34-
r = sub, act, obj, scope
38+
# Application must provide appropriate scope based on business logic.
39+
r = sub, act, scope
3540

3641
[policy_definition]
37-
# Policy format: subject, action, object, effect
38-
#
39-
# sub = role or direct user with namespace (e.g., "role:org_admin", "user:bob")
40-
# act = action identifier (e.g., "act:manage", "act:read", "act:edit-courses"). Uses g2 relation for action grouping.
41-
# obj = object selector - supports multiple patterns via keyMatch:
42-
# - Exact ID: "course:course-v1:OpenedX+Demo+2024"
43-
# - Namespace wildcard: "course:*" (matches all courses)
44-
# - Prefix patterns: "course:course-v1:OpenedX+*" (matches all OpenedX courses)
45-
# - Scope targets: "org:OpenedX" (organization itself)
46-
# eft = "allow" or "deny" (deny overrides allow for exceptions)
47-
p = sub, act, obj, eft
42+
# Policy format: subject, action, scope, effect
43+
#
44+
# sub = role or user with namespace (e.g., "role:org_admin", "user:bob")
45+
# act = action identifier (e.g., "act:manage", "act:read", "act:edit-courses"). Uses g2 relation for action grouping.
46+
# scope = scope where policy applies (e.g., "*", "org:OpenedX", "course-v1:...")
47+
# eft = "allow" or "deny" (deny overrides allow for exceptions)
48+
p = sub, act, scope, eft
4849

4950
[role_definition]
50-
# g: Role assignments with scope
51-
# Format: user/subject, role, scope
51+
# g: Role assignments (without scope)
52+
# Format: user/subject, role
53+
#
54+
# This is a simplified role assignment where users are assigned roles globally,
55+
# without being tied to specific scopes. All role assignments apply system-wide.
5256
#
5357
# Examples:
54-
# g, user:alice, role:org_admin, org:OpenedX # Alice is org admin for OpenedX
55-
# g, user:bob, role:course_instructor, course:course-v1:... # Bob is instructor for specific course
56-
# g, user:carol, role:platform_admin, * # Carol is global platform admin
57-
# g, service:lms, role:system_service, * # LMS service has system-wide access
58+
# g, user:alice, role:org_admin # Alice is org admin
59+
# g, user:bob, role:course_instructor # Bob is course instructor
60+
# g, user:carol, role:platform_admin # Carol is platform admin
61+
# g, service:lms, role:system_service # LMS service has system-wide access
5862
#
5963
# Role hierarchy (optional):
60-
# g, role:org_admin, role:org_editor, org:OpenedX # org_admin inherits org_editor permissions
61-
g = _, _, _
64+
# g, role:org_admin, role:org_editor # org_admin inherits org_editor permissions
65+
#
66+
# NOTE: Without scope in role assignments, authorization control must rely entirely
67+
# on policy definitions (p) to restrict access to appropriate scopes/contexts.
68+
g = _, _
6269

6370
# g2: Action grouping and implications
6471
# Maps high-level actions to specific actions to reduce policy duplication
6572
#
6673
# Examples:
67-
# g2, act:manage, act:read # manage implies read
6874
# g2, act:manage, act:edit # manage implies edit
69-
# g2, act:manage, act:write # manage implies write
7075
# g2, act:manage, act:delete # manage implies delete
71-
# g2, act:edit-courses, act:read # edit-courses implies read (for resource grouping)
72-
# g2, act:edit-courses, act:write # edit-courses implies write
76+
# g2, act:edit-courses, act:read # edit-courses implies read (for resource access)
77+
# g2, act:edit-courses, act:write # edit-courses implies write (for resource modification)
7378
g2 = _, _
7479

7580
[policy_effect]
@@ -87,21 +92,26 @@ e = some(where (p.eft == allow)) && !some(where (p.eft == deny))
8792
[matchers]
8893
# Authorization matching logic
8994
#
90-
# SCOPE MATCHING:
91-
# - g(r.sub, p.sub, r.scope): check if subject has role in requested scope
92-
# - g(r.sub, p.sub, "*"): check if subject has global role (applies everywhere)
93-
#
94-
# OBJECT MATCHING:
95-
# - keyMatch(r.obj, p.obj): pattern-based object matching
96-
# Supports wildcards like "course:*" matching "course:course-v1:OpenedX+Demo+2024"
97-
# Also supports exact matches when no wildcards are used
95+
# SUBJECT MATCHING:
96+
# - g(r.sub, p.sub): check if request subject matches policy subject (role assignment)
97+
# This handles user-to-role mappings defined in the role_definition section
9898
#
9999
# ACTION MATCHING:
100-
# - g2(p.act, r.act): policy action implies requested action via action grouping
100+
# - g2(p.act, r.act): policy action implies requested action via grouping
101101
# Allows high-level actions (manage) to grant specific actions (read/write/delete)
102102
#
103+
# SCOPE MATCHING:
104+
# - keyMatch(r.scope, p.scope): check if request scope matches policy scope
105+
# Supports wildcard matching (e.g., "*" matches any scope)
106+
# Enables hierarchical scope matching for nested authorization contexts
107+
#
103108
# All conditions must be true for a policy to match:
104-
# 1. Subject must have the required role in scope OR globally
105-
# 2. Object must match the policy object pattern
106-
# 3. Policy action must imply the requested action
107-
m = (g(r.sub, p.sub, r.scope) || g(r.sub, p.sub, "*")) && keyMatch(r.obj, p.obj) && g2(p.act, r.act)
109+
# 1. Subject must have the required role (via role assignment)
110+
# 2. Policy action must imply the requested action (via action grouping)
111+
# 3. Request scope must match the policy scope (with wildcard support)
112+
#
113+
# SCOPE-BASED AUTHORIZATION:
114+
# The matcher uses keyMatch for flexible scope matching, allowing policies
115+
# to apply to specific scopes (org:OpenedX) or globally (*), providing
116+
# fine-grained control over authorization contexts.
117+
m = g(r.sub, p.sub) && g2(p.act, r.act) && keyMatch(r.scope, p.scope)

openedx_authz/management/commands/enforcement.py

Lines changed: 15 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,15 @@
88
The command supports:
99
- Loading Casbin model from the built-in model.conf file
1010
- Using custom policy files (specified via --policy-file-path argument)
11-
- Interactive testing with format: subject action object scope
11+
- Interactive testing with format: subject action scope
1212
- Real-time enforcement results with visual feedback (✓ ALLOWED / ✗ DENIED)
1313
- Display of loaded policies, role assignments, and action grouping rules
1414
1515
Example usage:
1616
python manage.py lms enforcement --policy-file-path /path/to/authz.policy
1717
1818
Example test input:
19-
user:alice act:read lib:test-lib org:OpenedX
19+
user:alice act:read org:OpenedX
2020
"""
2121

2222
import os
@@ -38,7 +38,7 @@ class Command(BaseCommand):
3838

3939
help = (
4040
"Interactive mode for testing Casbin enforcement policies using model.conf and a custom policy file. "
41-
"Provides real-time authorization testing with format: subject action object scope. "
41+
"Provides real-time authorization testing with format: subject action scope. "
4242
"Use --policy-file-path to specify the policy file location."
4343
)
4444

@@ -114,7 +114,7 @@ def _run_interactive_mode(self, enforcer: casbin.Enforcer) -> None:
114114
"""Start the interactive enforcement testing shell.
115115
116116
Provides a continuous loop where users can input enforcement requests
117-
in the format 'subject action object scope' and receive immediate
117+
in the format 'subject action scope' and receive immediate
118118
authorization results with visual feedback.
119119
120120
Args:
@@ -127,8 +127,8 @@ def _run_interactive_mode(self, enforcer: casbin.Enforcer) -> None:
127127
self.stdout.write("Test custom enforcement requests interactively.")
128128
self.stdout.write("Enter 'quit', 'exit', or 'q' to exit the interactive mode.")
129129
self.stdout.write("")
130-
self.stdout.write("Format: subject action object scope")
131-
self.stdout.write("Example: user:alice act:read lib:test-lib org:OpenedX")
130+
self.stdout.write("Format: subject action scope")
131+
self.stdout.write("Example: user:alice act:read org:OpenedX")
132132
self.stdout.write("")
133133

134134
while True:
@@ -154,29 +154,28 @@ def _test_interactive_request(self, enforcer: casbin.Enforcer, user_input: str)
154154
155155
Args:
156156
enforcer (casbin.Enforcer): The Casbin enforcer instance to use for testing.
157-
user_input (str): The user's input string in format 'subject action object scope'.
157+
user_input (str): The user's input string in format 'subject action scope'.
158158
159159
Expected format:
160160
subject: The requesting entity (e.g., 'user:alice')
161161
action: The requested action (e.g., 'act:read')
162-
object: The target resource (e.g., 'lib:test-lib')
163162
scope: The authorization context (e.g., 'org:OpenedX')
164163
"""
165164
try:
166165
parts = [part.strip() for part in user_input.split()]
167-
if len(parts) != 4:
168-
self.stdout.write(self.style.ERROR(f"✗ Invalid format. Expected 4 parts, got {len(parts)}"))
169-
self.stdout.write("Format: subject action object scope")
170-
self.stdout.write("Example: user:alice act:read lib:test-lib org:OpenedX")
166+
if len(parts) != 3:
167+
self.stdout.write(self.style.ERROR(f"✗ Invalid format. Expected 3 parts, got {len(parts)}"))
168+
self.stdout.write("Format: subject action scope")
169+
self.stdout.write("Example: user:alice act:read org:OpenedX")
171170
return
172171

173-
subject, action, obj, scope = parts
174-
result = enforcer.enforce(subject, action, obj, scope)
172+
subject, action, scope = parts
173+
result = enforcer.enforce(subject, action, scope)
175174

176175
if result:
177-
self.stdout.write(self.style.SUCCESS(f"✓ ALLOWED: {subject} {action} {obj} {scope}"))
176+
self.stdout.write(self.style.SUCCESS(f"✓ ALLOWED: {subject} {action} {scope}"))
178177
else:
179-
self.stdout.write(self.style.ERROR(f"✗ DENIED: {subject} {action} {obj} {scope}"))
178+
self.stdout.write(self.style.ERROR(f"✗ DENIED: {subject} {action} {scope}"))
180179

181180
except (ValueError, IndexError, TypeError) as e:
182181
self.stdout.write(self.style.ERROR(f"✗ Error processing request: {str(e)}"))
Lines changed: 42 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,56 +1,64 @@
1-
# ===== POLICIES (p) =====
1+
############################################
2+
# Example Policies for Scope-based Model
3+
############################################
24

3-
# Platform-level permissions
5+
# ===== Policies (p) =====
6+
# Format: p, subject(role), action, scope, effect
7+
# Note: Scope-based authorization naturally prevents cross-scope access
8+
9+
# Global platform admin: can manage everything
410
p, role:platform_admin, act:manage, *, allow
511

6-
# Organization-level permissions
7-
p, role:org_admin, act:manage, lib:*, allow
8-
p, role:org_editor, act:edit, lib:*, allow
12+
# Org admin: can manage all resources within org OpenedX
13+
p, role:org_admin, act:manage, org:OpenedX, allow
14+
15+
# Org editor: can edit resources within org OpenedX
16+
p, role:org_editor, act:edit, org:OpenedX, allow
17+
18+
# Course instructor: can manage resources within a specific course
19+
p, role:course_instructor, act:manage, course-v1:OpenedX+DemoX+CS101, allow
920

10-
# Library-specific permissions
11-
p, role:library_author, act:edit, lib:*, allow
12-
p, role:library_reviewer, act:read, lib:*, allow
13-
p, role:editor, act:edit, lib:*, allow
21+
# Reviewer: can only read within a specific library
22+
p, role:library_reviewer, act:read, lib:math-basics, allow
1423

15-
# Report permissions
16-
p, role:report_viewer, act:read, report:*, allow
24+
# Org admin all: can manage all orgs
25+
p, role:org_admin_all, act:manage, org:*, allow
1726

18-
# Access restrictions and exceptions
19-
p, role:org_editor, act:edit, lib:restricted-content, deny
20-
p, role:org_admin, act:manage, lib:another-restricted-content, deny
27+
# Explictly deny the org admin for one org
28+
p, role:org_admin_all, act:manage, org:2U, deny
2129

2230

23-
# ===== ROLE ASSIGNMENTS (g) =====
31+
# ===== Role Assignments (g) =====
32+
# Format: g, user, role
2433

25-
# Platform administrators
26-
g, user:admin, role:platform_admin, *
34+
# Alice is platform admin
35+
g, user:alice, role:platform_admin
2736

28-
# Organization administrators
29-
g, user:alice, role:org_admin, org:OpenedX
37+
# Bob is org admin
38+
g, user:bob, role:org_admin
3039

31-
# Organization editors
32-
g, user:bob, role:org_editor, org:MIT
33-
g, user:paul, role:editor, org:OpenedX
40+
# Eve is org editor
41+
g, user:eve, role:org_editor
3442

35-
# Library authors
36-
g, user:mary, role:library_author, lib:math-basics
37-
g, user:john, role:library_author, lib:science-101
43+
# Carol is course instructor
44+
g, user:carol, role:course_instructor
3845

39-
# Library reviewers
40-
g, user:sarah, role:library_reviewer, lib:math-basics
46+
# David is library reviewer
47+
g, user:david, role:library_reviewer
4148

42-
# Report viewers
43-
g, user:maria, role:report_viewer, org:OpenedX
49+
# Steve is org admin (all orgs)
50+
g, user:steve, role:org_admin_all
4451

4552

46-
# ===== ACTION GROUPING (g2) =====
53+
# ===== Action Grouping (g2) =====
54+
# Format: g2, high-level-action, implied-action
4755

48-
# manage implies edit, delete, read, write
56+
# manage implies all other actions
4957
g2, act:manage, act:edit
58+
g2, act:manage, act:read
59+
g2, act:manage, act:write
5060
g2, act:manage, act:delete
51-
g2, act:edit, act:read
52-
g2, act:edit, act:write
5361

54-
# edit implies read, write
62+
# edit implies read and write
5563
g2, act:edit, act:read
5664
g2, act:edit, act:write

0 commit comments

Comments
 (0)