Skip to content

Commit 58bdf2c

Browse files
feat: first version of public API for roles
1 parent 79e56d4 commit 58bdf2c

11 files changed

Lines changed: 827 additions & 410 deletions

File tree

openedx_authz/api/permissions.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,14 @@
44
allowed actions(s) a subject can perform on an object. In Casbin, permissions
55
are not explicitly defined, but are inferred from the policy rules.
66
"""
7+
8+
9+
def has_permission(user: str, resource: str, action: str, scope: str = None) -> bool:
10+
"""Check if a user has a specific permission.
11+
12+
Args:
13+
user: The user to check.
14+
resource: The resource to check.
15+
action: The action to check.
16+
scope: The scope to check (optional).
17+
"""

openedx_authz/api/policy.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,11 @@
1212
"""
1313

1414
from django.db.models import QuerySet
15+
1516
from openedx_authz.engine.enforcer import enforcer
1617
from openedx_authz.engine.filter import Filter
1718

19+
1820
# TODO: should this be cached and called for each request depending on the user?
1921
def get_policies(filter: Filter) -> QuerySet:
2022
"""Get all policies from the policy store.
@@ -33,8 +35,8 @@ def get_policies(filter: Filter) -> QuerySet:
3335
['role:report_viewer', 'act:read', 'report:*', 'allow'],
3436
].
3537
"""
36-
# TODO: This should be a queryset that's evaluated only when enforcing
37-
# Here we have a filter that we should turn into Q objects to load
38-
# a qs into memory
39-
# Debemos probar que método de cache tiene mejor performance: org, user, SAOC
38+
# TODO: This should be a queryset that's evaluated only when enforcing
39+
# Here we have a filter that we should turn into Q objects to load
40+
# a qs into memory
41+
# Debemos probar que método de cache tiene mejor performance: org, user, SAOC
4042
return enforcer.load_filtered_policy(filter)

openedx_authz/api/roles.py

Lines changed: 110 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -8,97 +8,147 @@
88
assertions.
99
"""
1010

11+
from typing import Literal
12+
13+
from attrs import define
14+
15+
# from openedx_authz.engine.enforcer import enforcer
16+
# TODO: should we dependency inject the enforcer to the API functions?
17+
# For now, we create a global enforcer instance for testing purposes
1118
from openedx_authz.engine.enforcer import enforcer
1219

13-
# TODO: should we use attrs to define the Role class? Scopes and so on?
14-
# def create_role(
15-
# role_name: str,
16-
# description: str,
17-
# actions: list[str],
18-
# resources: list[str],
19-
# scopes: list[str] = None,
20-
# context: str = None,
21-
# inherit_from: str = None,
22-
# ) -> None:
23-
# """Create a new role in the policy store.
24-
25-
# Args:
26-
# role_name: The name of the role.
27-
# description: The description of the role.
28-
# """
29-
# pass
30-
31-
# def add_permission_to_role(role_name: str, permission_name: str) -> None:
32-
# """Add a permission to a role.
33-
34-
# Args:
35-
# role_name: The name of the role.
36-
# permission_name: The name of the permission.
37-
# """
38-
# pass
39-
40-
# def remove_permission_from_role(role_name: str, permission_name: str) -> None:
41-
# """Remove a permission from a role.
42-
43-
# Args:
44-
# role_name: The name of the role.
45-
# permission_name: The name of the permission.
46-
# """
47-
# pass
48-
49-
def get_permissions_for_role(role_name: str) -> list[str]:
50-
"""Get the permissions for a role.
5120

52-
Args:
53-
role_name: The name of the role.
21+
@define
22+
class Permission: # TODO: change to policy?
23+
"""A permission is an action that can be performed under certain conditions.
24+
25+
Attributes:
26+
name: The name of the permission.
27+
"""
28+
29+
# TODO: what other attributes should a permission have?
30+
name: str
31+
effect: Literal["allow", "deny"] = "allow"
32+
33+
34+
@define
35+
class Role:
36+
"""A role is a named group of permissions.
37+
38+
Attributes:
39+
name: The name of the role.
40+
permissions: A list of permissions assigned to the role.
41+
scopes: A list of scopes assigned to the role.
42+
metadata: A dictionary of metadata assigned to the role. This can include
43+
information such as the description of the role, creation date, etc.
5444
"""
5545

56-
def get_role_metadata(role_name: str) -> dict:
57-
"""Get the metadata for a role.
46+
name: str
47+
permissions: list[Permission] = None
48+
scopes: list[str] = None
49+
metadata: dict[str, str] = None
50+
51+
52+
def create_role_in_scope_and_assign_permissions(role_name: str, permissions: list[Permission], scope: str) -> None:
53+
"""Create a role and assign permissions to it.
5854
5955
Args:
6056
role_name: The name of the role.
57+
permissions: A list of permissions to assign to the role.
58+
scope: The scope in which to create the role.
6159
"""
62-
pass
60+
for permission in permissions:
61+
enforcer.add_policy(role_name, permission.name, scope, permission.effect)
62+
63+
64+
def get_permissions_for_roles(role_names: list[str]) -> dict[str, list[Permission]]:
65+
"""Get the permissions for a list of roles.
66+
67+
A permission is a policy rule with the effect 'allow' assigned to a role.
6368
64-
def assign_role_to_user(user_id: str, role_name: str) -> None:
69+
Args:
70+
role_names: A list of role names.
71+
72+
Returns:
73+
dict[str, list[Permission]]: A dictionary mapping role names to a list of permissions.
74+
"""
75+
# TODO: do I need to return implicit permissions as well?
76+
# TODO: This considers that there is no inheritance between roles
77+
# TODO: should we say policies instead of permissions?
78+
permissions_by_role = {}
79+
for role_name in role_names:
80+
permissions = enforcer.get_permissions_for_user(role_name)
81+
permissions_by_role[role_name] = [
82+
Permission(name=perm[1], effect=perm[3]) for perm in permissions
83+
]
84+
return permissions_by_role
85+
86+
87+
def assign_role_to_user_in_scope(username: str, role_name: str, scope: str) -> None:
6588
"""Assign a role to a user.
6689
6790
Args:
68-
user_id: The ID of the user.
91+
username: The ID of the user.
6992
role_name: The name of the role.
93+
scope: The scope in which to assign the role.
7094
"""
71-
pass
95+
return enforcer.add_role_for_user_in_domain(username, role_name, scope)
96+
7297

