diff --git a/dejacode/static/css/dejacode_bootstrap.css b/dejacode/static/css/dejacode_bootstrap.css index 480ba4ce..b242c496 100644 --- a/dejacode/static/css/dejacode_bootstrap.css +++ b/dejacode/static/css/dejacode_bootstrap.css @@ -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; diff --git a/dje/api_permissions.py b/dje/api_permissions.py new file mode 100644 index 00000000..0626a978 --- /dev/null +++ b/dje/api_permissions.py @@ -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) diff --git a/dje/registration.py b/dje/registration.py index 868efa8f..e2a16c6d 100644 --- a/dje/registration.py +++ b/dje/registration.py @@ -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): diff --git a/product_portfolio/api.py b/product_portfolio/api.py index 9f6fa0af..a25d78ad 100644 --- a/product_portfolio/api.py +++ b/product_portfolio/api.py @@ -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 @@ -311,6 +312,7 @@ class Meta: class ProductViewSet( + ObjectPermissionsMixin, SendAboutFilesMixin, AboutCodeFilesActionMixin, SPDXDocumentActionMixin, diff --git a/pyproject.toml b/pyproject.toml index 4acdede1..3a803c45 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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", @@ -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 @@ -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 diff --git a/thirdparty/dist/bleach-6.2.0-py3-none-any.whl.ABOUT b/thirdparty/dist/bleach-6.2.0-py3-none-any.whl.ABOUT deleted file mode 100644 index bf3c6b62..00000000 --- a/thirdparty/dist/bleach-6.2.0-py3-none-any.whl.ABOUT +++ /dev/null @@ -1,18 +0,0 @@ -about_resource: bleach-6.2.0-py3-none-any.whl -name: bleach -version: 6.2.0 -download_url: https://files.pythonhosted.org/packages/fc/55/96142937f66150805c25c4d0f31ee4132fd33497753400734f9dfdcbdc66/bleach-6.2.0-py3-none-any.whl -package_url: pkg:pypi/bleach@6.2.0 -license_expression: apache-2.0 AND mit -copyright: Copyright bleach project contributors -attribute: yes -track_changes: yes -checksum_md5: 660e2e515dcab76aaba61c1ef9dac291 -checksum_sha1: 413fc1f05fd745e698b8b93068d3ba3760574e19 -licenses: - - key: apache-2.0 - name: Apache License 2.0 - file: apache-2.0.LICENSE - - key: mit - name: MIT License - file: mit.LICENSE diff --git a/thirdparty/dist/bleach-6.2.0-py3-none-any.whl b/thirdparty/dist/bleach-6.3.0-py3-none-any.whl similarity index 70% rename from thirdparty/dist/bleach-6.2.0-py3-none-any.whl rename to thirdparty/dist/bleach-6.3.0-py3-none-any.whl index bd65dc29..6dd8ebad 100644 Binary files a/thirdparty/dist/bleach-6.2.0-py3-none-any.whl and b/thirdparty/dist/bleach-6.3.0-py3-none-any.whl differ diff --git a/thirdparty/dist/bleach-6.3.0-py3-none-any.whl.ABOUT b/thirdparty/dist/bleach-6.3.0-py3-none-any.whl.ABOUT new file mode 100644 index 00000000..7814d78e --- /dev/null +++ b/thirdparty/dist/bleach-6.3.0-py3-none-any.whl.ABOUT @@ -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 diff --git a/thirdparty/dist/charset_normalizer-3.4.3-py3-none-any.whl.ABOUT b/thirdparty/dist/charset_normalizer-3.4.3-py3-none-any.whl.ABOUT deleted file mode 100644 index decbc026..00000000 --- a/thirdparty/dist/charset_normalizer-3.4.3-py3-none-any.whl.ABOUT +++ /dev/null @@ -1,14 +0,0 @@ -about_resource: charset_normalizer-3.4.3-py3-none-any.whl -name: charset-normalizer -version: 3.4.3 -download_url: https://files.pythonhosted.org/packages/8a/1f/f041989e93b001bc4e44bb1669ccdcf54d3f00e628229a85b08d330615c5/charset_normalizer-3.4.3-py3-none-any.whl -package_url: pkg:pypi/charset-normalizer@3.4.3 -license_expression: mit -copyright: Copyright Ahmed TAHRI @Ousret, Denny Vrandecic, TAHRI Ahmed R. -attribute: yes -checksum_md5: 4a43811bb5747201dc3694e76763e446 -checksum_sha1: 562c6324f09dae2623f6745989e7d077f53c3bd3 -licenses: - - key: mit - name: MIT License - file: mit.LICENSE diff --git a/thirdparty/dist/charset_normalizer-3.4.3-py3-none-any.whl b/thirdparty/dist/charset_normalizer-3.4.4-py3-none-any.whl similarity index 69% rename from thirdparty/dist/charset_normalizer-3.4.3-py3-none-any.whl rename to thirdparty/dist/charset_normalizer-3.4.4-py3-none-any.whl index 813bdf9e..13507564 100644 Binary files a/thirdparty/dist/charset_normalizer-3.4.3-py3-none-any.whl and b/thirdparty/dist/charset_normalizer-3.4.4-py3-none-any.whl differ diff --git a/thirdparty/dist/charset_normalizer-3.4.4-py3-none-any.whl.ABOUT b/thirdparty/dist/charset_normalizer-3.4.4-py3-none-any.whl.ABOUT new file mode 100644 index 00000000..f0c4892d --- /dev/null +++ b/thirdparty/dist/charset_normalizer-3.4.4-py3-none-any.whl.ABOUT @@ -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 diff --git a/thirdparty/dist/django_altcha-0.3.0-py3-none-any.whl b/thirdparty/dist/django_altcha-0.3.0-py3-none-any.whl deleted file mode 100644 index d7766a92..00000000 Binary files a/thirdparty/dist/django_altcha-0.3.0-py3-none-any.whl and /dev/null differ diff --git a/thirdparty/dist/django_altcha-0.3.0-py3-none-any.whl.ABOUT b/thirdparty/dist/django_altcha-0.3.0-py3-none-any.whl.ABOUT deleted file mode 100644 index 980e91de..00000000 --- a/thirdparty/dist/django_altcha-0.3.0-py3-none-any.whl.ABOUT +++ /dev/null @@ -1,17 +0,0 @@ -about_resource: django_altcha-0.3.0-py3-none-any.whl -name: django-altcha -version: 0.3.0 -download_url: https://files.pythonhosted.org/packages/9e/6e/ad184a95de2652f5834243f5b3403c8949993f5f3de08a5609b15a63b091/django_altcha-0.3.0-py3-none-any.whl -package_url: pkg:pypi/django-altcha@0.3.0 -license_expression: mit AND unknown-license-reference -copyright: Copyright Daniel Regeci -attribute: yes -checksum_md5: 8144c699aa87c898862d844341c7adb1 -checksum_sha1: dc853e0283e7992aba12567d07129953cf32fa1b -licenses: - - key: mit - name: MIT License - file: mit.LICENSE - - key: unknown-license-reference - name: Unknown License file reference - file: unknown-license-reference.LICENSE diff --git a/thirdparty/dist/django_altcha-0.4.0-py3-none-any.whl b/thirdparty/dist/django_altcha-0.4.0-py3-none-any.whl new file mode 100644 index 00000000..6e0512c5 Binary files /dev/null and b/thirdparty/dist/django_altcha-0.4.0-py3-none-any.whl differ diff --git a/thirdparty/dist/django_altcha-0.4.0-py3-none-any.whl.ABOUT b/thirdparty/dist/django_altcha-0.4.0-py3-none-any.whl.ABOUT new file mode 100644 index 00000000..4ec6024c --- /dev/null +++ b/thirdparty/dist/django_altcha-0.4.0-py3-none-any.whl.ABOUT @@ -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 diff --git a/thirdparty/dist/pip-25.1.1-py3-none-any.whl.ABOUT b/thirdparty/dist/pip-25.1.1-py3-none-any.whl.ABOUT deleted file mode 100644 index 241644a5..00000000 --- a/thirdparty/dist/pip-25.1.1-py3-none-any.whl.ABOUT +++ /dev/null @@ -1,14 +0,0 @@ -about_resource: pip-25.1.1-py3-none-any.whl -name: pip -version: 25.1.1 -download_url: https://files.pythonhosted.org/packages/29/a2/d40fb2460e883eca5199c62cfc2463fd261f760556ae6290f88488c362c0/pip-25.1.1-py3-none-any.whl -package_url: pkg:pypi/pip@25.1.1 -license_expression: mit -copyright: Copyright pip project contributors -attribute: yes -checksum_md5: 6a01d861bf88ee075c6942419a04839d -checksum_sha1: 30c8926cbf1c22cb4b692e0f80d7f86c7bc38cdb -licenses: - - key: mit - name: MIT License - file: mit.LICENSE diff --git a/thirdparty/dist/pip-25.1.1-py3-none-any.whl b/thirdparty/dist/pip-25.3-py3-none-any.whl similarity index 57% rename from thirdparty/dist/pip-25.1.1-py3-none-any.whl rename to thirdparty/dist/pip-25.3-py3-none-any.whl index 2fdcfbf9..755e1aa0 100644 Binary files a/thirdparty/dist/pip-25.1.1-py3-none-any.whl and b/thirdparty/dist/pip-25.3-py3-none-any.whl differ diff --git a/thirdparty/dist/pip-25.3-py3-none-any.whl.ABOUT b/thirdparty/dist/pip-25.3-py3-none-any.whl.ABOUT new file mode 100644 index 00000000..3c7cf176 --- /dev/null +++ b/thirdparty/dist/pip-25.3-py3-none-any.whl.ABOUT @@ -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