Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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: 3 additions & 0 deletions dejacode/static/css/dejacode_bootstrap.css
Original file line number Diff line number Diff line change
Expand Up @@ -717,6 +717,9 @@ body.signup-form .nav-link.active {
body.signup-form label {
display: none;
}
body.signup-form label.altcha-label {
display: initial !important;
}
body.signup-form #div_id_updates_email_notification label {
display: block;
margin-bottom: 0!important;
Expand Down
166 changes: 166 additions & 0 deletions dje/api_permissions.py
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)

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)

return Response({"status": "permissions removed"}, status=status.HTTP_200_OK)
3 changes: 2 additions & 1 deletion dje/registration.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,8 +88,9 @@ class DejaCodeRegistrationForm(RegistrationFormUniqueEmail):

use_required_attribute = True
captcha = AltchaField(
floating=True,
floating="auto",
hidefooter=True,
hidelogo=True,
)

class Meta(RegistrationFormUniqueEmail.Meta):
Expand Down
2 changes: 2 additions & 0 deletions product_portfolio/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
from dje.api import NameVersionHyperlinkedRelatedField
from dje.api import ProductRelatedViewSet
from dje.api import SPDXDocumentActionMixin
from dje.api_permissions import ObjectPermissionsMixin
from dje.filters import LastModifiedDateFilter
from dje.filters import MultipleCharFilter
from dje.filters import MultipleUUIDFilter
Expand Down Expand Up @@ -311,6 +312,7 @@ class Meta:


class ProductViewSet(
ObjectPermissionsMixin,
SendAboutFilesMixin,
AboutCodeFilesActionMixin,
SPDXDocumentActionMixin,
Expand Down
8 changes: 4 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ dependencies = [
# Base configuration tools
"setuptools==80.9.0",
"wheel==0.45.1",
"pip==25.1.1",
"pip==25.3",
# Django
"Django==5.2.7",
"asgiref==3.10.0",
Expand All @@ -51,7 +51,7 @@ dependencies = [
"django-debug-toolbar==6.0.0",
# CAPTCHA
"altcha==0.2.0",
"django_altcha==0.3.0",
"django_altcha==0.4.0",
# REST API
"djangorestframework==3.16.1",
# API documentation
Expand Down Expand Up @@ -98,14 +98,14 @@ dependencies = [
"six==1.17.0",
"requests==2.32.5",
"idna==3.11",
"charset-normalizer==3.4.3",
"charset-normalizer==3.4.4",
"PyYAML==6.0.2",
"cython==3.1.1",
"zipp==3.23.0",
"XlsxWriter==3.2.9",
# Markdown
"markdown==3.9",
"bleach==6.2.0",
"bleach==6.3.0",
"bleach_allowlist==1.0.3",
"webencodings==0.5.1",
# Authentication
Expand Down
18 changes: 0 additions & 18 deletions thirdparty/dist/bleach-6.2.0-py3-none-any.whl.ABOUT

This file was deleted.

Binary file not shown.
18 changes: 18 additions & 0 deletions thirdparty/dist/bleach-6.3.0-py3-none-any.whl.ABOUT
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
about_resource: bleach-6.3.0-py3-none-any.whl
name: bleach
version: 6.3.0
download_url: https://files.pythonhosted.org/packages/cd/3a/577b549de0cc09d95f11087ee63c739bba856cd3952697eec4c4bb91350a/bleach-6.3.0-py3-none-any.whl
package_url: pkg:pypi/bleach@6.3.0
license_expression: apache-2.0 AND mit
copyright: Copyright bleach project contributors
attribute: yes
track_changes: yes
checksum_md5: 582f05dac01de36bf93c8e05b9cba11b
checksum_sha1: 74792becf1c32fb1edd3e04594acaee490969170
licenses:
- key: apache-2.0
name: Apache License 2.0
file: apache-2.0.LICENSE
- key: mit
name: MIT License
file: mit.LICENSE
14 changes: 0 additions & 14 deletions thirdparty/dist/charset_normalizer-3.4.3-py3-none-any.whl.ABOUT

This file was deleted.

Binary file not shown.
14 changes: 14 additions & 0 deletions thirdparty/dist/charset_normalizer-3.4.4-py3-none-any.whl.ABOUT
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
about_resource: charset_normalizer-3.4.4-py3-none-any.whl
name: charset-normalizer
version: 3.4.4
download_url: https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl
package_url: pkg:pypi/charset-normalizer@3.4.4
license_expression: mit
copyright: Copyright Ahmed TAHRI @Ousret, Denny Vrandecic, TAHRI Ahmed R.
attribute: yes
checksum_md5: a425e0aabe24dde0df1bc1eaca464c5b
checksum_sha1: f8e205046cfeb85815f79705565c73be32e5c193
licenses:
- key: mit
name: MIT License
file: mit.LICENSE
Binary file removed thirdparty/dist/django_altcha-0.3.0-py3-none-any.whl
Binary file not shown.
17 changes: 0 additions & 17 deletions thirdparty/dist/django_altcha-0.3.0-py3-none-any.whl.ABOUT

This file was deleted.

Binary file not shown.
17 changes: 17 additions & 0 deletions thirdparty/dist/django_altcha-0.4.0-py3-none-any.whl.ABOUT
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
about_resource: django_altcha-0.4.0-py3-none-any.whl
name: django-altcha
version: 0.4.0
download_url: https://files.pythonhosted.org/packages/c4/d7/e5cae136502860f25628532614cb81f4982ef65101e497ce774a8a70c803/django_altcha-0.4.0-py3-none-any.whl
package_url: pkg:pypi/django-altcha@0.4.0
license_expression: mit AND unknown-license-reference
copyright: Copyright Daniel Regeci
attribute: yes
checksum_md5: 6fa8832efc1f5f036e0e5fee602cf3b4
checksum_sha1: ac52db4601c7c828deaeb15b9ece68b7aaf51794
licenses:
- key: mit
name: MIT License
file: mit.LICENSE
- key: unknown-license-reference
name: Unknown License file reference
file: unknown-license-reference.LICENSE
14 changes: 0 additions & 14 deletions thirdparty/dist/pip-25.1.1-py3-none-any.whl.ABOUT

This file was deleted.

Binary file not shown.
21 changes: 21 additions & 0 deletions thirdparty/dist/pip-25.3-py3-none-any.whl.ABOUT
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
about_resource: pip-25.3-py3-none-any.whl
name: pip
version: '25.3'
download_url: https://files.pythonhosted.org/packages/44/3c/d717024885424591d5376220b5e836c2d5293ce2011523c9de23ff7bf068/pip-25.3-py3-none-any.whl
package_url: pkg:pypi/pip@25.3
license_expression: apache-2.0 AND bsd-new AND unknown-license-reference
copyright: Copyright pip project contributors
attribute: yes
track_changes: yes
checksum_md5: 4bbbf9f0745c4117c8ecc77c561ef74b
checksum_sha1: 8de392fcd36b5622be9a2b16e07117158c377533
licenses:
- key: apache-2.0
name: Apache License 2.0
file: apache-2.0.LICENSE
- key: bsd-new
name: BSD-3-Clause
file: bsd-new.LICENSE
- key: unknown-license-reference
name: Unknown License file reference
file: unknown-license-reference.LICENSE