-
Notifications
You must be signed in to change notification settings - Fork 6
[FC-0099] feat: add public API to interact with roles and permissions #75
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 22 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 b39f026
feat: first version of public API for roles
mariajgrimaldi 18232a3
refactor: update gitignore with .sqlite files
mariajgrimaldi ca26e5e
refactor: place roles, permissions and user functions into their own …
mariajgrimaldi bf94a4b
refactor: wrap namespace into attrs classes
mariajgrimaldi 172aeb6
test: add test suite for enforcer based on policy lifecycle
mariajgrimaldi 7920359
fix: remove conflict markings
mariajgrimaldi 4c0d56e
fix: revert changes in non-relevant files
mariajgrimaldi dfc8c96
refactor: change user to subject
mariajgrimaldi 624ea8f
refactor: make specific data classes inherit from generic
mariajgrimaldi 75e61de
refactor: drop unnecessary assert
mariajgrimaldi 637cbdd
refactor: remove duplication while assigning
mariajgrimaldi 6c7d069
refactor: completely abstract consumers of internal naming conventions
mariajgrimaldi 947d1cc
refactor: return typed role assignemnts for easier management
mariajgrimaldi d762a84
test: add tests for users and data modules
mariajgrimaldi f2c164f
refactor: add tests for getting assignments for role
mariajgrimaldi fa51feb
refactor: drop print for debugging
mariajgrimaldi 147d533
refactor: drop assert to check duplicates
mariajgrimaldi d003279
feat: implement function to get all role assignments within a scope
mariajgrimaldi 23936eb
temp: hardcode content library scope while implementing factory pattern
mariajgrimaldi 776058b
refactor: generalize definitions for data classes
mariajgrimaldi ccc1088
refactor: make easier to change separator and namespace
mariajgrimaldi 3098228
refactor: implement factory class as metaclass for scope and libraries
mariajgrimaldi f39a330
feat: implement factory class as metaclass for subject
mariajgrimaldi bf7e447
refactor: drop black changes for not modified files
mariajgrimaldi 3593be0
refactor: address quality issues
mariajgrimaldi becc68e
refactor: address quality issues
mariajgrimaldi c97a4c4
refactor: address doc quality issues
mariajgrimaldi c75bfb7
refactor: drop :no-index: for a more maintainable solution
mariajgrimaldi ee329fa
refactor: address docs quality failures
mariajgrimaldi 3a10db0
refactor: drop debug prints
mariajgrimaldi 4535f9f
refactor: raise value error when policy is malformed
mariajgrimaldi 4ebed98
refactor: add cases for post_init method for attrs classes
mariajgrimaldi e27c02a
docs: add comment to migrate class ContentLibraryData(ScopeData)
mariajgrimaldi a11161f
refactor: address PR reviews
mariajgrimaldi 152f738
refactor: group role assignments for all subjects
mariajgrimaldi c347a18
refactor: make defaults maintainable over time
mariajgrimaldi 5e4dfcd
refactor: update docstrings for new separator
mariajgrimaldi 3778ffe
docs: use autodoc mock imports configuration to avoid duplicates failure
mariajgrimaldi 92e1d3e
docs: change the docstring for the engine utils to match generalization
mariajgrimaldi 769e223
refactor: add error management when getting scope subclasses
mariajgrimaldi 59cd003
refactor: use is_user_allowed instead of has permission to improve co…
mariajgrimaldi b7dbf55
refactor: address quality errors
mariajgrimaldi 1383535
refactor: add __str__ and __repr__ to data classes for better represe…
mariajgrimaldi c97cdf9
refactor: address PR reviews
mariajgrimaldi b077c11
docs: improve docstrings of data classes
mariajgrimaldi 2a31090
docs: update docstrings with latest model changes
mariajgrimaldi 66c8eec
refactor: address PR reviews for namespaced key
mariajgrimaldi a850d98
test: add test cases for empty namespaces
mariajgrimaldi 9eb6c8f
docs: add use for generic scopes
mariajgrimaldi 72e65e1
refactor: address quality issues
mariajgrimaldi 7a72d93
docs: update changelog for release
mariajgrimaldi File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 * |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,235 @@ | ||
| """Data classes and enums for representing roles, permissions, and policies.""" | ||
|
|
||
| from enum import Enum | ||
| from typing import ClassVar, Literal, Type | ||
|
|
||
| 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 | ||
|
|
||
|
|
||
| class AuthzBaseClass: | ||
| """Base class for all authz classes. | ||
|
|
||
| Attributes: | ||
| SEPARATOR: The separator between the namespace and the identifier (e.g., ':', '@'). | ||
| NAMESPACE: The namespace prefix for the data type (e.g., 'user', 'role'). | ||
| """ | ||
|
|
||
| SEPARATOR: ClassVar[str] = "^" | ||
| NAMESPACE: ClassVar[str] = None | ||
|
|
||
| @define | ||
| class AuthZData(AuthzBaseClass): | ||
| """Base class for all authz data classes. | ||
|
|
||
| Attributes: | ||
| NAMESPACE: The namespace prefix for the data type (e.g., 'user', 'role'). | ||
| SEPARATOR: The separator between the namespace and the identifier (e.g., ':', '@'). | ||
|
mariajgrimaldi marked this conversation as resolved.
Outdated
|
||
| external_key: The ID for the object outside of the authz system (e.g., username). | ||
| Could also be used for human-readable names (e.g., role or action name). | ||
| namespaced_key: The ID for the object within the authz system (e.g., 'user@john_doe'). | ||
|
mariajgrimaldi marked this conversation as resolved.
Outdated
|
||
| """ | ||
|
|
||
| external_key: str = "" | ||
| namespaced_key: str = "" | ||
|
|
||
| def __attrs_post_init__(self): | ||
| """Post-initialization processing for attributes. | ||
|
|
||
| This method ensures that either external_key or namespaced_key is provided, | ||
| and derives the other attribute based on the NAMESPACE and SEPARATOR. | ||
|
|
||
| Note: | ||
| I will always instantiate with either external_key or namespaced_key, never both. | ||
| So we need to derive the other one based on the NAMESPACE. | ||
| """ | ||
| if self.NAMESPACE and not self.namespaced_key: | ||
| self.namespaced_key = f"{self.NAMESPACE}{self.SEPARATOR}{self.external_key}" | ||
|
bmtcril marked this conversation as resolved.
|
||
|
|
||
| if self.NAMESPACE and not self.external_key and self.namespaced_key: | ||
| self.external_key = self.namespaced_key.split(self.SEPARATOR, 1)[1] | ||
|
|
||
|
|
||
| @define | ||
| class ScopeData(AuthZData): | ||
| """A scope is a context in which roles and permissions are assigned. | ||
|
|
||
| Attributes: | ||
| namespaced_key: The scope identifier (e.g., 'org@Demo'). | ||
|
mariajgrimaldi marked this conversation as resolved.
Outdated
|
||
| """ | ||
|
|
||
| NAMESPACE: ClassVar[str] = "sc" | ||
|
|
||
|
|
||
| @define | ||
| class ContentLibraryData(ScopeData): | ||
|
bmtcril marked this conversation as resolved.
|
||
| """A content library is a collection of content items. | ||
|
|
||
| Attributes: | ||
| library_id: The content library identifier (e.g., 'library-v1:edX+DemoX+2021_T1'). | ||
|
mariajgrimaldi marked this conversation as resolved.
Outdated
|
||
| namespaced_key: Inherited from ScopeData, auto-generated from name if not provided. | ||
| """ | ||
|
|
||
| NAMESPACE: ClassVar[str] = "lib" | ||
| library_id: str = "" | ||
|
mariajgrimaldi marked this conversation as resolved.
Outdated
|
||
|
|
||
| @property | ||
| def library_id(self) -> str: | ||
| """The library identifier as used in Open edX (e.g., 'math_101', 'library-v1:edX+DemoX'). | ||
|
mariajgrimaldi marked this conversation as resolved.
Outdated
|
||
|
|
||
| This is an alias for external_key that represents the library ID without the namespace prefix. | ||
|
|
||
| Returns: | ||
| str: The library identifier without namespace. | ||
| """ | ||
| return self.external_key | ||
|
|
||
|
|
||
| @define | ||
| class SubjectData(AuthZData): | ||
| """A subject is an entity that can be assigned roles and permissions. | ||
|
|
||
| Attributes: | ||
| namespaced_key: The subject identifier namespaced (e.g., 'sub@generic'). | ||
|
mariajgrimaldi marked this conversation as resolved.
Outdated
|
||
| """ | ||
|
|
||
| NAMESPACE: ClassVar[str] = "sub" | ||
|
|
||
| @define | ||
| class UserData(SubjectData): | ||
| """A user is a subject that can be assigned roles and permissions. | ||
|
|
||
| Attributes: | ||
| username: The username for the user (e.g., 'john_doe'). | ||
|
bmtcril marked this conversation as resolved.
Outdated
|
||
| namespaced_key: Inherited from SubjectData, auto-generated from username if not provided. | ||
|
|
||
| This class automatically adds the 'user@' namespace prefix to the subject ID. | ||
|
mariajgrimaldi marked this conversation as resolved.
Outdated
|
||
| Can be initialized with either external_key= or namespaced_key= parameter. | ||
| """ | ||
|
|
||
| NAMESPACE: ClassVar[str] = "user" | ||
|
|
||
| @property | ||
| def username(self) -> str: | ||
| """The username for the user (e.g., 'john_doe'). | ||
|
|
||
| This is an alias for external_key that represents the username without the namespace prefix. | ||
|
|
||
| Returns: | ||
| str: The username without namespace. | ||
| """ | ||
| return self.external_key | ||
|
|
||
|
|
||
| @define | ||
| class ActionData(AuthZData): | ||
| """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. | ||
|
mariajgrimaldi marked this conversation as resolved.
Outdated
|
||
| """ | ||
|
|
||
| NAMESPACE: ClassVar[str] = "act" | ||
| name: str = "" | ||
|
mariajgrimaldi marked this conversation as resolved.
Outdated
|
||
|
|
||
| @property | ||
| def name(self) -> str: | ||
| """The human-readable name of the action (e.g., 'Delete Library', 'Edit Content'). | ||
|
|
||
| This property transforms the external_key into a human-readable display name | ||
| by replacing underscores with spaces and capitalizing each word. | ||
|
|
||
| Returns: | ||
| str: The human-readable action name (e.g., 'Delete Library'). | ||
| """ | ||
| return self.external_key.replace("_", " ").title() | ||
|
bmtcril marked this conversation as resolved.
|
||
|
|
||
|
|
||
| @define | ||
| class PermissionData(AuthZData): | ||
| """A permission is an action that can be performed under certain conditions. | ||
|
|
||
| Attributes: | ||
| name: The name of the permission. | ||
| """ | ||
|
|
||
| action: ActionData = None | ||
| effect: Literal["allow", "deny"] = "allow" | ||
|
|
||
|
|
||
| @define | ||
| class RoleMetadataData(AuthZData): | ||
| """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 | ||
|
mariajgrimaldi marked this conversation as resolved.
Outdated
|
||
|
|
||
|
|
||
| @define | ||
| class RoleData(AuthZData): | ||
| """A role is a named group of permissions. | ||
|
|
||
| Attributes: | ||
| name: The name of the role. Must have 'role@' namespace prefix. | ||
|
mariajgrimaldi marked this conversation as resolved.
Outdated
|
||
| role_id: The role identifier namespaced (e.g., 'role@instructor'). | ||
| permissions: A list of permissions assigned to the role. | ||
| metadata: A dictionary of metadata assigned to the role. This can include | ||
|
bmtcril marked this conversation as resolved.
Outdated
|
||
| information such as the description of the role, creation date, etc. | ||
| """ | ||
|
|
||
| NAMESPACE: ClassVar[str] = "role" | ||
| permissions: list[PermissionData] = None | ||
|
mariajgrimaldi marked this conversation as resolved.
Outdated
|
||
| metadata: RoleMetadataData = None | ||
|
|
||
| @property | ||
| def name(self) -> str: | ||
| """The human-readable name of the role (e.g., 'Library Admin', 'Course Instructor'). | ||
|
|
||
| This property transforms the external_key into a human-readable display name | ||
| by replacing underscores with spaces and capitalizing each word. | ||
|
|
||
| Returns: | ||
| str: The human-readable role name (e.g., 'Library Admin'). | ||
| """ | ||
| return self.external_key.replace("_", " ").title() | ||
|
|
||
|
|
||
| @define | ||
| class RoleAssignmentData(AuthZData): | ||
|
mariajgrimaldi marked this conversation as resolved.
Outdated
|
||
| """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'). | ||
|
bmtcril marked this conversation as resolved.
Outdated
|
||
| email: The email of the user. | ||
|
bmtcril marked this conversation as resolved.
Outdated
|
||
| role_name: The name of the role. | ||
| scope: The scope in which the role is assigned. | ||
| """ | ||
|
|
||
| subject: SubjectData = None | ||
| role: RoleData = None | ||
| scope: ScopeData = None | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,70 @@ | ||
| """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 | ||
|
bmtcril marked this conversation as resolved.
|
||
| return PermissionData(action=ActionData(namespaced_key=""), effect="allow") | ||
|
|
||
| return PermissionData( | ||
| action=ActionData(namespaced_key=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.namespaced_key) | ||
| 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.namespaced_key, action.namespaced_key, scope.namespaced_key) | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.