Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
c72eca5
feat: add scaffolding for python API for roles
mariajgrimaldi Sep 23, 2025
b39f026
feat: first version of public API for roles
mariajgrimaldi Sep 24, 2025
18232a3
refactor: update gitignore with .sqlite files
mariajgrimaldi Sep 29, 2025
ca26e5e
refactor: place roles, permissions and user functions into their own …
mariajgrimaldi Sep 29, 2025
bf94a4b
refactor: wrap namespace into attrs classes
mariajgrimaldi Sep 30, 2025
172aeb6
test: add test suite for enforcer based on policy lifecycle
mariajgrimaldi Sep 30, 2025
7920359
fix: remove conflict markings
mariajgrimaldi Sep 30, 2025
4c0d56e
fix: revert changes in non-relevant files
mariajgrimaldi Sep 30, 2025
dfc8c96
refactor: change user to subject
mariajgrimaldi Sep 30, 2025
624ea8f
refactor: make specific data classes inherit from generic
mariajgrimaldi Oct 1, 2025
75e61de
refactor: drop unnecessary assert
mariajgrimaldi Oct 1, 2025
637cbdd
refactor: remove duplication while assigning
mariajgrimaldi Oct 1, 2025
6c7d069
refactor: completely abstract consumers of internal naming conventions
mariajgrimaldi Oct 1, 2025
947d1cc
refactor: return typed role assignemnts for easier management
mariajgrimaldi Oct 1, 2025
d762a84
test: add tests for users and data modules
mariajgrimaldi Oct 1, 2025
f2c164f
refactor: add tests for getting assignments for role
mariajgrimaldi Oct 1, 2025
fa51feb
refactor: drop print for debugging
mariajgrimaldi Oct 1, 2025
147d533
refactor: drop assert to check duplicates
mariajgrimaldi Oct 1, 2025
d003279
feat: implement function to get all role assignments within a scope
mariajgrimaldi Oct 2, 2025
23936eb
temp: hardcode content library scope while implementing factory pattern
mariajgrimaldi Oct 2, 2025
776058b
refactor: generalize definitions for data classes
mariajgrimaldi Oct 2, 2025
ccc1088
refactor: make easier to change separator and namespace
mariajgrimaldi Oct 3, 2025
3098228
refactor: implement factory class as metaclass for scope and libraries
mariajgrimaldi Oct 3, 2025
f39a330
feat: implement factory class as metaclass for subject
mariajgrimaldi Oct 6, 2025
bf7e447
refactor: drop black changes for not modified files
mariajgrimaldi Oct 6, 2025
3593be0
refactor: address quality issues
mariajgrimaldi Oct 6, 2025
becc68e
refactor: address quality issues
mariajgrimaldi Oct 6, 2025
c97a4c4
refactor: address doc quality issues
mariajgrimaldi Oct 6, 2025
c75bfb7
refactor: drop :no-index: for a more maintainable solution
mariajgrimaldi Oct 6, 2025
ee329fa
refactor: address docs quality failures
mariajgrimaldi Oct 6, 2025
3a10db0
refactor: drop debug prints
mariajgrimaldi Oct 6, 2025
4535f9f
refactor: raise value error when policy is malformed
mariajgrimaldi Oct 6, 2025
4ebed98
refactor: add cases for post_init method for attrs classes
mariajgrimaldi Oct 6, 2025
e27c02a
docs: add comment to migrate class ContentLibraryData(ScopeData)
mariajgrimaldi Oct 6, 2025
a11161f
refactor: address PR reviews
mariajgrimaldi Oct 6, 2025
152f738
refactor: group role assignments for all subjects
mariajgrimaldi Oct 6, 2025
c347a18
refactor: make defaults maintainable over time
mariajgrimaldi Oct 6, 2025
5e4dfcd
refactor: update docstrings for new separator
mariajgrimaldi Oct 6, 2025
3778ffe
docs: use autodoc mock imports configuration to avoid duplicates failure
mariajgrimaldi Oct 6, 2025
92e1d3e
docs: change the docstring for the engine utils to match generalization
mariajgrimaldi Oct 6, 2025
769e223
refactor: add error management when getting scope subclasses
mariajgrimaldi Oct 7, 2025
59cd003
refactor: use is_user_allowed instead of has permission to improve co…
mariajgrimaldi Oct 7, 2025
b7dbf55
refactor: address quality errors
mariajgrimaldi Oct 7, 2025
1383535
refactor: add __str__ and __repr__ to data classes for better represe…
mariajgrimaldi Oct 8, 2025
c97cdf9
refactor: address PR reviews
mariajgrimaldi Oct 8, 2025
b077c11
docs: improve docstrings of data classes
mariajgrimaldi Oct 8, 2025
2a31090
docs: update docstrings with latest model changes
mariajgrimaldi Oct 9, 2025
66c8eec
refactor: address PR reviews for namespaced key
mariajgrimaldi Oct 9, 2025
a850d98
test: add test cases for empty namespaces
mariajgrimaldi Oct 9, 2025
9eb6c8f
docs: add use for generic scopes
mariajgrimaldi Oct 9, 2025
72e65e1
refactor: address quality issues
mariajgrimaldi Oct 10, 2025
7a72d93
docs: update changelog for release
mariajgrimaldi Oct 10, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -64,5 +64,6 @@ docs/openedx_authz.*.rst
requirements/private.in
requirements/private.txt

