Skip to content
Draft
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
193 changes: 8 additions & 185 deletions dojo/api_v2/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
import tagulous
from django.conf import settings
from django.contrib.auth.models import Permission
from django.contrib.auth.password_validation import validate_password
from django.core.exceptions import PermissionDenied, ValidationError
from django.db import transaction
from django.db.utils import IntegrityError
Expand Down Expand Up @@ -36,7 +35,6 @@
Announcement,
App_Analysis,
Development_Environment,
Dojo_User,
DojoMeta,
Endpoint,
Endpoint_Params,
Expand All @@ -58,13 +56,11 @@
SLA_Configuration,
Sonarqube_Issue,
Sonarqube_Issue_Transition,
System_Settings,
Test,
Tool_Configuration,
Tool_Product_Settings,
Tool_Type,
User,
UserContactInfo,
get_current_date,
)
from dojo.product_announcements import (
Expand All @@ -76,7 +72,6 @@
requires_file,
requires_tool_type,
)
from dojo.user.utils import get_configuration_permissions_codenames
from dojo.utils import is_scan_file_too_large
from dojo.validators import ImporterFileExtensionValidator, tag_validator

Expand Down Expand Up @@ -303,177 +298,13 @@ def validate(self, data):
return data


class UserSerializer(serializers.ModelSerializer):
date_joined = serializers.DateTimeField(read_only=True)
last_login = serializers.DateTimeField(read_only=True, allow_null=True)
email = serializers.EmailField(required=True)
token_last_reset = serializers.SerializerMethodField()
password_last_reset = serializers.SerializerMethodField()
password = serializers.CharField(
write_only=True,
style={"input_type": "password"},
required=False,
validators=[validate_password],
)
configuration_permissions = serializers.PrimaryKeyRelatedField(
allow_null=True,
queryset=Permission.objects.filter(
codename__in=get_configuration_permissions_codenames(),
),
many=True,
required=False,
source="user_permissions",
)

class Meta:
model = Dojo_User
fields = (
"id",
"username",
"first_name",
"last_name",
"email",
"date_joined",
"last_login",
"is_active",
"is_staff",
"is_superuser",
"token_last_reset",
"password_last_reset",
"password",
"configuration_permissions",
)

@extend_schema_field(serializers.DateTimeField(allow_null=True))
def get_token_last_reset(self, instance):
uci = getattr(instance, "usercontactinfo", None)
return getattr(uci, "token_last_reset", None)

@extend_schema_field(serializers.DateTimeField(allow_null=True))
def get_password_last_reset(self, instance):
uci = getattr(instance, "usercontactinfo", None)
return getattr(uci, "password_last_reset", None)

def to_representation(self, instance):
ret = super().to_representation(instance)

# This will show only "configuration_permissions" even if user has also
# other permissions
all_permissions = set(ret["configuration_permissions"])
allowed_configuration_permissions = set(
self.fields[
"configuration_permissions"
].child_relation.queryset.values_list("id", flat=True),
)
ret["configuration_permissions"] = list(
all_permissions.intersection(allowed_configuration_permissions),
)

return ret

def update(self, instance, validated_data):
permissions_in_payload = None
new_configuration_permissions = None
if (
"user_permissions" in validated_data
): # This field was renamed from "configuration_permissions" in the meantime
permissions_in_payload = validated_data.pop("user_permissions")
new_configuration_permissions = set(permissions_in_payload)

instance = super().update(instance, validated_data)

# This will update only Permissions from category
# "configuration_permissions". Others will be untouched
if new_configuration_permissions:
allowed_configuration_permissions = set(
self.fields[
"configuration_permissions"
].child_relation.queryset.all(),
)
non_configuration_permissions = (
set(instance.user_permissions.all())
- allowed_configuration_permissions
)
new_permissions = non_configuration_permissions.union(
new_configuration_permissions,
)
instance.user_permissions.set(new_permissions)

# Clear all configuration permissions if an empty list is provided
if isinstance(permissions_in_payload, list) and len(permissions_in_payload) == 0:
instance.user_permissions.clear()

