-
-
Notifications
You must be signed in to change notification settings - Fork 23
feat: add API action to manage object level permission on Products #395
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 1 commit
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
dc0555c
Add API action to manage object level permission on Products #386
tdruez 12f1205
refine code and add test
tdruez 1d95465
refine the code
tdruez f199bda
refine code and add tests
tdruez d53c301
add support for group
tdruez 5947c7a
add documentation
tdruez 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 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,166 @@ | ||
| # | ||
| # Copyright (c) nexB Inc. and others. All rights reserved. | ||
| # DejaCode is a trademark of nexB Inc. | ||
| # SPDX-License-Identifier: AGPL-3.0-only | ||
| # See https://github.com/aboutcode-org/dejacode for support or download. | ||
| # See https://aboutcode.org for more information about AboutCode FOSS projects. | ||
| # | ||
|
|
||
|
|
||
| from django.contrib.auth import get_user_model | ||
| from django.core.exceptions import ObjectDoesNotExist | ||
|
|
||
| from guardian.shortcuts import assign_perm | ||
| from guardian.shortcuts import get_perms | ||
| from guardian.shortcuts import get_user_perms | ||
| from guardian.shortcuts import get_users_with_perms | ||
| from guardian.shortcuts import remove_perm | ||
| from rest_framework import permissions | ||
| from rest_framework import serializers | ||
| from rest_framework import status | ||
| from rest_framework.decorators import action | ||
| from rest_framework.response import Response | ||
|
|
||
| User = get_user_model() | ||
|
|
||
|
|
||
| class CanManageObjectPermissions(permissions.BasePermission): | ||
| """ | ||
| Allows managing object-level permissions if the user is: | ||
| - a superuser, or | ||
| - the object's owner (configurable via ``owner_field`` on the View), or | ||
| - has a special manage permission (global or object-level). | ||
| """ | ||
|
|
||
| owner_field = "created_by" | ||
| manage_permission_codename = "manage_object_permissions" | ||
|
|
||
| def has_object_permission(self, request, view, obj): | ||
| user = request.user | ||
| if not user.is_authenticated: | ||
| return False | ||
|
|
||
| # 1. Superusers always allowed | ||
| if user.is_superuser: | ||
| return True | ||
|
|
||
| # 2. Check if user matches object's owner field | ||
| # The field can be overridden on the ViewSet (e.g., owner_field = "owner") | ||
| owner_field = getattr(view, "owner_field", self.owner_field) | ||
| owner = getattr(obj, owner_field, None) | ||
| if owner == user: | ||
| return True | ||
|
|
||
| # 3. Check for specific manage permission (global or object-level) | ||
| app_label = obj._meta.app_label | ||
| codename = getattr(view, "manage_permission_codename", self.manage_permission_codename) | ||
| perm_name = f"{app_label}.{codename}" | ||
| if user.has_perm(perm_name) or user.has_perm(perm_name, obj): | ||
| return True | ||
|
|
||
| return False | ||
|
|
||
|
|
||
| class ObjectPermissionSerializer(serializers.Serializer): | ||
| """ | ||
| Generic serializer for representing or updating object-level permissions. | ||
| Accepts: | ||
| - user: user ID | ||
| - permissions: list of permission codenames | ||
| """ | ||
|
|
||
| # TODO: Scope by dataspace, see DataspacedSlugRelatedField | ||
| user = serializers.SlugRelatedField( | ||
| queryset=User.objects.all(), | ||
| slug_field="username", | ||
| ) | ||
| # user = DataspacedSlugRelatedField(slug_field="username") | ||
| permissions = serializers.ListField(child=serializers.CharField(), allow_empty=False) | ||
|
|
||
| class Meta: | ||
| fields = ( | ||
| "user", | ||
| "permissions", | ||
| ) | ||
|
|
||
| def to_representation(self, instance): | ||
| """Make sure to provide the target object in context via `context["object"]`.""" | ||
| obj = self.context.get("object") | ||
| user = instance | ||
| return { | ||
| "dataspace": user.dataspace.name, | ||
| "username": user.get_username(), | ||
| "object_permissions": get_user_perms(user, obj), | ||
| "model_permissions": get_perms(user, obj), | ||
| } | ||
|
|
||
|
|
||
| class ObjectPermissionsMixin: | ||
| """ | ||
| Mixin that adds a `/permissions/` endpoint for any object-level ViewSet. | ||
| Supports GET (list), POST (assign), and DELETE (remove) operations. | ||
|
|
||
| GET /api/{model}/{uuid}/permissions/ → list all users and perms | ||
| POST /api/{model}/{uuid}/permissions/ → assign perms to a user | ||
| DELETE /api/{model}/{uuid}/permissions/ → remove perms from a user | ||
| """ | ||
|
|
||
| @action( | ||
| detail=True, | ||
| methods=["get", "post", "delete"], | ||
| url_path="permissions", | ||
| serializer_class=ObjectPermissionSerializer, | ||
| permission_classes=[CanManageObjectPermissions], | ||
| ) | ||
| def manage_permissions(self, request, *args, **kwargs): | ||
| """ | ||
| Manage object-level permissions for this object. | ||
|
|
||
| - GET: List users and their permissions. | ||
| - POST: Assign permissions to a user. Provide `user` ID and `permissions` list. | ||
| - DELETE: Remove permissions from a user. Provide `user` ID and `permissions` | ||
| list. | ||
| """ | ||
| obj = self.get_object() | ||
| serializer_context = {"object": obj} | ||
|
|
||
| if request.method == "GET": | ||
| users_with_perms = get_users_with_perms(obj, attach_perms=True) | ||
| serializer = self.get_serializer( | ||
| users_with_perms.keys(), many=True, context=serializer_context | ||
| ) | ||
| return Response(serializer.data, status=status.HTTP_200_OK) | ||
|
|
||
| # POST or DELETE | ||
| serializer = self.get_serializer(data=request.data, context=serializer_context) | ||
| if not serializer.is_valid(): | ||
| return Response({"errors": serializer.errors}, status=status.HTTP_400_BAD_REQUEST) | ||
|
|
||
| user = serializer.validated_data["user"] | ||
| perms = serializer.validated_data["permissions"] | ||
|
|
||
| if request.method == "POST": | ||
| errors = [] | ||
| for perm in perms: | ||
| try: | ||
| assign_perm(perm, user, obj) | ||
| except ObjectDoesNotExist as e: | ||
| errors.append(f"Cannot assign permission '{perm}': {str(e)}") | ||
|
|
||
| if errors: | ||
| return Response({"errors": errors}, status=status.HTTP_400_BAD_REQUEST) | ||
Check warningCode scanning / CodeQL Information exposure through an exception Medium Stack trace information Error loading related location Loading |
||
|
|
||
| return Response({"status": "permissions assigned"}, status=status.HTTP_200_OK) | ||
|
|
||
| if request.method == "DELETE": | ||
| errors = [] | ||
| for perm in perms: | ||
| try: | ||
| remove_perm(perm, user, obj) | ||
| except ObjectDoesNotExist as e: | ||
| errors.append(f"Cannot remove permission '{perm}': {str(e)}") | ||
|
|
||
| if errors: | ||
| return Response({"errors": errors}, status=status.HTTP_400_BAD_REQUEST) | ||
Check warningCode scanning / CodeQL Information exposure through an exception Medium Stack trace information Error loading related location Loading |
||
|
github-advanced-security[bot] marked this conversation as resolved.
Fixed
|
||
|
|
||
| return Response({"status": "permissions removed"}, status=status.HTTP_200_OK) | ||
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
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.