-
Notifications
You must be signed in to change notification settings - Fork 2
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
Fix permission issues #155
Open
yld-weng
wants to merge
7
commits into
dev
Choose a base branch
from
fix/permissions
base: dev
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+188
−30
Open
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
02e9daa
docs: add explanation for requires_permission function
yld-weng 60b1b56
fix: Error when member or guest navigating to myorganisation
yld-weng 78c8144
chore: add required obj params to requires_permission function
yld-weng 94f6093
docs: permission system
yld-weng c0c50b0
refactor: improve organisation projects fetching queries
yld-weng 5fee59a
chore: obj_param should be a required field
yld-weng d5852a9
chore: handle zero organisation
yld-weng 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
There are no files selected for viewing
This file contains 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,91 @@ | ||
# Permission Management | ||
|
||
This folder implements a permission management system that provides role-based access control (RBAC) for organisations and projects. The system implements a flexible permissions model with support for admin and project manager roles, along with granular view/edit permissions at both organisation and project levels. | ||
|
||
## Implementation | ||
|
||
The current implementation provides: | ||
|
||
- Abstract base class `BasePermissionService` with core permission methods | ||
- Permission decorators `requires_permission` for method-level access control for services | ||
|
||
Service classes can inherit from `BasePermissionService` to implement custom permission checks. The `requires_permission` decorator can be used to enforce access control at the method level. | ||
|
||
Why don't we use Django's built-in permissions system? The Django permissions system is designed for managing (global) access to models and views within a Django application. It is not designed to handle complex permission requirements such as role-based access control (RBAC) across multiple resources. The custom permission management system provides more flexibility and control over access control requirements. | ||
|
||
## Permission Model | ||
|
||
### Roles | ||
|
||
For simplicity, the system supports two roles: | ||
|
||
- Admin: Full access to organisation and its projects | ||
- Project Manager: Limited access based on granted permissions. A PM can be granted view and edit permissions for multiple projects within an organisation. | ||
|
||
### Permission Levels | ||
|
||
View: Read-only access | ||
Edit: Ability to modify resources | ||
Delete: Ability to remove resources | ||
Create: Ability to create new resources | ||
|
||
|
||
## Usage | ||
|
||
```python | ||
@requires_permission("view", obj_param="organisation") | ||
def get_organisation(self, user: User, organisation: Organisation) -> Organisation: | ||
return organisation | ||
``` | ||
|
||
The `requires_permission` decorator can be used to enforce access control at the method level. The decorator takes the permission level and the object parameter name as arguments. The permission level is used to check if the user has the required permission to access the object. The `obj_param` argument is used to specify the name of the object parameter in the method signature. If `organisation: Organisation` were renamed to `org: Organisation`, the decorator would be `@requires_permission("view", obj_param="org")`. | ||
|
||
|
||
## Future Improvements | ||
|
||
The current permission system utilises a decorator-based approach with service-level checks, centred around `@requires_permission` and role verification methods. Its strength lies in simplicity and maintainability - the decorator pattern makes permissions explicit, while centralised service logic ensures consistent enforcement across the application. This design fits well with the service-oriented architecture and makes permission checks reusable across different views. | ||
|
||
However, the system has notable limitations. Performance can be a concern due to multiple database queries per check with no built-in caching. The hardcoded roles (_Admin_ and _Project Manager_) make it inflexible for custom permission schemes. Some code duplication exists across services, and testing requires some mocking setups. | ||
|
||
The current approach works well for basic needs, its simplicity comes at the cost of flexibility and scalability. Future improvements could focus on implementing a policy-based system with better performance characteristics while maintaining the current system's clarity and ease of use, if required. | ||
|
||
For example: | ||
|
||
```python | ||
class Permission(Enum): | ||
VIEW = "view" | ||
EDIT = "edit" | ||
DELETE = "delete" | ||
CREATE = "create" | ||
|
||
@dataclass | ||
class OrganisationPolicy: | ||
user: User | ||
organisation: Organisation | ||
|
||
def get_role(self) -> Optional[str]: | ||
membership = self.organisation.organisationmembership_set.filter( | ||
user=self.user | ||
).first() | ||
return membership.role if membership else None | ||
|
||
def can(self, permission: Permission) -> bool: | ||
role = self.get_role() | ||
if not role: | ||
return False | ||
|
||
permission_matrix = { | ||
Permission.VIEW: [ROLE_ADMIN, ROLE_PROJECT_MANAGER], | ||
Permission.EDIT: [ROLE_ADMIN], | ||
Permission.DELETE: [ROLE_ADMIN], | ||
Permission.CREATE: [ROLE_ADMIN], | ||
} | ||
return role in permission_matrix[permission] | ||
|
||
class OrganisationService: | ||
def get_policy(self, user: User, org: Organisation) -> OrganisationPolicy: | ||
return OrganisationPolicy(user, org) | ||
|
||
def can_view(self, user: User, org: Organisation) -> bool: | ||
return self.get_policy(user, org).can(Permission.VIEW) | ||
``` |
This file contains 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 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 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 |
---|---|---|
|
@@ -20,7 +20,10 @@ class ProjectService(BasePermissionService): | |
"""Service for managing projects with integrated permissions""" | ||
|
||
def get_user_role(self, user: User, project: Project) -> Optional[str]: | ||
"""Get user's highest role across project's organisations""" | ||
""" | ||
Get user's highest role across project's organisations | ||
TODO: this assuming a project can be linked to multiple organisations, check if still the case | ||
""" | ||
project_orgs = project.organisations.all() | ||
|
||
# Check for admin role first | ||
|
@@ -53,7 +56,7 @@ def can_view(self, user: User, project: Project) -> bool: | |
if role == ROLE_ADMIN: | ||
return True | ||
elif role == ROLE_PROJECT_MANAGER: | ||
return self.get_user_permission(project, user) is not None | ||
return self.get_user_permission(user, project) is not None | ||
|
||
return False | ||
|
||
|
@@ -63,7 +66,7 @@ def can_edit(self, user: User, project: Project) -> bool: | |
if role == ROLE_ADMIN: | ||
return True | ||
elif role == ROLE_PROJECT_MANAGER: | ||
permission = self.get_user_permission(project, user) | ||
permission = self.get_user_permission(user, project) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fix for #102. |
||
return permission and permission.permission == "EDIT" | ||
|
||
return False | ||
|
@@ -81,22 +84,25 @@ def update_project(self, user: User, project: Project, data: Dict) -> Project: | |
"""Update project with provided data""" | ||
for key, value in data.items(): | ||
setattr(project, key, value) | ||
|
||
project.save() | ||
return project | ||
|
||
@requires_permission("view") | ||
@requires_permission("view", obj_param="project") | ||
def get_project(self, user: User, project: Project) -> Project: | ||
"""Get project if user has permission""" | ||
return project | ||
|
||
def create_project( | ||
self, user: User, name: str, organisation: Organisation, description: str="") -> Project: | ||
self, user: User, name: str, organisation: Organisation, description: str = "" | ||
) -> Project: | ||
"""Create a new project""" | ||
if not self.can_create(user): | ||
raise PermissionDenied("User cannot create projects") | ||
|
||
project = Project.objects.create(name=name, description=description, created_by=user) | ||
project = Project.objects.create( | ||
name=name, description=description, created_by=user | ||
) | ||
self.link_project_to_organisation( | ||
user=user, project=project, organisation=organisation, permission="EDIT" | ||
) | ||
|
@@ -110,8 +116,7 @@ def delete_project(self, user: User, project: Project): | |
project.delete() | ||
return parent_org | ||
|
||
|
||
@requires_permission("edit", "project") | ||
@requires_permission("edit", obj_param="project") | ||
def grant_permission( | ||
self, | ||
user: User, | ||
|
@@ -136,7 +141,7 @@ def grant_permission( | |
defaults={"granted_by": user, "permission": permission}, | ||
) | ||
|
||
@requires_permission("edit", "project") | ||
@requires_permission("edit", obj_param="project") | ||
def revoke_permission( | ||
self, user: User, project: Project, project_manager: User | ||
) -> None: | ||
|
This file contains 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 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
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this still true?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@yld-weng Thanks for this Yuliang. From my understanding, a project should only be linked to one org. But the PI (and their team) should have access to all projects/orgs who agree to share their data with us.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also just to clarify - all managers in a given organisation should have access to all of the org's projects.