return instance

def create(self, validated_data):
password = validated_data.pop("password", None)

new_configuration_permissions = None
if (
"user_permissions" in validated_data
): # This field was renamed from "configuration_permissions" in the meantime
new_configuration_permissions = set(
validated_data.pop("user_permissions"),
)

user = Dojo_User.objects.create(**validated_data)

if password:
user.set_password(password)
else:
user.set_unusable_password()

# This will create only Permissions from category
# "configuration_permissions". There are no other Permissions.
if new_configuration_permissions:
user.user_permissions.set(new_configuration_permissions)

user.save()
return user

def validate(self, data):
instance_is_superuser = self.instance.is_superuser if self.instance is not None else False
data_is_superuser = data.get("is_superuser", False)
if not self.context["request"].user.is_superuser and (
instance_is_superuser or data_is_superuser
):
msg = "Only superusers are allowed to add or edit superusers."
raise ValidationError(msg)

if self.context["request"].method in {"PATCH", "PUT"} and "password" in data:
msg = "Update of password though API is not allowed"
raise ValidationError(msg)
if self.context["request"].method == "POST" and "password" not in data and settings.REQUIRE_PASSWORD_ON_USER:
msg = "Passwords must be supplied for new users"
raise ValidationError(msg)
return super().validate(data)


class UserContactInfoSerializer(serializers.ModelSerializer):
user_profile = UserSerializer(many=False, source="user", read_only=True)

class Meta:
model = UserContactInfo
fields = "__all__"

def validate(self, data):
user = data.get("user", None) or self.instance.user
if data.get("force_password_reset", False) and not user.has_usable_password():
msg = "Password resets are not allowed for users authorized through SSO."
raise ValidationError(msg)
return super().validate(data)


class UserStubSerializer(serializers.ModelSerializer):
class Meta:
model = Dojo_User
fields = ("id", "username", "first_name", "last_name")


class AddUserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ("id", "username")
from dojo.user.api.serializer import ( # noqa: E402, F401 -- backward compat + prefetcher discovery
AddUserSerializer,
UserContactInfoSerializer,
UserProfileSerializer,
UserSerializer,
UserStubSerializer,
)


class NoteTypeSerializer(serializers.ModelSerializer):
Expand Down Expand Up @@ -1627,10 +1458,7 @@ class TagSerializer(serializers.Serializer):
tags = TagListSerializerField(required=True)


class SystemSettingsSerializer(serializers.ModelSerializer):
class Meta:
model = System_Settings
fields = "__all__"
from dojo.system_settings.api.serializer import SystemSettingsSerializer # noqa: E402, F401 -- backward compat


class CeleryStatusSerializer(serializers.Serializer):
Expand Down Expand Up @@ -1681,11 +1509,6 @@ def validate(self, data):
return data


class UserProfileSerializer(serializers.Serializer):
user = UserSerializer(many=False)
user_contact_info = UserContactInfoSerializer(many=False, required=False)


class DeletePreviewSerializer(serializers.Serializer):
model = serializers.CharField(read_only=True)
id = serializers.IntegerField(read_only=True, allow_null=True)
Expand Down
97 changes: 0 additions & 97 deletions dojo/api_v2/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
from pathlib import Path