73-
def unassign_role_from_user(user_id: str, role_name: str) -> None:
98+
def unassign_role_from_user_in_scope(username: str, role_name: str, scope: str) -> None:
7499
"""Unassign a role from a user.
75100
76101
Args:
77-
user_id: The ID of the user.
102+
username: The ID of the user.
78103
role_name: The name of the role.
104+
scope: The scope from which to unassign the role.
79105
"""
80-
pass
106+
return enforcer.remove_role_for_user_in_domain(username, role_name, scope)
81107

82-
def get_available_roles() -> list[str]:
83-
"""Get the available roles.
108+
109+
def get_all_roles() -> list[Role]:
110+
"""Get all the available roles in the current environment.
84111
85112
Returns:
86-
A list of available roles.
113+
list[Role]: A list of role names and all their metadata.
87114
"""
88-
pass
115+
return enforcer.get_all_subjects()
89116

90-
def get_users_for_role(role_name: str) -> list[str]:
91-
"""Get the users for a role.
117+
118+
def get_roles_in_scope(scope: str) -> list[Role]:
119+
"""Get the available roles for the current environment.
120+
121+
In this case, we return all the roles defined in the policy file that
122+
match the given scope.
92123
93124
Args:
94-
role_name: The name of the role.
125+
scope: The scope to filter roles (e.g., 'library:123' or '*' for global).
126+
127+
Returns:
128+
list[Role]: A list of roles available in the specified scope.
95129
"""
96-
pass
130+
return enforcer.get_all_roles_by_domain(scope)
131+
97132

98-
def get_roles_for_user(user_id: str) -> list[str]:
133+
def get_roles_for_user_in_scope(username: str, scope: str) -> list[Role]:
99134
"""Get the roles for a user.
100135
101136
Args:
102-
user_id: The ID of the user.
137+
username: The ID of the user namespaced (e.g., 'user:john_doe').
138+
139+
Returns:
140+
list[Role]: A list of role names and all their metadata assigned to the user.
141+
"""
142+
return enforcer.get_roles_for_user_in_domain(username, scope)
143+
144+
145+
def get_users_for_role_in_scope(role_name: str, scope: str) -> list[str]:
146+
"""Get the users for a role.
147+
148+
Args:
149+
role_name: The name of the role.
150+
151+
Returns:
152+
list[str]: A list of user IDs (usernames) assigned to the role.
103153
"""
104-
pass
154+
return enforcer.get_users_for_role_in_domain(role_name, scope)

openedx_authz/api/tests/test_roles.py

Whitespace-only changes.