# Sqlite Database
# Persistent database files
*.sqlite3
*.db
11 changes: 11 additions & 0 deletions openedx_authz/api/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
"""Public API for the Open edX AuthZ framework.

This module provides a public API as part of the Open edX AuthZ framework. This
is part of the Open edX Layer used to abstract the authorization engine and
provide a simpler interface for other services in the Open edX ecosystem.
"""

from openedx_authz.api.data import *
from openedx_authz.api.permissions import *
from openedx_authz.api.roles import *
from openedx_authz.api.users import *
151 changes: 151 additions & 0 deletions openedx_authz/api/data.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
"""Data classes and enums for representing roles, permissions, and policies."""

from enum import Enum
from typing import Literal

from attrs import define


class GroupingPolicyIndex(Enum):
"""Index of fields in a grouping policy."""

SUBJECT = 0
ROLE = 1
SCOPE = 2
# The rest of the fields are optional and can be ignored for now


class PolicyIndex(Enum):
"""Index of fields in a policy."""

ROLE = 0
ACT = 1
SCOPE = 2
EFFECT = 3
# The rest of the fields are optional and can be ignored for now


@define
class UserData:
"""A user is a subject that can be assigned roles and permissions.

Attributes:
username: The username. Automatically prefixed with 'user:' if not present.
"""

username: str

def __attrs_post_init__(self):
"""Ensure username has 'user:' namespace prefix."""
if not self.username.startswith("user:"):
object.__setattr__(self, "username", f"user:{self.username}")


@define
class ScopeData:
"""A scope is a context in which roles and permissions are assigned.

Attributes:
scope_id: The scope identifier (e.g., 'course-v1:edX+DemoX+2021_T1').

This class assumes that the scope is already namespaced appropriately
before being passed in, as scopes can vary widely (e.g., courses, organizations).
"""

scope_id: str


@define
class SubjectData:
"""A subject is an entity that can be assigned roles and permissions.

Attributes:
subject_id: The subject identifier namespaced (e.g., 'user:john_doe').

This class assumes that the subject was already namespaced by their own
type (e.g., 'user:', 'group:') before being passed in since subjects can be
users, groups, or other entities.
"""

subject_id: str


@define
class ActionData:
"""An action is an operation that can be performed in a specific scope.

Attributes:
action: The action name. Automatically prefixed with 'act:' if not present.
"""

action_id: str