import pghistory
from crum import get_current_user
from dateutil.relativedelta import relativedelta
from django.conf import settings
from django.contrib.auth.models import Permission
Expand All @@ -27,7 +26,6 @@
from drf_spectacular.views import SpectacularAPIView
from rest_framework import mixins, status, viewsets
from rest_framework.decorators import action
from rest_framework.generics import GenericAPIView
from rest_framework.parsers import MultiPartParser
from rest_framework.permissions import DjangoModelPermissions, IsAuthenticated
from rest_framework.response import Response
Expand All @@ -51,7 +49,6 @@
ApiDojoMetaFilter,
ApiEndpointFilter,
ApiRiskAcceptanceFilter,
ApiUserFilter,
)
from dojo.finding.ui.filters import (
ReportFindingFilter,
Expand Down Expand Up @@ -86,8 +83,6 @@
Tool_Configuration,
Tool_Product_Settings,
Tool_Type,
User,
UserContactInfo,
)
from dojo.product.queries import (
get_authorized_app_analysis,
Expand All @@ -104,7 +99,6 @@
from dojo.risk_acceptance.queries import get_authorized_risk_acceptances
from dojo.test.queries import get_authorized_tests
from dojo.tool_product.queries import get_authorized_tool_product_settings
from dojo.user.authentication import reset_token_for_user
from dojo.user.utils import get_configuration_permissions_codenames
from dojo.utils import (
get_celery_queue_details,
Expand Down Expand Up @@ -654,82 +648,6 @@ def get_queryset(self):
return Regulation.objects.all().order_by("id")


# Authorization: configuration
class UsersViewSet(
DojoModelViewSet,
):
serializer_class = serializers.UserSerializer
queryset = User.objects.none()
filter_backends = (DjangoFilterBackend,)
filterset_class = ApiUserFilter
permission_classes = (permissions.UserHasConfigurationPermissionSuperuser,)

def get_queryset(self):
return User.objects.all().order_by("id")

def destroy(self, request, *args, **kwargs):
instance = self.get_object()
if request.user == instance:
return Response(
"Users may not delete themselves",
status=status.HTTP_400_BAD_REQUEST,
)
self.perform_destroy(instance)
return Response(status=status.HTTP_204_NO_CONTENT)

@action(
detail=True,
methods=["post"],
url_path="reset_api_token",
permission_classes=(IsAuthenticated, permissions.IsSuperUserOrGlobalOwner),
filter_backends=[],
pagination_class=None,
)
def reset_api_token(self, request, pk=None):
target_user = self.get_object()
reset_token_for_user(acting_user=request.user, target_user=target_user)
return Response(status=status.HTTP_204_NO_CONTENT)


# Authorization: superuser
@extend_schema_view(**schema_with_prefetch())
class UserContactInfoViewSet(
PrefetchDojoModelViewSet,
):
serializer_class = serializers.UserContactInfoSerializer
queryset = UserContactInfo.objects.none()
filter_backends = (DjangoFilterBackend,)
filterset_fields = "__all__"
permission_classes = (permissions.IsSuperUser, DjangoModelPermissions)

def get_queryset(self):
return UserContactInfo.objects.all().order_by("id")


# Authorization: authenticated users
class UserProfileView(GenericAPIView):
permission_classes = (IsAuthenticated,)
pagination_class = None
serializer_class = serializers.UserProfileSerializer

@action(
detail=True, methods=["get"], filter_backends=[], pagination_class=None,
)
def get(self, request, _=None):
user = get_current_user()
user_contact_info = (
user.usercontactinfo if hasattr(user, "usercontactinfo") else None
)
serializer = serializers.UserProfileSerializer(
{
"user": user,
"user_contact_info": user_contact_info,
},
many=False,
)
return Response(serializer.data)


# Authorization: authenticated users, DjangoModelPermissions
class ImportScanView(mixins.CreateModelMixin, viewsets.GenericViewSet):

Expand Down Expand Up @@ -1270,21 +1188,6 @@ def report_generate(request, obj, options):
return result


# Authorization: superuser
class SystemSettingsViewSet(
mixins.ListModelMixin, mixins.UpdateModelMixin, viewsets.GenericViewSet,
):

"""Basic control over System Settings. Use 'id' 1 for PUT, PATCH operations"""

permission_classes = (permissions.IsSuperUser, DjangoModelPermissions)
serializer_class = serializers.SystemSettingsSerializer
queryset = System_Settings.objects.none()

def get_queryset(self):
return System_Settings.objects.all().order_by("id")


class CeleryViewSet(viewsets.ViewSet):
permission_classes = (permissions.IsSuperUser, DjangoModelPermissions)
queryset = System_Settings.objects.none()
Expand Down
Loading