openedx_authz/engine/adapter.py

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -67,16 +67,18 @@ class ExtendedAdapter(Adapter, FilteredAdapter):
6767
FilteredAdapter: Interface for filtered policy loading.
6868
"""
6969

70-
def is_filtered(self) -> bool:
71-
"""
72-
Check if the adapter supports filtering.
73-
74-
Returns:
75-
bool: True if the adapter supports filtered policy loading, False otherwise.
76-
"""
77-
return True
78-
79-
def load_filtered_policy(self, model: Model, filter: Filter) -> None: # pylint: disable=redefined-builtin
70+
# def is_filtered(self) -> bool:
71+
# """
72+
# Check if the adapter supports filtering.
73+
74+
# Returns:
75+
# bool: True if the adapter supports filtered policy loading, False otherwise.
76+
# """
77+
# return True
78+
79+
def load_filtered_policy(
80+
self, model: Model, filter: Filter
81+
) -> None: # pylint: disable=redefined-builtin
8082
"""
8183
Load policy rules from storage with filtering applied.
8284
@@ -99,7 +101,9 @@ def load_filtered_policy(self, model: Model, filter: Filter) -> None: # pylint:
99101
for line in filtered_queryset:
100102
persist.load_policy_line(str(line), model)
101103

102-
def filter_query(self, queryset: QuerySet, filter: Filter) -> QuerySet: # pylint: disable=redefined-builtin
104+
def filter_query(
105+
self, queryset: QuerySet, filter: Filter
106+
) -> QuerySet: # pylint: disable=redefined-builtin
103107
"""
104108
Apply filter criteria to the policy queryset.
105109
Lines changed: 57 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,59 @@
1-
# ===== ACTION GROUPING (g2) =====
1+
# Policies for roles - format: p = subject(role), action, scope, effect
2+
# Library Admin Role Policies
3+
p, role:library_admin, act:delete_library, library:*, allow
4+
p, role:library_admin, act:publish_library, library:*, allow
5+
p, role:library_admin, act:manage_library_team, library:*, allow
6+
p, role:library_admin, act:manage_library_tags, library:*, allow
7+
p, role:library_admin, act:delete_library_content, library:*, allow
8+
p, role:library_admin, act:publish_library_content, library:*, allow
9+
p, role:library_admin, act:delete_library_collection, library:*, allow
10+
p, role:library_admin, act:create_library, library:*, allow
11+
p, role:library_admin, act:create_library_collection, library:*, allow
212

3-
# manage implies edit, delete, read, write
4-
g2, act:manage, act:edit
5-
g2, act:manage, act:delete
6-
g2, act:edit, act:read
7-
g2, act:edit, act:write
13+
# Library Author Role Policies
14+
p, role:library_author, act:delete_library_content, library:*, allow
15+
p, role:library_author, act:publish_library_content, library:*, allow
16+
p, role:library_author, act:edit_library, library:*, allow
17+
p, role:library_author, act:manage_library_tags, library:*, allow
18+
p, role:library_author, act:create_library_collection, library:*, allow
19+
p, role:library_author, act:edit_library_collection, library:*, allow
20+
p, role:library_author, act:delete_library_collection, library:*, allow
821

9-
# edit implies read, write
10-
g2, act:edit, act:read
11-
g2, act:edit, act:write
22+
# Library Collaborator Role Policies
23+
p, role:library_collaborator, act:edit_library, library:*, allow
24+
p, role:library_collaborator, act:delete_library_content, library:*, allow
25+
p, role:library_collaborator, act:manage_library_tags, library:*, allow
26+
p, role:library_collaborator, act:create_library_collection, library:*, allow
27+
p, role:library_collaborator, act:edit_library_collection, library:*, allow
28+
p, role:library_collaborator, act:delete_library_collection, library:*, allow
29+
30+
# Library User Role Policies
31+
p, role:library_user, act:view_library, library:*, allow
32+
p, role:library_user, act:view_library_team, library:*, allow
33+
p, role:library_user, act:reuse_library_content, library:*, allow
34+
35+
# User-to-Role assignments (g) - format: g = user, role, scope
36+
# These would be populated dynamically based on actual user assignments
37+
g, user:alice_admin, role:library_admin, library:math_101
38+
g, user:bob_author, role:library_author, library:history_201
39+
g, user:carol_collaborator, role:library_collaborator, library:science_301
40+
g, user:dave_user, role:library_user, library:english_101
41+
g, user:eve_multi, role:library_admin, library:physics_401
42+
g, user:eve_multi, role:library_author, library:chemistry_501
43+
g, user:frank_global, role:library_user, *
44+
45+
# Action Inheritance (g2) - format: g2 = parent_action, child_action
46+
# The logical inheritance hierarchy for actions
47+
g2, act:edit_library, act:delete_library
48+
g2, act:view_library, act:edit_library
49+
g2, act:edit_library, act:create_library
50+
g2, act:view_library, act:publish_library
51+
g2, act:view_library_team, act:manage_library_team
52+
g2, act:view_library_tags, act:manage_library_tags
53+
g2, act:edit_library_collection, act:delete_library_collection
54+
g2, act:view_library_collection, act:edit_library_collection
55+
g2, act:edit_library_collection, act:create_library_collection
56+
g2, act:view_library_content, act:edit_library_content
57+
g2, act:edit_library_content, act:delete_library_content
58+
g2, act:view_library_content, act:publish_library_content
59+
g2, act:view_library_content, act:reuse_library_content

openedx_authz/engine/watcher.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ def create_watcher():
5050
return watcher
5151
except Exception as e:
5252
logger.error(f"Failed to create Redis watcher: {e}")
53-
raise
53+
return None
5454

5555

5656
if settings.CASBIN_WATCHER_ENABLED:

0 commit comments

Comments
 (0)