def __attrs_post_init__(self):
"""Ensure action name has 'act:' namespace prefix."""
if not self.action_id.startswith("act:"):
object.__setattr__(self, "action_id", f"act:{self.action_id}")


@define
class PermissionData: # TODO: change to policy?
"""A permission is an action that can be performed under certain conditions.

Attributes:
name: The name of the permission.
"""

action: ActionData
effect: Literal["allow", "deny"] = "allow"


@define
class RoleMetadataData:
"""Metadata for a role.

Attributes:
description: A description of the role.
created_at: The date and time the role was created.
created_by: The ID of the subject who created the role.
"""

description: str = None
created_at: str = None
created_by: str = None


@define
class RoleData:
"""A role is a named group of permissions.

Attributes:
name: The name of the role. Must have 'role:' namespace prefix.
permissions: A list of permissions assigned to the role.
scopes: A list of scopes assigned to the role.
metadata: A dictionary of metadata assigned to the role. This can include
Comment thread
bmtcril marked this conversation as resolved.
Outdated
information such as the description of the role, creation date, etc.
"""

name: str
permissions: list[PermissionData] = None
Comment thread
mariajgrimaldi marked this conversation as resolved.
Outdated
metadata: RoleMetadataData = None

def __attrs_post_init__(self):
"""Ensure role name has 'role:' namespace prefix."""
if not self.name.startswith("role:"):
object.__setattr__(self, "name", f"role:{self.name}")


@define
class RoleAssignmentData:
"""A role assignment is the assignment of a role to a subject in a specific scope.

Attributes:
subject: The ID of the user namespaced (e.g., 'user:john_doe').
email: The email of the user.
Comment thread
bmtcril marked this conversation as resolved.
Outdated
role_name: The name of the role.
scope: The scope in which the role is assigned.
"""

subject: UserData
role: RoleData
scope: ScopeData
64 changes: 64 additions & 0 deletions openedx_authz/api/permissions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
"""Public API for permissions management.

A permission is the authorization granted by a policy. It represents the
allowed actions(s) a subject can perform on an object. In Casbin, permissions
are not explicitly defined, but are inferred from the policy rules.
"""

from openedx_authz.api.data import ActionData, PermissionData, PolicyIndex, ScopeData, SubjectData
from openedx_authz.engine.enforcer import enforcer

__all__ = [
"get_permission_from_policy",
"get_all_permissions_in_scope",
"has_permission",
]


def get_permission_from_policy(policy: list[str]) -> PermissionData:
"""Convert a Casbin policy list to a PermissionData object.

Args:
policy: A list representing a Casbin policy.

Returns:
PermissionData: The corresponding PermissionData object or an empty PermissionData if the policy is invalid.
"""
if len(policy) < 4: # Do not count ptype
Comment thread
bmtcril marked this conversation as resolved.
return PermissionData(action=ActionData(action_id=""), effect="allow")

return PermissionData(
action=ActionData(action_id=policy[PolicyIndex.ACT.value]),
effect=policy[PolicyIndex.EFFECT.value],
)


def get_all_permissions_in_scope(scope: ScopeData) -> list[PermissionData]:
"""Retrieve all permissions associated with a specific scope.

Args:
scope: The scope to filter permissions by.

Returns:
list of PermissionData: A list of PermissionData objects associated with the given scope.
"""
actions = enforcer.get_filtered_policy(PolicyIndex.SCOPE.value, scope.scope_id)
return [get_permission_from_policy(action) for action in actions]


def has_permission(
subject: SubjectData,
action: ActionData,
scope: ScopeData,
) -> bool:
"""Check if a subject has a specific permission in a given scope.

Args:
subject: The subject to check (e.g., user or service).
action: The action to check (e.g., 'view_course').
scope: The scope in which to check the permission (e.g., 'course-v1:edX+DemoX+2021_T1').

Returns:
bool: True if the subject has the specified permission in the scope, False otherwise.
"""
return enforcer.enforce(subject.subject_id, action.action_id, scope.scope_id)
Loading
Loading