Beispiel #1
0
def test_inline_serializer(no_warnings):
    @extend_schema(responses=inline_serializer(
        name='InlineOneOffSerializer',
        fields={
            'char':
            serializers.CharField(),
            'choice':
            serializers.ChoiceField(choices=(('A', 'A'), ('B', 'B'))),
            'nested_inline':
            inline_serializer(
                name='NestedInlineOneOffSerializer',
                fields={
                    'char': serializers.CharField(),
                    'int': serializers.IntegerField(),
                },
                allow_null=True,
            )
        }))
    @api_view(['GET'])
    def one_off(request, foo):
        pass  # pragma: no cover

    schema = generate_schema('x', view_function=one_off)
    assert get_response_schema(schema['paths']['/x']['get'])['$ref'] == (
        '#/components/schemas/InlineOneOff')
    assert len(schema['components']['schemas']) == 3

    one_off = schema['components']['schemas']['InlineOneOff']
    one_off_nested = schema['components']['schemas']['NestedInlineOneOff']

    assert len(one_off['properties']) == 3
    assert one_off['properties']['nested_inline']['nullable'] is True
    assert one_off['properties']['nested_inline']['allOf'][0]['$ref'] == (
        '#/components/schemas/NestedInlineOneOff')
    assert len(one_off_nested['properties']) == 2
Beispiel #2
0
class OrganizationMemberRequestViewSet(mixins.DestroyModelMixin,
                                       viewsets.ReadOnlyModelViewSet):
    serializer_class = OrganizationMemberRequestSerializer
    permission_classes = [permissions.IsAuthenticated]
    filterset_class = OrganizationMemberRequestFilter

    def get_queryset(self):
        user = self.request.user
        return (OrganizationMemberRequest.objects.filter(
            Q(organization__admins=user) | Q(user=user)).prefetch_related(
                "organization__admins").distinct())

    @extend_schema(responses=inline_serializer(
        name="OrganizationMemberRequestAcceptResponseSerializer",
        fields={
            "detail":
            serializers.CharField(
                default=_("Member request successfully accepted"))
        },
    ))
    @action(
        methods=["post"],
        detail=True,
        serializer_class=serializers.Serializer,
        permission_classes=[IsMemberRequestOrganizationAdmin],
    )
    def accept(self, request, *args, **kwargs):
        member_request = self.get_object()
        if member_request.status != "accepted":
            member_request.updated_by = request.user
            member_request.status = "accepted"
            member_request.save()
        return Response({"detail": _("Member request successfully accepted")})

    @extend_schema(responses=inline_serializer(
        name="OrganizationMemberRequestRejectResponseSerializer",
        fields={
            "detail":
            serializers.CharField(
                default=_("Member request successfully rejected"))
        },
    ))
    @action(
        methods=["post"],
        detail=True,
        serializer_class=serializers.Serializer,
        permission_classes=[IsMemberRequestOrganizationAdmin],
    )
    def reject(self, request, *args, **kwargs):
        member_request = self.get_object()
        if member_request.status != "rejected":
            member_request.updated_by = request.user
            member_request.status = "rejected"
            member_request.save()
        return Response({"detail": _("Member request successfully rejected")})
 def map_serializer(self, auto_schema, direction):
     Fixed = inline_serializer('Fixed', fields={
         self.target_class.username_field: serializers.CharField(write_only=True),
         'password': serializers.CharField(write_only=True),
         'token': serializers.CharField(read_only=True),
     })
     return auto_schema._map_serializer(Fixed, direction)
 def map_serializer(self, auto_schema, direction):
     Fixed = inline_serializer('Fixed',
                               fields={
                                   'token':
                                   serializers.CharField(write_only=True),
                               })
     return auto_schema._map_serializer(Fixed, direction)
Beispiel #5
0
class WorkerView(APIView):
    """Get currently connected worker count."""

    permission_classes = [IsAdminUser]

    @extend_schema(responses=inline_serializer(
        "Workers", fields={"count": IntegerField()}))
    def get(self, request: Request) -> Response:
        """Get currently connected worker count."""
        return Response({"count": len(CELERY_APP.control.ping(timeout=0.5))})
Beispiel #6
0
class WorkerView(APIView):
    """Get currently connected worker count."""

    permission_classes = [IsAdminUser]

    @extend_schema(responses=inline_serializer(
        "Workers", fields={"count": IntegerField()}))
    def get(self, request: Request) -> Response:
        """Get currently connected worker count."""
        count = len(CELERY_APP.control.ping(timeout=0.5))
        # In debug we run with `CELERY_TASK_ALWAYS_EAGER`, so tasks are ran on the main process
        if settings.DEBUG:  # pragma: no cover
            count += 1
        return Response({"count": count})
class SnakeCasedResponse(APIView):
    renderer_classes = [JSONRenderer]

    @extend_schema(
        responses={
            200:
            inline_serializer(
                name="SnakeCaseSerializer",
                many=True,
                fields={"this_is_snake_case": serializers.CharField()})
        })
    @get_snake_cased_response()
    def get(self, request: Request, version: int, **kwargs) -> Response:
        return Response({"this_is_snake_case": "test"}, 200)
Beispiel #8
0
class SurveyResultFeedbackViewSet(
        mixins.RetrieveModelMixin,
        mixins.ListModelMixin,
        viewsets.GenericViewSet,
):
    serializer_class = SurveyResultFeedbackSerializer
    permission_classes = [permissions.IsAuthenticated]
    filterset_class = SurveyResultFeedbackFilter

    def get_queryset(self):
        current_user = self.request.user
        projects = read_allowed_project_for_user(current_user)
        surveys = Survey.objects.filter(
            Q(project__in=projects) | Q(created_by=current_user))
        return SurveyResultFeedback.objects.filter(
            survey_result__survey__in=surveys).select_related(
                "survey_result__survey")

    @extend_schema(
        responses=inline_serializer(
            name="AcknowledgeSurveyResultFeedbackResponseSerializer",
            fields={
                "detail":
                serializers.CharField(default=_(
                    "Successfully acknowledged survey result feedback"))
            },
        ), )
    @action(
        methods=["post"],
        detail=True,
        permission_classes=[CanAcknowledgeSurveyResultFeedback],
        serializer_class=serializers.Serializer,
    )
    def acknowledge(self, request, *args, **kwargs):
        survey_result_feedback = self.get_object()
        survey_result_feedback.status = SurveyResultFeedback.StatusChoice.ACKNOWLEDGED
        survey_result_feedback.updated_by = self.request.user
        survey_result_feedback.save()
        return Response(
            {"detail": _("Successfully acknowledged survey result feedback")},
            status=status.HTTP_200_OK,
        )
Beispiel #9
0
class AnalyzerListAPI(generics.ListAPIView):

    serializer_class = AnalyzerConfigSerializer

    @add_docs(
        description="""
        Get the uploaded analyzer configuration,
        can be useful if you want to choose the analyzers programmatically""",
        parameters=[],
        responses={
            200:
            AnalyzerConfigSerializer,
            500:
            inline_serializer(
                name="GetAnalyzerConfigsFailedResponse",
                fields={"error": BaseSerializer.StringRelatedField()},
            ),
        },
    )
    def get(self, request, *args, **kwargs):
        # @extend_schema needs to be applied to the entrypoint method of the view
        # `list` call is proxied through the entrypoint `get`
        return super().get(request, *args, **kwargs)

    def list(self, request):
        try:
            logger.info(
                f"get_analyzer_configs received request from {str(request.user)}."
            )
            ac = self.serializer_class.read_and_verify_config()
            return Response(ac, status=status.HTTP_200_OK)
        except Exception as e:
            logger.exception(
                f"get_analyzer_configs requester:{str(request.user)} error:{e}."
            )
            return Response(
                {"error": "error in get_analyzer_configs. Check logs."},
                status=status.HTTP_500_INTERNAL_SERVER_ERROR,
            )
Beispiel #10
0
            raise ValidationError(
                {"detail": "Plugin call status should be failed or killed"})

        # retry with the same arguments
        self.perform_retry(report)
        return Response(status=status.HTTP_204_NO_CONTENT)


@add_docs(
    description="Health Check: if instance associated with plugin is up or not",
    request=None,
    responses={
        200:
        inline_serializer(
            name="PluginHealthCheckSuccessResponse",
            fields={
                "status": rfs.NullBooleanField(),
            },
        ),
    },
)
class PluginHealthCheckAPI(APIView, metaclass=ABCMeta):
    @abstractmethod
    def perform_healthcheck(self, plugin_name):
        raise NotImplementedError()

    def get(self, request, name):
        health_status = self.perform_healthcheck(name)
        return Response(data={"status": health_status},
                        status=status.HTTP_200_OK)
Beispiel #11
0
class VideoCommentsViewSetV2(
        mixins.RetrieveModelMixin,
        mixins.ListModelMixin,
        mixins.CreateModelMixin,
        mixins.UpdateModelMixin,
        WithPKOverflowProtection,
        viewsets.GenericViewSet,
):
    """Post, edit, mark comments."""

    UPDATE_DOCSTRING = {
        'list': "List and filter comments",
        'retrieve': "Get one comment",
        'create': "Comment on a video",
        'update': "Change all fields in a comment",
        'partial_update': "Change some fields in a comment"
    }

    KWARGS_DICT = {
        'create': {
            'responses': {
                400: None,
                201: VideoCommentsSerializerV2
            }
        },
        'update': {
            'responses': {
                400: None,
                404: None,
                201: VideoCommentsSerializerV2,
                200: VideoCommentsSerializerV2
            }
        },
        'partial_update': {
            'responses': {
                400: None,
                404: None,
                201: VideoCommentsSerializerV2,
                200: VideoCommentsSerializerV2
            }
        },
        'retrieve': {
            'responses': {
                404: None,
                200: VideoCommentsSerializerV2
            }
        },
        'list': {
            'responses': {
                200: VideoCommentsSerializerV2(many=True),
                400: None
            }
        }
    }

    queryset = VideoComment.objects.all()
    serializer_class = VideoCommentsSerializerV2
    filterset_class = VideoCommentsFilterV2

    def get_queryset(self, pk=None):
        """Adding the proposed metric."""
        queryset = VideoComment.objects.select_related()

        # only selecting comments from certified users, or from me
        queryset = queryset.annotate(n_cert_email=Count(
            'user__user__userinformation__emails',
            filter=Q(user__user__userinformation__emails__domain_fk__status=
                     EmailDomain.STATUS_ACCEPTED,
                     user__user__userinformation__emails__is_verified=True)))
        queryset = queryset.filter(
            Q(user__user__username=self.request.user.username)
            | Q(n_cert_email__gt=0))

        # adding votes
        queryset = queryset.annotate(
            votes_plus_=Count(
                'videocommentmarker_comment',
                filter=Q(videocommentmarker_comment__which="vote_plus")),
            votes_minus_=Count(
                'videocommentmarker_comment',
                filter=Q(videocommentmarker_comment__which="vote_minus")),
            red_flags_=Count(
                'videocommentmarker_comment',
                filter=Q(videocommentmarker_comment__which="red_flag")))

        # computing the metric, see #47
        queryset = queryset.annotate(
            sort_metric=(F('votes_plus_') * 1.1 + 1) /
            (1 + F('votes_plus_') + F('votes_minus_')) -
            VideoComment.red_flag_weight * F('red_flags_'))

        return queryset

    # add/delete actions for markers
    marker_actions = ["add", "delete", "toggle"]

    @extend_schema(
        responses={
            201: VideoCommentsSerializerV2(many=False),
            400: None,
            422: None,
            404: None
        },
        parameters=[
            OpenApiParameter(
                name='marker',
                description=
                f'The marker to set, one of {VideoCommentMarker.MARKER_CHOICES_1}',
                required=True,
                type=str,
                enum=VideoCommentMarker.MARKER_CHOICES_1),
            OpenApiParameter(
                name='action',
                description=
                f'Delete or add the marker, one of {marker_actions}',
                required=True,
                type=str,
                enum=marker_actions),
        ],
        request=inline_serializer(name="Empty", fields={}),
        operation_id="api_v2_video_comments_set_mark")
    @action(methods=['POST'], detail=True, name="Set markers")
    def set_mark(self, request, pk=None):
        """Mark a comment with a flag (like/dislike/red flag)."""
        fields = VideoCommentMarker.MARKER_CHOICES_1

        if request.query_params.get('marker', "") not in fields:
            return Response(
                status=400,
                data={'explanation': f"Marker must be one of {fields}"})
        if request.query_params.get("action", "") not in self.marker_actions:
            return Response(status=400,
                            data={
                                'explanation':
                                f"Action must be one of {self.marker_actions}"
                            })

        f = request.query_params['marker']
        action_ = request.query_params['action']

        f0 = VideoCommentMarker.MARKER_CHOICES_1to0[f]

        if not self.get_queryset().filter(id=pk).count():
            return Response(status=404)

        c = self.get_object()
        marker_user = get_user_preferences(request)

        kwargs = dict(comment=c, which=f0, user=marker_user)

        if action_ == "delete" and not VideoCommentMarker.objects.filter(
                **kwargs).count():
            return Response(
                status=422,
                data={'explanation': "Cannot delete, marker does not exist"})
        if action_ == "add" and VideoCommentMarker.objects.filter(
                **kwargs).count():
            return Response(
                status=422,
                data={'explanation': "Cannot add, marker already exists"})

        if action_ == "add":
            VideoCommentMarker.objects.create(**kwargs).save()
        elif action_ == "delete":
            VideoCommentMarker.objects.filter(**kwargs).delete()
        elif action_ == "toggle":
            n_now = VideoCommentMarker.objects.filter(**kwargs).count()
            if n_now:
                VideoCommentMarker.objects.filter(**kwargs).delete()
            else:
                VideoCommentMarker.objects.create(**kwargs).save()

        return Response(self.get_serializer(c, many=False).data, status=201)

    def filter_queryset(self, queryset):
        queryset = super(VideoCommentsViewSetV2,
                         self).filter_queryset(queryset)
        return queryset.order_by('-sort_metric')

    def perform_create(self, serializer):
        serializer.save(user=get_user_preferences(self.request))

    def perform_update(self, serializer):
        serializer.save(user=get_user_preferences(self.request))
Beispiel #12
0
class VideoRateLaterViewSetV2(mixins.CreateModelMixin,
                              mixins.ListModelMixin,
                              mixins.RetrieveModelMixin,
                              mixins.DestroyModelMixin,
                              WithPKOverflowProtection,
                              viewsets.GenericViewSet, ):
    """Get/set rate the personal later list."""

    UPDATE_DOCSTRING = {
        'list': "Get videos queued to be rated later",
        'retrieve': "Get one video to be rated later (by object ID)",
        'create': "Schedule a video to be rated later",
        'destroy': "Remove a video from rate later list"}

    KWARGS_DICT = {
        'create': {
            'responses': {
                400: None, 201: VideoRateLaterSerializerV2}}, 'retrieve': {
            'responses': {
                404: None, 200: VideoRateLaterSerializerV2}}, 'list': {
            'responses': {
                200: VideoRateLaterSerializerV2(many=True),
                400: None, 404: None}}, 'destroy': {
            'responses': {
                204: None, 404: None, 400: None}}}

    serializer_class = VideoRateLaterSerializerV2
    permission_classes = [IsAuthenticated]
    filterset_class = VideoRateLaterFilterV2

    def get_queryset(self, pk=None):
        """Only my rate later objects."""
        qs = VideoRateLater.objects.all()

        qs = qs.filter(user__user__username=self.request.user.username)

        return qs

    @extend_schema(
        request=inline_serializer("VideoRateLaterDelete",
                                  fields={'video_id': serializers.CharField(
                                      help_text="Video id")},
                                  many=True),
        responses={200: None,
                   400: None})
    @action(methods=['PATCH'], detail=False, name="Bulk delete videos by IDs")
    def bulk_delete(self, request):
        """Delete many videos from the list by IDs."""
        video_ids = []

        if not isinstance(request.data, list):
            return Response({'detail': f"Request is not a list: {str(request.data)}"}, status=400)

        for entry in request.data:
            if isinstance(entry, str):
                video_ids.append(entry)
            elif isinstance(entry, dict):
                if 'video_id' in entry:
                    video_ids.append(entry['video_id'])
                else:
                    return Response({'detail': "Dictionary request, but no video_id field"},
                                    status=400)
            else:
                return Response({'detail': "Unknown request"}, status=400)

        deleted, _ = VideoRateLater.objects.filter(user__user__username=request.user.username,
                                                   video__video_id__in=video_ids).delete()

        return Response({'received': len(video_ids),
                         'deleted': deleted}, status=200)
Beispiel #13
0
class RolesMixin:
    @extend_schema(
        description="List roles assigned to this object.",
        responses={
            200: inline_serializer(
                name="ObjectRolesSerializer",
                fields={"roles": ListField(child=NestedRoleSerializer())},
            )
        },
    )
    @action(detail=True, methods=["get"])
    def list_roles(self, request, pk):
        obj = self.get_object()
        obj_type = ContentType.objects.get_for_model(obj)
        user_qs = UserRole.objects.filter(
            content_type_id=obj_type.id, object_id=obj.pk
        ).select_related("user", "role")
        group_qs = GroupRole.objects.filter(
            content_type_id=obj_type.id, object_id=obj.pk
        ).select_related("group", "role")
        roles = {}
        for user_role in user_qs:
            if user_role.role.name not in roles:
                roles[user_role.role.name] = {
                    "role": user_role.role.name,
                    "users": [],
                    "groups": [],
                }
            roles[user_role.role.name]["users"].append(user_role.user.username)
        for group_role in group_qs:
            if group_role.role.name not in roles:
                roles[group_role.role.name] = {
                    "role": group_role.role.name,
                    "users": [],
                    "groups": [],
                }
            roles[group_role.role.name]["groups"].append(group_role.group.name)
        result = {"roles": list(roles.values())}
        return Response(result)

    @extend_schema(
        description="Add a role for this object to users/groups.",
        responses={201: NestedRoleSerializer},
    )
    @action(detail=True, methods=["post"], serializer_class=NestedRoleSerializer)
    def add_role(self, request, pk):
        obj = self.get_object()
        serializer = NestedRoleSerializer(
            data=request.data, context={"request": request, "content_object": obj, "assign": True}
        )
        serializer.is_valid(raise_exception=True)
        with transaction.atomic():
            if serializer.validated_data["users"]:
                UserRole.objects.bulk_create(
                    [
                        UserRole(
                            content_object=obj,
                            user=user,
                            role=serializer.validated_data["role"],
                        )
                        for user in serializer.validated_data["users"]
                    ]
                )
            if serializer.validated_data["groups"]:
                GroupRole.objects.bulk_create(
                    [
                        GroupRole(
                            content_object=obj,
                            group=group,
                            role=serializer.validated_data["role"],
                        )
                        for group in serializer.validated_data["groups"]
                    ]
                )
        return Response(serializer.data, status=201)

    @extend_schema(
        description="Remove a role for this object from users/groups.",
        responses={201: NestedRoleSerializer},
    )
    @action(detail=True, methods=["post"], serializer_class=NestedRoleSerializer)
    def remove_role(self, request, pk):
        obj = self.get_object()
        serializer = NestedRoleSerializer(
            data=request.data, context={"request": request, "content_object": obj, "assign": False}
        )
        serializer.is_valid(raise_exception=True)
        with transaction.atomic():
            UserRole.objects.filter(pk__in=serializer.user_role_pks).delete()
            GroupRole.objects.filter(pk__in=serializer.group_role_pks).delete()
        return Response(serializer.data, status=201)

    @extend_schema(
        description="List permissions available to the current user on this object.",
        responses={
            200: inline_serializer(
                name="MyPermissionsSerializer", fields={"permissions": ListField(child=CharField())}
            )
        },
    )
    @action(detail=True, methods=["get"])
    def my_permissions(self, request, pk=None):
        obj = self.get_object()
        app_label = obj._meta.app_label
        permissions = [
            ".".join((app_label, codename)) for codename in request.user.get_all_permissions(obj)
        ]
        return Response({"permissions": permissions})
Beispiel #14
0
         "If we are looking for an analysis executed with this flag set",
     ),
     OpenApiParameter(
         name="running_only",
         type=OpenApiTypes.BOOL,
         description="""
         Check only for running analysis,
         default False, any value is True""",
     ),
 ],
 responses={
     200:
     inline_serializer(
         name="AskAnalysisAvailabilitySuccessResponse",
         fields={
             "status": BaseSerializer.StringRelatedField(),
             "job_id": BaseSerializer.StringRelatedField(),
             "analyzers_to_execute": OpenApiTypes.OBJECT,
         },
     ),
     400:
     inline_serializer(
         name="AskAnalysisAvailabilityInsufficientDataResponse",
         fields={
             "error": BaseSerializer.StringRelatedField(),
         },
     ),
     500:
     inline_serializer(
         name="AskAnalysisAvailabilityErrorResponse",
         fields={
             "detail": BaseSerializer.StringRelatedField(),
Beispiel #15
0
from drf_spectacular.utils import inline_serializer
from rest_framework.serializers import CharField

bad_request_serializer = inline_serializer(name="bad_request",
                                           fields={"detail": CharField()})
forbidden_request_serializer = inline_serializer(
    name="forbbiden_request", fields={"detail": CharField()})
Beispiel #16
0
    def __call__(self, request):

        # https://stackoverflow.com/questions/26240832/django-and-middleware-which-uses-request-user-is-always-anonymous
        request.iam_context = SimpleLazyObject(lambda: get_context(request))

        return self.get_response(request)


@extend_schema(tags=['auth'])
@extend_schema_view(post=extend_schema(
    summary='This method signs URL for access to the server',
    description=
    'Signed URL contains a token which authenticates a user on the server.'
    'Signed URL is valid during 30 seconds since signing.',
    request=inline_serializer(name='Signing',
                              fields={
                                  'url': serializers.CharField(),
                              }),
    responses={
        '200': OpenApiResponse(response=OpenApiTypes.STR,
                               description='text URL')
    }))
class SigningView(views.APIView):
    def post(self, request):
        url = request.data.get('url')
        if not url:
            raise ValidationError('Please provide `url` parameter')

        signer = Signer()
        url = self.request.build_absolute_uri(url)
        sign = signer.sign(self.request.user, url)
Beispiel #17
0
class FlowViewSet(ModelViewSet):
    """Flow Viewset"""

    queryset = Flow.objects.all()
    serializer_class = FlowSerializer
    lookup_field = "slug"
    search_fields = ["name", "slug", "designation", "title"]
    filterset_fields = ["flow_uuid", "name", "slug", "designation"]

    @permission_required(None, ["authentik_flows.view_flow_cache"])
    @extend_schema(responses={200: CacheSerializer(many=False)})
    @action(detail=False, pagination_class=None, filter_backends=[])
    def cache_info(self, request: Request) -> Response:
        """Info about cached flows"""
        return Response(data={"count": len(cache.keys("flow_*"))})

    @permission_required(None, ["authentik_flows.clear_flow_cache"])
    @extend_schema(
        request=OpenApiTypes.NONE,
        responses={
            204: OpenApiResponse(description="Successfully cleared cache"),
            400: OpenApiResponse(description="Bad request"),
        },
    )
    @action(detail=False, methods=["POST"])
    def cache_clear(self, request: Request) -> Response:
        """Clear flow cache"""
        keys = cache.keys("flow_*")
        cache.delete_many(keys)
        LOGGER.debug("Cleared flow cache", keys=len(keys))
        return Response(status=204)

    @permission_required(
        None,
        [
            "authentik_flows.add_flow",
            "authentik_flows.change_flow",
            "authentik_flows.add_flowstagebinding",
            "authentik_flows.change_flowstagebinding",
            "authentik_flows.add_stage",
            "authentik_flows.change_stage",
            "authentik_policies.add_policy",
            "authentik_policies.change_policy",
            "authentik_policies.add_policybinding",
            "authentik_policies.change_policybinding",
            "authentik_stages_prompt.add_prompt",
            "authentik_stages_prompt.change_prompt",
        ],
    )
    @extend_schema(
        request={
            "multipart/form-data":
            inline_serializer("SetIcon", fields={"file": FileField()})
        },
        responses={
            204: OpenApiResponse(description="Successfully imported flow"),
            400: OpenApiResponse(description="Bad request"),
        },
    )
    @action(detail=False, methods=["POST"], parser_classes=(MultiPartParser, ))
    def import_flow(self, request: Request) -> Response:
        """Import flow from .akflow file"""
        file = request.FILES.get("file", None)
        if not file:
            return HttpResponseBadRequest()
        importer = FlowImporter(file.read().decode())
        valid = importer.validate()
        if not valid:
            return HttpResponseBadRequest()
        successful = importer.apply()
        if not successful:
            return Response(status=204)
        return HttpResponseBadRequest()

    @permission_required(
        "authentik_flows.export_flow",
        [
            "authentik_flows.view_flow",
            "authentik_flows.view_flowstagebinding",
            "authentik_flows.view_stage",
            "authentik_policies.view_policy",
            "authentik_policies.view_policybinding",
            "authentik_stages_prompt.view_prompt",
        ],
    )
    @extend_schema(
        responses={
            "200": OpenApiResponse(response=OpenApiTypes.BINARY),
        }, )
    @action(detail=True, pagination_class=None, filter_backends=[])
    # pylint: disable=unused-argument
    def export(self, request: Request, slug: str) -> Response:
        """Export flow to .akflow file"""
        flow = self.get_object()
        exporter = FlowExporter(flow)
        response = JsonResponse(exporter.export(),
                                encoder=DataclassEncoder,
                                safe=False)
        response[
            "Content-Disposition"] = f'attachment; filename="{flow.slug}.akflow"'
        return response

    @extend_schema(responses={200: FlowDiagramSerializer()})
    @action(detail=True,
            pagination_class=None,
            filter_backends=[],
            methods=["get"])
    # pylint: disable=unused-argument
    def diagram(self, request: Request, slug: str) -> Response:
        """Return diagram for flow with slug `slug`, in the format used by flowchart.js"""
        flow = self.get_object()
        header = [
            DiagramElement("st", "start", "Start"),
        ]
        body: list[DiagramElement] = []
        footer = []
        # First, collect all elements we need
        for s_index, stage_binding in enumerate(
                get_objects_for_user(
                    request.user,
                    "authentik_flows.view_flowstagebinding").filter(
                        target=flow).order_by("order")):
            for p_index, policy_binding in enumerate(
                    get_objects_for_user(
                        request.user,
                        "authentik_policies.view_policybinding").filter(
                            target=stage_binding).exclude(
                                policy__isnull=True).order_by("order")):
                body.append(
                    DiagramElement(
                        f"stage_{s_index}_policy_{p_index}",
                        "condition",
                        f"Policy\n{policy_binding.policy.name}",
                    ))
            body.append(
                DiagramElement(
                    f"stage_{s_index}",
                    "operation",
                    f"Stage\n{stage_binding.stage.name}",
                ))
        # If the 2nd last element is a policy, we need to have an item to point to
        # for a negative case
        body.append(DiagramElement("e", "end", "End|future"), )
        if len(body) == 1:
            footer.append("st(right)->e")
        else:
            # Actual diagram flow
            footer.append(f"st(right)->{body[0].identifier}")
            for index in range(len(body) - 1):
                element: DiagramElement = body[index]
                if element.type == "condition":
                    # Policy passes, link policy yes to next stage
                    footer.append(
                        f"{element.identifier}(yes, right)->{body[index + 1].identifier}"
                    )
                    # Policy doesn't pass, go to stage after next stage
                    no_element = body[index + 1]
                    if no_element.type != "end":
                        no_element = body[index + 2]
                    footer.append(
                        f"{element.identifier}(no, bottom)->{no_element.identifier}"
                    )
                elif element.type == "operation":
                    footer.append(
                        f"{element.identifier}(bottom)->{body[index + 1].identifier}"
                    )
        diagram = "\n".join([str(x) for x in header + body + footer])
        return Response({"diagram": diagram})

    @permission_required("authentik_flows.change_flow")
    @extend_schema(
        request={
            "multipart/form-data":
            inline_serializer("SetIcon", fields={"file": FileField()})
        },
        responses={
            200: OpenApiResponse(description="Success"),
            400: OpenApiResponse(description="Bad request"),
        },
    )
    @action(
        detail=True,
        pagination_class=None,
        filter_backends=[],
        methods=["POST"],
        parser_classes=(MultiPartParser, ),
    )
    # pylint: disable=unused-argument
    def set_background(self, request: Request, slug: str):
        """Set Flow background"""
        flow: Flow = self.get_object()
        icon = request.FILES.get("file", None)
        if not icon:
            return HttpResponseBadRequest()
        flow.background = icon
        flow.save()
        return Response({})

    @permission_required("authentik_core.change_application")
    @extend_schema(
        request=inline_serializer("SetIconURL", fields={"url": CharField()}),
        responses={
            200: OpenApiResponse(description="Success"),
            400: OpenApiResponse(description="Bad request"),
        },
    )
    @action(
        detail=True,
        pagination_class=None,
        filter_backends=[],
        methods=["POST"],
    )
    # pylint: disable=unused-argument
    def set_background_url(self, request: Request, slug: str):
        """Set Flow background (as URL)"""
        flow: Flow = self.get_object()
        url = request.data.get("url", None)
        if not url:
            return HttpResponseBadRequest()
        flow.background = url
        flow.save()
        return Response({})

    @extend_schema(
        responses={
            200: LinkSerializer(many=False),
            400: OpenApiResponse(description="Flow not applicable"),
        }, )
    @action(detail=True, pagination_class=None, filter_backends=[])
    # pylint: disable=unused-argument
    def execute(self, request: Request, slug: str):
        """Execute flow for current user"""
        flow: Flow = self.get_object()
        planner = FlowPlanner(flow)
        planner.use_cache = False
        try:
            plan = planner.plan(self.request,
                                {PLAN_CONTEXT_PENDING_USER: request.user})
            self.request.session[SESSION_KEY_PLAN] = plan
        except FlowNonApplicableException as exc:
            return bad_request_message(
                request,
                _("Flow not applicable to current user/request: %(messages)s" %
                  {"messages": str(exc)}),
            )
        return Response({
            "link":
            request._request.build_absolute_uri(
                reverse("authentik_core:if-flow",
                        kwargs={"flow_slug": flow.slug}))
        })
Beispiel #18
0
class StatementViewSet(UserStampedModelViewSetMixin, viewsets.ModelViewSet):
    serializer_class = StatementSerializer
    queryset = Statement.objects.all()
    filterset_class = StatementFilter

    @extend_schema(responses=inline_serializer(
        name="UploadWeightageResponseSerializer",
        fields={
            "detail":
            serializers.CharField(
                default=_("Successfully uploaded weightage for statement"))
        },
    ))
    @action(
        detail=True,
        methods=["post"],
        serializer_class=UploadWeightageSerializer,
    )
    def upload_weightage(self, request, *args, **kwargs):
        statement = self.get_object()
        user = self.request.user
        serializer = self.get_serializer(data=request.data)
        if not serializer.is_valid():
            return Response(serializer.errors,
                            status=status.HTTP_400_BAD_REQUEST)
        data = serializer.validated_data
        question_group = data.pop("question_group")
        module = data.pop("module", None)
        has_formula = "formula" in data.keys()
        if has_formula and module is None:
            return Response(
                {"error": _("Module required if formula is present")})
        try:
            with transaction.atomic():
                QuestionStatement.objects.filter(statement=statement,
                                                 question_group=question_group,
                                                 version="draft").delete()

                for question_statement_data in data["questions"]:
                    QuestionStatement.objects.create(
                        **question_statement_data,
                        question_group=question_group,
                        statement=statement,
                        version="draft",
                        created_by=user,
                    )

                OptionStatement.objects.filter(statement=statement,
                                               question_group=question_group,
                                               version="draft").delete()
                for option_statement_data in data["options"]:
                    OptionStatement.objects.create(
                        **option_statement_data,
                        question_group=question_group,
                        statement=statement,
                        version="draft",
                        created_by=user,
                    )

                if has_formula:
                    formula = data["formula"]
                    StatementFormula.objects.filter(
                        statement=statement,
                        question_group=question_group,
                        module=module,
                        version="draft",
                    ).delete()
                    if formula is not None:
                        StatementFormula.objects.create(
                            formula=formula,
                            module=module,
                            statement=statement,
                            question_group=question_group,
                            version="draft",
                            created_by=user,
                        )
        except Exception as e:
            print(e)
            return Response(
                {"error": _("Failed to upload weightage for statement")},
                status=status.HTTP_400_BAD_REQUEST,
            )
        return Response(
            {"detail": _("Successfully uploaded weightage for statement")},
            status=status.HTTP_201_CREATED,
        )

    @extend_schema(responses=inline_serializer(
        name="ActivateVersionResponseSerializer",
        fields={
            "detail":
            serializers.CharField(
                default=_("Successfully activate new version"))
        },
    ))
    @action(
        detail=True,
        methods=["post"],
        serializer_class=ActivateVersionSerializer,
    )
    def activate_version(self, request, *args, **kwargs):
        statement = self.get_object()
        user = self.request.user
        serializer = self.get_serializer(data=request.data)
        if not serializer.is_valid():
            return Response(serializer.errors,
                            status=status.HTTP_400_BAD_REQUEST)
        data = serializer.validated_data
        version = data["version"]
        question_group = data.pop("question_group", None)
        if not QuestionStatement.objects.filter(
                version=version,
                statement=statement,
                question_group=question_group).exists():
            return Response(
                {"error": _("No question statements found for this version")},
                status=status.HTTP_400_BAD_REQUEST,
            )
        if not OptionStatement.objects.filter(
                version=version,
                statement=statement,
                question_group=question_group).exists():
            return Response(
                {"error": _("No option statements found for this version")},
                status=status.HTTP_400_BAD_REQUEST,
            )
        if not StatementFormula.objects.filter(
                version=version,
                statement=statement,
                question_group=question_group).exists():
            return Response(
                {"error": _("No statement formulas found for this version")},
                status=status.HTTP_400_BAD_REQUEST,
            )
        with transaction.atomic():
            QuestionStatement.objects.filter(
                statement=statement,
                question_group=question_group).exclude(version=version).update(
                    is_active=False, updated_by=user)
            QuestionStatement.objects.filter(
                statement=statement,
                question_group=question_group).filter(version=version).update(
                    is_active=True, updated_by=user)
            OptionStatement.objects.filter(
                statement=statement,
                question_group=question_group).exclude(version=version).update(
                    is_active=False, updated_by=user)
            OptionStatement.objects.filter(
                statement=statement,
                question_group=question_group).filter(version=version).update(
                    is_active=True, updated_by=user)
            StatementFormula.objects.filter(
                statement=statement,
                question_group=question_group).exclude(version=version).update(
                    is_active=False, updated_by=user)
            StatementFormula.objects.filter(
                statement=statement,
                question_group=question_group).filter(version=version).update(
                    is_active=True, updated_by=user)
        return Response({"detail": _("Successfully activate new version")})

    @extend_schema(responses=inline_serializer(
        name="ActivateDraftVersionResponseSerializer",
        fields={
            "detail":
            serializers.CharField(
                default=_("Successfully activate draft version"))
        },
    ))
    @action(
        detail=True,
        methods=["post"],
        serializer_class=ActivateDraftVersionSerializer,
    )
    def activate_draft_version(self, request, *args, **kwargs):
        statement = self.get_object()
        user = self.request.user
        version = timezone.now().strftime("%Y-%m-%d %H:%M:%S")
        serializer = self.get_serializer(data=request.data)
        if not serializer.is_valid():
            return Response(serializer.errors,
                            status=status.HTTP_400_BAD_REQUEST)
        data = serializer.validated_data
        question_group = data.pop("question_group", None)
        if not QuestionStatement.objects.filter(
                version="draft",
                statement=statement,
                question_group=question_group).exists():
            return Response(
                {"error": _("No question statements found for draft")},
                status=status.HTTP_400_BAD_REQUEST,
            )
        if not OptionStatement.objects.filter(
                version="draft",
                statement=statement,
                question_group=question_group).exists():
            return Response(
                {"error": _("No option statements found for draft")},
                status=status.HTTP_400_BAD_REQUEST,
            )
        if not StatementFormula.objects.filter(
                version="draft",
                statement=statement,
                question_group=question_group).exists():
            return Response(
                {"error": _("No statement formulas found for draft")},
                status=status.HTTP_400_BAD_REQUEST,
            )
        with transaction.atomic():
            # Rename version to date time based version
            QuestionStatement.objects.filter(
                statement=statement,
                version="draft",
                question_group=question_group).update(version=version)
            OptionStatement.objects.filter(
                statement=statement,
                version="draft",
                question_group=question_group).update(version=version)
            StatementFormula.objects.filter(
                statement=statement,
                version="draft",
                question_group=question_group).update(version=version)
            # Make all old version inactive and new version active version
            QuestionStatement.objects.filter(
                statement=statement,
                question_group=question_group).exclude(version=version).update(
                    is_active=False, updated_by=user)
            QuestionStatement.objects.filter(
                statement=statement,
                question_group=question_group).filter(version=version).update(
                    is_active=True, updated_by=user)
            OptionStatement.objects.filter(
                statement=statement,
                question_group=question_group).exclude(version=version).update(
                    is_active=False, updated_by=user)
            OptionStatement.objects.filter(
                statement=statement,
                question_group=question_group).filter(version=version).update(
                    is_active=True, updated_by=user)
            StatementFormula.objects.filter(
                statement=statement,
                question_group=question_group).exclude(version=version).update(
                    is_active=False, updated_by=user)
            StatementFormula.objects.filter(
                statement=statement,
                question_group=question_group).filter(version=version).update(
                    is_active=True, updated_by=user)
        return Response({"detail": _("Successfully activate draft version")})
Beispiel #19
0
from dms.serializers.common import (
    AuthorSerializer,
    PublisherSerializer,
    SubjectSerializer,
)


class BookSerializer(DocumentSerializer):
    authors = AuthorSerializer(many=True, read_only=True)
    publishers = PublisherSerializer(many=True, read_only=True)
    subjects = SubjectSerializer(many=True, read_only=True)

    class Meta:
        model = Book
        fields = "__all__"


class BookMutateSerializer(DocumentMutateSerializer):
    class Meta:
        model = Book
        fields = "__all__"


BookMutateSerializerForSwagger = inline_serializer(
    name="BookMutateInline",
    fields={
        "content": serializers.FileField(),
        "data": BookMutateSerializer(),
    },
)
Beispiel #20
0
class UserViewSet(mixins.ListModelMixin, viewsets.GenericViewSet):
    serializer_class = UserSerializer
    permission_classes = [permissions.IsAuthenticated]

    def get_queryset(self):
        search_term = self.request.query_params.get("search", "")
        if len(search_term) < 3:
            return User.objects.none()
        return User.objects.filter(
            Q(username__icontains=search_term)
            | Q(first_name__icontains=search_term)
            | Q(last_name__icontains=search_term)).filter(is_active=True)

    @action(
        methods=[],
        detail=False,
        serializer_class=PrivateUserSerializer,
    )
    def me(self, request, *args, **kwargs):
        pass

    @me.mapping.get
    def get_me(self, request, *args, **kwargs):
        serializer = self.get_serializer(self.request.user)
        return Response(serializer.data)

    @me.mapping.patch
    def patch_me(self, request, *args, **kwargs):
        serializer = self.get_serializer(self.request.user,
                                         data=request.data,
                                         partial=True)
        if not serializer.is_valid():
            return Response(serializer.errors,
                            status=status.HTTP_400_BAD_REQUEST)
        serializer.save()
        return Response(serializer.data)

    @extend_schema(responses=inline_serializer(
        name="ChangePasswordResponseSerializer",
        fields={
            "detail":
            serializers.CharField(default=_("Password successfully updated"))
        },
    ))
    @action(
        methods=["post"],
        detail=False,
        serializer_class=ChangePasswordSerializer,
        url_path="me/change_password",
    )
    def change_password(self, request, *args, **kwargs):
        serializer = self.get_serializer(data=request.data)
        if not serializer.is_valid():
            return Response(
                serializer.errors,
                status=status.HTTP_400_BAD_REQUEST,
            )
        data = serializer.data
        user = self.request.user
        new_password = data["new_password"]
        try:
            validate_password(password=new_password, user=user)
        except ValidationError as e:
            errors = list(e.messages)
            return Response({"non_field_errors": errors},
                            status=status.HTTP_400_BAD_REQUEST)
        user.set_password(new_password)
        user.save()
        return Response({"detail": _("Password successfully updated")})

    @extend_schema(responses=inline_serializer(
        name="RegisterUserResponseSerializer",
        fields={
            "detail":
            serializers.CharField(default=_(
                "User successfully registered and email send to user's email address"
            ))
        },
    ))
    @action(
        methods=["post"],
        detail=False,
        permission_classes=[permissions.AllowAny],
        serializer_class=CurrentlyEnabledUserRegisterSerializer,
    )
    def register(self, request, *args, **kwargs):
        serializer = self.get_serializer(data=request.data)
        if not serializer.is_valid():
            return Response(
                serializer.errors,
                status=status.HTTP_400_BAD_REQUEST,
            )
        data = serializer.data
        user_exists = User.objects.filter_by_username(
            data["username"]).exists()
        if user_exists:
            return Response(
                {"error": _("User with username/email already exists")},
                status=status.HTTP_400_BAD_REQUEST,
            )
        data.pop("re_password")
        user_password = data.pop("password")
        try:
            validate_password(password=user_password)
        except ValidationError as e:
            errors = list(e.messages)
            return Response({"password": errors},
                            status=status.HTTP_400_BAD_REQUEST)
        user = User.objects.create_user(**data)
        user.set_password(user_password)
        user.save()
        return Response(
            {
                "detail":
                _("User successfully registered and email send to user's email address"
                  )
            },
            status=status.HTTP_201_CREATED,
        )

    @extend_schema(responses=inline_serializer(
        name="PasswordResetResponseSerializer",
        fields={
            "detail":
            serializers.CharField(
                default=_("Password reset email successfully send"))
        },
    ))
    @action(
        methods=["post"],
        detail=False,
        permission_classes=[permissions.AllowAny],
        serializer_class=UserNameSerializer,
    )
    def password_reset(self, request, *args, **kwargs):
        serializer = self.get_serializer(data=request.data)
        if not serializer.is_valid():
            return Response(
                serializer.errors,
                status=status.HTTP_400_BAD_REQUEST,
            )
        data = serializer.data
        username = data["username"]
        user = User.objects.filter_by_username(username,
                                               is_active=True).first()
        if not user:
            return Response(
                {
                    "error":
                    _("No active user present for username/email your account may be blocked"
                      )
                },
                status=status.HTTP_404_NOT_FOUND,
            )
        random_6_digit_pin = gen_random_number(6)
        active_for_one_hour = timezone.now() + timezone.timedelta(hours=1)
        identifier = gen_random_string(length=16)
        password_reset_pin_object, _created = PasswordResetPin.objects.update_or_create(
            user=user,
            defaults={
                "pin": random_6_digit_pin,
                "pin_expiry_time": active_for_one_hour,
                "is_active": True,
                "identifier": identifier,
            },
        )
        subject, html_message, text_message = EmailTemplate.objects.get(
            identifier="password_reset").get_email_contents(
                context={
                    "user": user,
                    "password_reset_object": password_reset_pin_object
                })
        user.email_user(subject, text_message, html_message=html_message)
        return Response(
            {"detail": _("Password reset email successfully send")})

    @extend_schema(responses=inline_serializer(
        name="PasswordResetVerifyResponseSerializer",
        fields={"identifier": serializers.CharField()},
    ))
    @action(
        methods=["post"],
        detail=False,
        permission_classes=[permissions.AllowAny],
        serializer_class=PinVerifySerializer,
        url_path="password_reset/verify",
    )
    def password_reset_verify(self, request, *args, **kwargs):
        serializer = self.get_serializer(data=request.data)
        if not serializer.is_valid():
            return Response(
                serializer.errors,
                status=status.HTTP_400_BAD_REQUEST,
            )
        data = serializer.data
        username = data["username"]
        user = User.objects.filter_by_username(username,
                                               is_active=True).first()
        if not user:
            return Response(
                {"error": _("No active user present for username/email")},
                status=status.HTTP_404_NOT_FOUND,
            )
        pin = data["pin"]
        current_time = timezone.now()
        password_reset_pin_object = PasswordResetPin.objects.filter(
            user=user,
            user__is_active=True,
            pin=pin,
            is_active=True,
            pin_expiry_time__gte=current_time,
        ).first()
        if not password_reset_pin_object:
            user_only_password_reset_object = PasswordResetPin.objects.filter(
                user=user).first()
            if user_only_password_reset_object:
                user_only_password_reset_object.no_of_incorrect_attempts += 1
                user_only_password_reset_object.save()
                if not user.is_active:
                    return Response(
                        {"error": _("User is inactive")},
                        status=status.status.HTTP_400_BAD_REQUEST,
                    )
                elif user_only_password_reset_object.no_of_incorrect_attempts >= 5:
                    user.is_active = False
                    user.save()
                    return Response(
                        {
                            "error":
                            _("User is now inactive for trying too many times")
                        },
                        status=status.HTTP_429_TOO_MANY_REQUESTS,
                    )
                elif user_only_password_reset_object.pin != pin:
                    return Response(
                        {"error": _("Password reset pin is incorrect")},
                        status=status.HTTP_400_BAD_REQUEST,
                    )
                else:
                    return Response(
                        {"error": _("Password reset pin has expired")},
                        status=status.HTTP_400_BAD_REQUEST,
                    )
            return Response(
                {"error": _("No matching active user pin found")},
                status=status.HTTP_404_NOT_FOUND,
            )
        else:
            password_reset_pin_object_identifier = password_reset_pin_object.identifier
            password_reset_pin_object.no_of_incorrect_attempts = 0
            password_reset_pin_object.save()
            return Response(
                {"identifier": password_reset_pin_object_identifier})

    @extend_schema(responses=inline_serializer(
        name="PasswordResetChangeResponseSerializer",
        fields={
            "detail":
            serializers.CharField(default=_("Password successfully changed"))
        },
    ))
    @action(
        methods=["post"],
        detail=False,
        permission_classes=[permissions.AllowAny],
        serializer_class=PasswordResetPasswordChangeSerializer,
        url_path="password_reset/change",
    )
    def password_reset_change(self, request, *args, **kwargs):
        serializer = self.get_serializer(data=request.data)
        if not serializer.is_valid():
            return Response(
                serializer.errors,
                status=status.HTTP_400_BAD_REQUEST,
            )
        data = serializer.data
        username = data["username"]
        user = User.objects.filter_by_username(username,
                                               is_active=True).first()
        if not user:
            return Response(
                {"error": _("No active user present for username/email")},
                status=status.HTTP_404_NOT_FOUND,
            )
        identifier = data["identifier"]
        password = data["password"]
        re_password = data["re_password"]
        if re_password != password:
            return Response(
                {"error": _("Password and re_password doesn't match")},
                status=status.HTTP_400_BAD_REQUEST,
            )
        current_time = timezone.now()
        password_reset_pin_object = PasswordResetPin.objects.filter(
            user=user,
            user__is_active=True,
            identifier=identifier,
            is_active=True,
            pin_expiry_time__gte=current_time,
        ).first()
        if not password_reset_pin_object:
            user_only_password_reset_object = PasswordResetPin.objects.filter(
                user=user).first()
            if user_only_password_reset_object:
                user_only_password_reset_object.no_of_incorrect_attempts += 1
                user_only_password_reset_object.save()
                if not user.is_active:
                    return Response(
                        {"error": _("User is inactive")},
                        status=status.status.HTTP_400_BAD_REQUEST,
                    )
                elif user_only_password_reset_object.no_of_incorrect_attempts >= 5:
                    user.is_active = False
                    user.save()
                    return Response(
                        {
                            "error":
                            _("User is now inactive for trying too many times")
                        },
                        status=status.HTTP_429_TOO_MANY_REQUESTS,
                    )
                elif user_only_password_reset_object.identifier != identifier:
                    return Response(
                        {"error": _("Password reset identifier is incorrect")},
                        status=status.HTTP_400_BAD_REQUEST,
                    )
                else:
                    return Response(
                        {"error": _("Password reset pin has expired")},
                        status=status.HTTP_400_BAD_REQUEST,
                    )
            return Response(
                {"error": _("No matching active user pin found")},
                status=status.HTTP_404_NOT_FOUND,
            )
        else:
            try:
                validate_password(password=password, user=user)
            except ValidationError as e:
                errors = list(e.messages)
                return Response({"password": errors},
                                status=status.HTTP_400_BAD_REQUEST)
            password_reset_pin_object.no_of_incorrect_attempts = 0
            password_reset_pin_object.is_active = False
            password_reset_pin_object.save()
            user.set_password(password)
            user.save()
            return Response({"detail": _("Password successfully changed")})

    @extend_schema(responses=inline_serializer(
        name="EmailConfirmResponseSerializer",
        fields={
            "detail":
            serializers.CharField(
                default=_("Email confirmation mail successfully sent"))
        },
    ))
    @action(
        methods=["post"],
        detail=False,
        permission_classes=[permissions.AllowAny],
        serializer_class=UserNameSerializer,
    )
    def email_confirm(self, request, *args, **kwargs):
        serializer = self.get_serializer(data=request.data)
        if not serializer.is_valid():
            return Response(
                serializer.errors,
                status=status.HTTP_400_BAD_REQUEST,
            )
        data = serializer.data
        username = data["username"]
        user = User.objects.filter_by_username(username).first()
        if not user:
            return Response(
                {
                    "error":
                    _("No user present with given email address/username")
                },
                status=status.HTTP_404_NOT_FOUND,
            )
        email_confirm_pin = EmailConfirmationPin.objects.filter(
            user=user).first()
        if email_confirm_pin:
            if email_confirm_pin.no_of_incorrect_attempts >= 5:
                return Response(
                    {"error": _("User is inactive for trying too many times")},
                    status=status.HTTP_400_BAD_REQUEST,
                )
            if not email_confirm_pin.is_active:
                return Response(
                    {"error": _("Email address has already been confirmed")},
                    status=status.HTTP_404_NOT_FOUND,
                )
        random_6_digit_pin = gen_random_number(6)
        active_for_one_hour = timezone.now() + timezone.timedelta(hours=1)
        (
            email_confirm_pin_object,
            _created,
        ) = EmailConfirmationPin.objects.update_or_create(
            user=user,
            defaults={
                "pin": random_6_digit_pin,
                "pin_expiry_time": active_for_one_hour,
                "is_active": True,
            },
        )
        subject, html_message, text_message = EmailTemplate.objects.get(
            identifier="email_confirm").get_email_contents({
                "user":
                user,
                "email_confirm_object":
                email_confirm_pin_object
            })
        user.email_user(subject, text_message, html_message=html_message)
        return Response(
            {"detail": _("Email confirmation mail successfully sent")})

    @extend_schema(responses=inline_serializer(
        name="EmailConfirmVerifyResponseSerializer",
        fields={
            "detail":
            serializers.CharField(default=_("Email successfully confirmed"))
        },
    ))
    @action(
        methods=["post"],
        detail=False,
        permission_classes=[permissions.AllowAny],
        serializer_class=PinVerifySerializer,
        url_path="email_confirm/verify",
    )
    def email_confirm_verify(self, request, *args, **kwargs):
        serializer = self.get_serializer(data=request.data)
        if not serializer.is_valid():
            return Response(
                serializer.errors,
                status=status.HTTP_400_BAD_REQUEST,
            )
        data = serializer.data
        username = data["username"]
        user = User.objects.filter_by_username(username,
                                               is_active=False).first()
        if not user:
            return Response(
                {"error": _("No inactive user present for username")},
                status=status.HTTP_404_NOT_FOUND,
            )
        pin = data["pin"]
        current_time = timezone.now()
        email_confirmation_mail_object = EmailConfirmationPin.objects.filter(
            user=user,
            pin=pin,
            is_active=True,
            pin_expiry_time__gte=current_time,
        ).first()
        if not email_confirmation_mail_object:
            user_only_email_confirm_mail_object = EmailConfirmationPin.objects.filter(
                user=user).first()
            if user_only_email_confirm_mail_object:
                if not user_only_email_confirm_mail_object.is_active:
                    return Response(
                        {"error": _("Email is already confirmed for user")},
                        status=status.HTTP_400_BAD_REQUEST,
                    )
                user_only_email_confirm_mail_object.no_of_incorrect_attempts += 1
                user_only_email_confirm_mail_object.save()
                if user_only_email_confirm_mail_object.no_of_incorrect_attempts >= 5:
                    return Response(
                        {
                            "error":
                            _("User is now inactive for trying too many times")
                        },
                        status=status.HTTP_429_TOO_MANY_REQUESTS,
                    )
                elif user_only_email_confirm_mail_object.pin != pin:
                    return Response(
                        {"error": _("Email confirmation pin is incorrect")},
                        status=status.HTTP_400_BAD_REQUEST,
                    )
                else:
                    return Response(
                        {"error": _("Email confirmation pin has expired")},
                        status=status.HTTP_400_BAD_REQUEST,
                    )
            return Response(
                {"error": _("No matching active username/email found")},
                status=status.HTTP_404_NOT_FOUND,
            )
        else:
            email_confirmation_mail_object.no_of_incorrect_attempts = 0
            email_confirmation_mail_object.is_active = False
            email_confirmation_mail_object.save()
            user.is_active = True
            user.save()
            return Response({"detail": _("Email successfully confirmed")})

    @extend_schema(responses=inline_serializer(
        name="EmailChangeResponseSerializer",
        fields={
            "detail":
            serializers.CharField(
                default=_("Email change mail successfully sent"))
        },
    ))
    @action(
        methods=["post"],
        detail=False,
        permission_classes=[permissions.AllowAny],
        serializer_class=EmailChangeSerializer,
    )
    def email_change(self, request, *args, **kwargs):
        serializer = self.get_serializer(data=request.data)
        if not serializer.is_valid():
            return Response(
                serializer.errors,
                status=status.HTTP_400_BAD_REQUEST,
            )
        data = serializer.data
        user = self.request.user
        email_change_pin = EmailChangePin.objects.filter(user=user).first()
        if email_change_pin:
            if email_change_pin.no_of_incorrect_attempts >= 5:
                return Response(
                    {"error": _("User is inactive for trying too many times")},
                    status=status.HTTP_400_BAD_REQUEST,
                )
        random_6_digit_pin = gen_random_number(6)
        active_for_one_hour = timezone.now() + timezone.timedelta(hours=1)
        email_change_pin_object, _created = EmailChangePin.objects.update_or_create(
            user=user,
            defaults={
                "pin": random_6_digit_pin,
                "pin_expiry_time": active_for_one_hour,
                "is_active": True,
                "new_email": data["new_email"],
            },
        )
        subject, html_message, text_message = EmailTemplate.objects.get(
            identifier="email_change").get_email_contents(
                {"email_change_object": email_change_pin_object})
        send_mail(
            subject,
            text_message,
            from_email=None,
            recipient_list=[email_change_pin_object.new_email],
            html_message=html_message,
        )
        return Response({"detail": _("Email change mail successfully sent")})

    @extend_schema(responses=inline_serializer(
        name="EmailChangeVerifyResponseSerializer",
        fields={
            "detail":
            serializers.CharField(default=_("Email successfully changed"))
        },
    ))
    @action(
        methods=["post"],
        detail=False,
        permission_classes=[permissions.IsAuthenticated],
        serializer_class=EmailChangePinVerifySerializer,
        url_path="email_change/verify",
    )
    def email_change_verify(self, request, *args, **kwargs):
        serializer = self.get_serializer(data=request.data)
        if not serializer.is_valid():
            return Response(
                serializer.errors,
                status=status.HTTP_400_BAD_REQUEST,
            )
        data = serializer.data
        user = self.request.user
        pin = data["pin"]
        current_time = timezone.now()
        email_change_mail_object = EmailChangePin.objects.filter(
            user=user,
            pin=pin,
            is_active=True,
            pin_expiry_time__gte=current_time,
        ).first()
        if not email_change_mail_object:
            user_only_email_change_mail_object = EmailChangePin.objects.filter(
                user=user).first()
            if user_only_email_change_mail_object:
                if not user_only_email_change_mail_object.is_active:
                    return Response(
                        {"error": _("Email is already changed for user")},
                        status=status.HTTP_400_BAD_REQUEST,
                    )
                user_only_email_change_mail_object.no_of_incorrect_attempts += 1
                user_only_email_change_mail_object.save()
                if user_only_email_change_mail_object.no_of_incorrect_attempts >= 5:
                    return Response(
                        {
                            "error":
                            _("User is now inactive for trying too many times")
                        },
                        status=status.HTTP_429_TOO_MANY_REQUESTS,
                    )
                elif user_only_email_change_mail_object.pin != pin:
                    return Response(
                        {"error": _("Email change pin is incorrect")},
                        status=status.HTTP_400_BAD_REQUEST,
                    )
                else:
                    return Response(
                        {"error": _("Email change pin has expired")},
                        status=status.HTTP_400_BAD_REQUEST,
                    )
            return Response(
                {"error": _("No matching active change change request found")},
                status=status.HTTP_404_NOT_FOUND,
            )
        else:
            if User.objects.filter(
                    email=email_change_mail_object.new_email).exists():
                return Response(
                    {"error": _("email already used for account creation")},
                    status=status.HTTP_400_BAD_REQUEST,
                )
            email_change_mail_object.no_of_incorrect_attempts = 0
            email_change_mail_object.is_active = False
            email_change_mail_object.save()
            user.email = email_change_mail_object.new_email
            user.save()
            return Response({"detail": _("Email successfully changed")})

    @extend_schema(responses=inline_serializer(
        name="UploadImageResponseSerializer",
        fields={
            "name": serializers.CharField(),
            "url": serializers.URLField()
        },
    ))
    @action(methods=["post"],
            detail=False,
            serializer_class=UploadImageSerializer)
    def upload_image(self, request, *args, **kwargs):
        username = self.request.user.username
        serializer = self.get_serializer(data=request.data)
        if not serializer.is_valid():
            return Response(
                serializer.errors,
                status=status.HTTP_400_BAD_REQUEST,
            )
        file = request.data["file"]
        upload_file_name = default_storage.get_valid_name(file.name)
        upload_path = os.path.join("user_uploaded_file", f"{username}",
                                   upload_file_name)
        saved_file = default_storage.save(upload_path, file)
        url = request.build_absolute_uri(default_storage.url(saved_file))
        data = {"name": saved_file, "url": url}
        return Response(data)
Beispiel #21
0
class CommentViewSet(
        mixins.ListModelMixin,
        mixins.CreateModelMixin,
        mixins.UpdateModelMixin,
        mixins.DestroyModelMixin,
        viewsets.GenericViewSet,
):
    serializer_class = TreeCommentSerializer
    pagination_class = LimitOffsetPagination
    queryset = BlogComment.objects.visible()
    permission_classes = [AllowAny]

    @method_decorator(csrf_exempt)
    def dispatch(self, request, *args, **kwargs):
        return super().dispatch(request, *args, **kwargs)

    @extend_schema(
        summary="Return comments tree", )
    @inject_comment_target
    def list(self, request, *args, **kwargs):
        return super().list(request, *args, **kwargs)

    def get_permissions(self):
        if self.action == "create":
            return [IsAuthenticated()]
        if self.action == "destroy":
            return [IsAuthenticated(), IsModerator()]
        if self.action in {"update", "partial_update"}:
            return [IsAuthenticated(), IsModeratorOrCreator()]
        return super().get_permissions()

    def get_serializer_class(self):
        if self.action == "list":
            return TreeCommentSerializer
        return CommentSerializer

    def filter_queryset(self, queryset):
        if self.action not in {"list"}:
            return super().filter_queryset(queryset)

        root_comments = super().filter_queryset(queryset)
        qs = (BlogComment.objects.get_queryset_descendants(
            queryset=root_comments.select_related("user"),
            include_self=True).select_related(
                "user", "parent", "parent__user").prefetch_related(
                    Prefetch(
                        "user__socialaccount_set",
                        queryset=SocialAccount.objects.all(),
                        to_attr="socialaccounts",
                    ),
                    Prefetch(
                        "parent__user__socialaccount_set",
                        queryset=SocialAccount.objects.all(),
                        to_attr="socialaccounts",
                    ),
                ))
        # root_comments.select_related("user")

        # 将 [root1, descendant1, descendant2, root2, descendant3, descendant4, root3]
        # 转换为:
        # [
        #   root1 (descendants = [descendant1, descendant2]),
        #   root2 (descendants = [descendant3, descendant4]),
        #   root3 (descendants = []),
        # ]
        root_comments_rv = reversed(root_comments)
        root = next(root_comments_rv, None)
        descendants = deque()
        for comment in reversed(qs):
            if comment.pk == root.pk:
                root.descendants = list(descendants)
                root = next(root_comments_rv, None)
                if root is None:
                    break
                descendants = deque()
                continue
            descendants.appendleft(comment)
        return root_comments

    def get_queryset(self):
        if self.action not in {"list"}:
            return super().get_queryset()

        target = self.kwargs.pop("target")
        target_ct = self.kwargs.pop("target_ct")
        root_comments = (BlogComment.objects.roots().filter(
            content_type=target_ct, object_pk=target.pk).prefetch_related(
                Prefetch(
                    "user__socialaccount_set",
                    queryset=SocialAccount.objects.all(),
                    to_attr="socialaccounts",
                ), ))
        return root_comments

    @extend_schema(
        summary="Create a comment",
        responses={200: CommentSerializer},
    )
    @inject_comment_target
    def create(self, request, *args, **kwargs):
        target = self.kwargs.pop("target")
        data = self.request.data.copy()
        data["user"] = self.request.user
        form = get_form()(target, data=data)

        if form.security_errors():
            raise ValidationError({
                "detail":
                _("The comment form failed security verification: %s") %
                escape(str(form.security_errors()))
            })

        if form.errors:
            return Response(
                {"detail": form.errors},
                status=status.HTTP_400_BAD_REQUEST,
            )

        site = get_current_site(request)
        ip_address, is_routable = get_client_ip(request)
        comment = form.get_comment_object(site_id=site.id)
        comment.ip_address = ip_address
        comment.user = request.user
        responses = signals.comment_will_be_posted.send(
            sender=comment.__class__, comment=comment, request=request)
        for (receiver, response) in responses:
            if response is False:
                return Response(
                    data={
                        "detail":
                        _("comment_will_be_posted receiver %r killed the comment"
                          ) % receiver.__name__
                    },
                    status=status.HTTP_412_PRECONDITION_FAILED,
                )

        comment.save()
        signals.comment_was_posted.send(sender=comment.__class__,
                                        comment=comment,
                                        request=request)

        serializer = CommentSerializer(instance=comment,
                                       context={"request": request})
        headers = self.get_success_headers(serializer.data)
        return Response(serializer.data,
                        status=status.HTTP_201_CREATED,
                        headers=headers)

    def perform_destroy(self, instance):
        instance.is_removed = True
        instance.save(update_fields=["is_removed"])

    @extend_schema(exclude=True)
    @field_whitelist(fields=["comment"], raise_exception=True)
    def update(self, request, *args, **kwargs):
        return super().update(request, *args, **kwargs)

    @extend_schema(
        summary="Edit comment",
        request=inline_serializer("UpdateCommentSerializer",
                                  fields={"comment": serializers.CharField()}),
        responses={200: CommentSerializer},
    )
    def partial_update(self, request, *args, **kwargs):
        return super().partial_update(request, *args, **kwargs)

    @extend_schema(
        summary="Get security data",
        responses={200: {
            "pong": "timestamp in milliseconds."
        }},
    )
    @inject_comment_target
    @action(methods=["get"], detail=False, url_path="security-data")
    def security_data(self, request, *args, **kwargs):
        target = self.kwargs.pop("target")
        form = get_form()(target)
        return Response(data=form.generate_security_data(),
                        status=status.HTTP_200_OK)
Beispiel #22
0
class OrganizationViewSet(UserStampedModelViewSetMixin, viewsets.ModelViewSet):
    filterset_class = OrganizationFilter
    permission_classes = [IsOrganizationAdminOrReadOnly]
    serializer_class = OrganizationSerializer

    def get_queryset(self):
        authenticated_user = self.request.user
        if authenticated_user.is_authenticated:
            filter_statement = Q(status="accepted") | Q(
                created_by=self.request.user)
        else:
            filter_statement = Q(status="accepted")
        return Organization.objects.filter(filter_statement).prefetch_related(
            "admins",
            "members",
        )

    @extend_schema(responses=inline_serializer(
        name="OrganizationProjectCreateResponseSerializer",
        fields={
            "detail":
            serializers.CharField(default=_("Successfully created project"))
        },
    ))
    @action(
        methods=["post"],
        detail=True,
        permission_classes=[permissions.IsAuthenticated],
        serializer_class=ProjectSerializer,
    )
    def create_project(self, request, *args, **kwargs):
        organization = self.get_object()
        data = request.data
        serializer = self.get_serializer(data=data)
        if not serializer.is_valid():
            return Response(serializer.errors,
                            status=status.HTTP_400_BAD_REQUEST)
        validated_data = serializer.validated_data
        Project.objects.create(**validated_data,
                               organization=organization,
                               created_by=self.request.user)
        return Response(
            {"detail": _("Successfully created project")},
            status=status.HTTP_201_CREATED,
        )

    @extend_schema(responses=inline_serializer(
        name="OrganizationMemberRequestResponseSerializer",
        fields={
            "detail":
            serializers.CharField(
                default=_("Successfully requested member access"))
        },
    ))
    @action(
        methods=["post"],
        detail=True,
        permission_classes=[permissions.IsAuthenticated],
        serializer_class=serializers.Serializer,
    )
    def member_request(self, request, *args, **kwargs):
        organization = self.get_object()
        user = self.request.user
        OrganizationMemberRequest.objects.create(user=user,
                                                 organization=organization,
                                                 created_by=user)
        return Response(
            {"detail": _("Successfully requested member access")},
            status=status.HTTP_200_OK,
        )

    @action(
        methods=["get"],
        detail=True,
        permission_classes=[IsOrganizationAdmin],
        serializer_class=ListOrganizationUserSerializer,
    )
    def users(self, request, *args, **kwargs):
        organization = self.get_object()
        users = []
        user_serializer_class = UserSerializer
        for admin in organization.admins.all():
            admin_data = user_serializer_class(admin).data
            users.append({"user": admin_data, "role": "admin"})
        for member in organization.members.all():
            member_data = user_serializer_class(member).data
            users.append({"user": member_data, "role": "member"})
        serializer = self.get_serializer(data=users, many=True)
        if not serializer.is_valid():
            return Response(serializer.errors,
                            status=status.HTTP_500_INTERNAL_SERVER_ERROR)
        return Response(serializer.data)

    @extend_schema(
        request=OrganizationUserSerializer(many=True),
        responses=inline_serializer(
            name="OrganizationAddUserSerializer",
            fields={
                "detail":
                serializers.CharField(
                    default=_("Successfully added all valid users"))
            },
        ),
    )
    @action(
        methods=["post"],
        detail=True,
        permission_classes=[IsOrganizationAdmin],
        serializer_class=OrganizationUserSerializer,
    )
    def update_or_add_users(self, request, *args, **kwargs):
        organization = self.get_object()
        serializer = self.get_serializer(data=request.data, many=True)
        if not serializer.is_valid():
            return Response(serializer.errors,
                            status=status.HTTP_400_BAD_REQUEST)

        try:
            with transaction.atomic():
                for validated_datum in serializer.validated_data:
                    user = User.objects.filter(
                        username=validated_datum["user"]).first()
                    if user:
                        if validated_datum["role"] == "admin":
                            organization.admins.add(user)
                            organization.members.remove(user)
                        elif validated_datum["role"] == "member":
                            organization.members.add(user)
                            organization.admins.remove(user)
        except Exception:
            return Response(
                {"error": _("Failed to add users to organization")},
                status=status.HTTP_400_BAD_REQUEST,
            )
        return Response({"detail": _("Successfully added all valid users")})

    @extend_schema(
        request=OrganizationUserSerializer(many=True),
        responses=inline_serializer(
            name="OrganizationAddUserSerializer",
            fields={
                "detail":
                serializers.CharField(
                    default=_("Successfully removed all valid users"))
            },
        ),
    )
    @action(
        methods=["post"],
        detail=True,
        permission_classes=[IsOrganizationAdmin],
        serializer_class=OrganizationUserSerializer,
    )
    def remove_users(self, request, *args, **kwargs):
        organization = self.get_object()
        serializer = self.get_serializer(data=request.data, many=True)
        if not serializer.is_valid():
            return Response(serializer.errors,
                            status=status.HTTP_400_BAD_REQUEST)

        try:
            with transaction.atomic():
                for validated_datum in serializer.validated_data:
                    user = User.objects.filter(
                        username=validated_datum["user"]).first()
                    if user:
                        if validated_datum["role"] == "admin":
                            organization.admins.remove(user)
                        elif validated_datum["role"] == "member":
                            organization.members.remove(user)
        except Exception:
            return Response(
                {"error": _("Failed to remove users from organization")},
                status=status.HTTP_400_BAD_REQUEST,
            )
        return Response({"detail": _("Successfully removed all valid users")})
Beispiel #23
0
class OrganizationSCIMTeamIndex(SCIMEndpoint, OrganizationTeamsEndpoint):
    permission_classes = (OrganizationSCIMTeamPermission,)
    public = {"GET", "POST"}

    def team_serializer_for_post(self):
        return TeamSCIMSerializer(expand=["members"])

    def should_add_creator_to_team(self, request: Request):
        return False

    @extend_schema(
        operation_id="List an Organization's Paginated Teams",
        parameters=[GLOBAL_PARAMS.ORG_SLUG, SCIMQueryParamSerializer],
        request=None,
        responses={
            200: scim_response_envelope(
                "SCIMTeamIndexResponse", OrganizationTeamSCIMSerializerResponse
            ),
            401: RESPONSE_UNAUTHORIZED,
            403: RESPONSE_FORBIDDEN,
            404: RESPONSE_NOTFOUND,
        },
        examples=[  # TODO: see if this can go on serializer object instead
            OpenApiExample(
                "listGroups",
                value={
                    "schemas": ["urn:ietf:params:scim:api:messages:2.0:ListResponse"],
                    "totalResults": 1,
                    "startIndex": 1,
                    "itemsPerPage": 1,
                    "Resources": [
                        {
                            "schemas": ["urn:ietf:params:scim:schemas:core:2.0:Group"],
                            "id": "23232",
                            "displayName": "test-scimv2",
                            "members": [],
                            "meta": {"resourceType": "Group"},
                        }
                    ],
                },
                status_codes=["200"],
            ),
        ],
    )
    def get(self, request: Request, organization) -> Response:
        """
        Returns a paginated list of teams bound to a organization with a SCIM Groups GET Request.
        - Note that the members field will only contain up to 10000 members.
        """

        query_params = self.get_query_parameters(request)

        queryset = Team.objects.filter(
            organization=organization, status=TeamStatus.VISIBLE
        ).order_by("slug")
        if query_params["filter"]:
            queryset = queryset.filter(slug__iexact=slugify(query_params["filter"]))

        def data_fn(offset, limit):
            return list(queryset[offset : offset + limit])

        def on_results(results):
            results = serialize(
                results,
                None,
                TeamSCIMSerializer(expand=_team_expand(query_params["excluded_attributes"])),
            )
            return self.list_api_format(results, queryset.count(), query_params["start_index"])

        return self.paginate(
            request=request,
            on_results=on_results,
            paginator=GenericOffsetPaginator(data_fn=data_fn),
            default_per_page=query_params["count"],
            queryset=queryset,
            cursor_cls=SCIMCursor,
        )

    @extend_schema(
        operation_id="Provision a New Team",
        parameters=[GLOBAL_PARAMS.ORG_SLUG],
        request=inline_serializer(
            "SCIMTeamRequestBody",
            fields={
                "schemas": serializers.ListField(serializers.CharField()),
                "displayName": serializers.CharField(),
                "members": serializers.ListField(serializers.IntegerField()),
            },
        ),
        responses={
            201: TeamSCIMSerializer,
            401: RESPONSE_UNAUTHORIZED,
            403: RESPONSE_FORBIDDEN,
            404: RESPONSE_NOTFOUND,
        },
        examples=[  # TODO: see if this can go on serializer object instead
            OpenApiExample(
                "provisionTeam",
                response_only=True,
                value={
                    "schemas": ["urn:ietf:params:scim:schemas:core:2.0:Group"],
                    "displayName": "Test SCIMv2",
                    "members": [],
                    "meta": {"resourceType": "Group"},
                    "id": "123",
                },
                status_codes=["201"],
            ),
        ],
    )
    def post(self, request: Request, organization) -> Response:
        """
        Create a new team bound to an organization via a SCIM Groups POST Request.
        Note that teams are always created with an empty member set.
        The endpoint will also do a normalization of uppercase / spaces to lowercase and dashes.
        """
        # shim displayName from SCIM api in order to work with
        # our regular team index POST
        request.data.update(
            {"name": request.data["displayName"], "slug": slugify(request.data["displayName"])}
        ),
        return super().post(request, organization)
Beispiel #24
0
class ProjectViewSet(UserStampedModelViewSetMixin, viewsets.ModelViewSet):
    permission_classes = [CanEditProjectOrReadOnly]
    filterset_class = ProjectFilter
    serializer_class = ProjectSerializer

    def get_queryset(self):
        current_user = self.request.user
        return (read_allowed_project_for_user(current_user).select_related(
            "created_by").prefetch_related("organization__admins"))

    @action(
        methods=["get"],
        detail=False,
        permission_classes=[CanAcceptRejectProject],
        serializer_class=ProjectSerializer,
    )
    def pending_requests(self, request, *args, **kwargs):
        user = self.request.user
        admin_organizations = Organization.objects.filter(admins=user)
        q_filter = Q(organization__in=admin_organizations)
        if user.is_superuser:
            q_filter = q_filter | Q(organization__isnull=True)
        projects = self.get_queryset().filter(q_filter)
        serializer = self.get_serializer(projects, many=True)
        return Response(serializer.data)

    @action(
        methods=["get"],
        detail=True,
        permission_classes=[CanEditProject],
        serializer_class=ProjectUserSerializer,
    )
    def users(self, request, *args, **kwargs):
        project = self.get_object()
        users = project.users.all()
        project_user = ProjectUser.objects.filter(project=project,
                                                  user__in=users)
        serializer = self.get_serializer(project_user, many=True)
        return Response(serializer.data)

    @extend_schema(responses=inline_serializer(
        name="ProjectAcceptResponseSerializer",
        fields={
            "detail":
            serializers.CharField(default=_("Project successfully accepted"))
        },
    ))
    @action(
        methods=["post"],
        detail=True,
        permission_classes=[CanAcceptRejectProject],
        serializer_class=serializers.Serializer,
    )
    def accept(self, request, *args, **kwargs):
        project = self.get_object()
        if project.status != "accepted":
            project.updated_by = request.user
            project.status = "accepted"
            project.save()
        return Response({"detail": _("Project successfully accepted")})

    @extend_schema(responses=inline_serializer(
        name="ProjectRejectResponseSerializer",
        fields={
            "detail":
            serializers.CharField(default=_("Project successfully rejected"))
        },
    ))
    @action(
        methods=["post"],
        detail=True,
        permission_classes=[CanAcceptRejectProject],
        serializer_class=serializers.Serializer,
    )
    def reject(self, request, *args, **kwargs):
        project = self.get_object()
        if project.status != "rejected":
            project.updated_by = request.user
            project.status = "rejected"
            project.save()
        return Response({"detail": _("Project successfully rejected")})

    @extend_schema(
        request=UpsertProjectUserSerializer(many=True),
        responses=inline_serializer(
            name="ProjectUpsertResponseSerializer",
            fields={
                "detail":
                serializers.CharField(
                    default=_("Successfully modified users list for project"))
            },
        ),
    )
    @action(
        methods=["post"],
        detail=True,
        permission_classes=[CanEditProject],
        serializer_class=UpsertProjectUserSerializer,
    )
    def update_or_add_users(self, request, *args, **kwargs):
        project = self.get_object()
        serializer = self.get_serializer(data=request.data, many=True)
        if not serializer.is_valid():
            return Response(serializer.errors,
                            status=status.HTTP_400_BAD_REQUEST)
        try:
            with transaction.atomic():
                for validated_datum in serializer.validated_data:
                    user = validated_datum.pop("user")
                    try:
                        project_user = ProjectUser.objects.get(project=project,
                                                               user=user)
                        validated_datum["updated_by"] = request.user
                        for key, value in validated_datum.items():
                            setattr(project_user, key, value)
                        project_user.save()
                    except ProjectUser.DoesNotExist:
                        validated_datum["created_by"] = request.user
                        project_user = ProjectUser.objects.create(
                            project=project, user=user, **validated_datum)
        except Exception:
            return Response(
                {
                    "error":
                    _("Failed to update or add users due to invalid data")
                },
                status=status.HTTP_400_BAD_REQUEST,
            )
        return Response(
            {"detail": _("Successfully modified users list for project")})

    @extend_schema(
        request=RemoveProjectUserSerializer(many=True),
        responses=inline_serializer(
            name="ProjectRemoveUserResponseSerializer",
            fields={
                "detail":
                serializers.CharField(
                    default=_("Successfully removed users from project"))
            },
        ),
    )
    @action(
        methods=["post"],
        detail=True,
        permission_classes=[CanEditProject],
        serializer_class=RemoveProjectUserSerializer,
    )
    def remove_users(self, request, *args, **kwargs):
        project = self.get_object()

        serializer = self.get_serializer(data=request.data, many=True)
        if not serializer.is_valid():
            return Response(serializer.errors,
                            status=status.HTTP_400_BAD_REQUEST)

        try:
            with transaction.atomic():
                for validated_datum in serializer.validated_data:
                    user_obj = validated_datum.pop("user")
                    project.users.remove(user_obj)
        except Exception:
            return Response(
                {"error": _("Failed to remove users due to invalid data")},
                status=status.HTTP_400_BAD_REQUEST,
            )
        return Response(
            {"detail": _("Successfully removed users from project")})

    @action(
        methods=["get"],
        detail=True,
        permission_classes=[permissions.IsAuthenticated],
        serializer_class=AccessLevelResponseSerializer,
    )
    def access_level(self, request, *args, **kwargs):
        project = self.get_object()
        user = self.request.user
        if project.organization and user in project.organization.admins.all():
            access_level = "organization_admin"
        elif user == project.created_by:
            access_level = "owner"
        elif user in project.users.all():
            permission = ProjectUser.objects.get(user=user,
                                                 project=project).permission
            if permission == "write":
                access_level = "write"
            else:
                access_level = "read_only"
        else:
            access_level = "visibility"
        data = {"access_level": access_level}
        serializer = self.get_serializer(data=data)
        if not serializer.is_valid():
            return Response(serializer.errors,
                            status=status.HTTP_500_INTERNAL_SERVER_ERROR)
        return Response(serializer.data)

    @extend_schema(responses=inline_serializer(
        name="ProjectSurveySubmitResponseSerializer",
        fields={
            "detail":
            serializers.CharField(default=_("Successfully submitted survey"))
        },
    ))
    @action(
        methods=["post"],
        detail=True,
        permission_classes=[CanCreateSurveyForProject],
        serializer_class=WritableSurveySerializer,
    )
    def create_survey(self, request, *args, **kwargs):
        project = self.get_object()
        data = request.data
        serializer = self.get_serializer(data=data)
        if not serializer.is_valid():
            return Response(serializer.errors,
                            status=status.HTTP_400_BAD_REQUEST)
        validated_data = serializer.validated_data
        answers = validated_data.pop("answers", [])
        results = validated_data.pop("results", [])
        try:
            with transaction.atomic():
                survey = Survey.objects.create(**validated_data,
                                               created_by=request.user,
                                               project=project)
                for answer in answers:
                    options = answer.pop("options", None)
                    survey_answer = SurveyAnswer.objects.create(
                        **answer, created_by=request.user, survey=survey)
                    if options:
                        survey_answer.options.add(*options)
                for result in results:
                    SurveyResult.objects.create(**result,
                                                created_by=request.user,
                                                survey=survey)
        except Exception:
            return Response(
                {
                    "error":
                    _("Failed to create survey or survey answer due to invalid data"
                      )
                },
                status=status.HTTP_400_BAD_REQUEST,
            )
        return Response(
            {"detail": _("Successfully submitted survey")},
            status=status.HTTP_201_CREATED,
        )
Beispiel #25
0
class VideoViewSetV2(mixins.CreateModelMixin,
                     mixins.ListModelMixin,
                     mixins.RetrieveModelMixin,
                     WithPKOverflowProtection,
                     viewsets.GenericViewSet, ):
    """Get videos and search results."""

    UPDATE_DOCSTRING = {
        'list': "List all videos with search/filter capability",
        'retrieve': "Get one video by internal ID",
        'create': "Add a video to the database (without filling the fields) from Youtube"}

    # serializers
    VSMany = VideoSerializerV2(many=True)
    VSOne = VideoSerializerV2

    KWARGS_DICT = {
        'create': {
            'responses': {
                400: None, 201: VideoSerializerV2}}, 'retrieve': {
            'responses': {
                404: None, 200: VideoSerializerV2}}, 'list': {
            'responses': {
                200: VSMany, 400: None, 404: None}}}

    queryset = Video.objects.filter(is_unlisted=False)
    serializer_class = VideoSerializerV2
    filter_backends = [filters.DjangoFilterBackend, filters_.OrderingFilter]
    filterset_class = VideoFilterV2
    permission_classes = [IsAuthenticatedOrReadOnly]

    # use Levenstein distance with Elastic search
    search_fields = ['name', 'description', 'uploader']

    # postgres weights https://docs.djangoproject.com/en/3.1/
    # ref/contrib/postgres/search/#weighting-queries
    search_weights = ['A', 'B', 'A']

    assert len(search_fields) == len(search_weights)

    ordering_fields = [
        'name',
        'video_id',
        'views',
        'language',
        'duration',
        'publication_date']

    def get_features_from_request(self):
        """Get preferences features from request, either from the user, or from attributes."""
        # by default, set to zeros
        vector = np.zeros(len(VIDEO_FIELDS))

        # trying to fill data from the user
        try:
            user_prefs = get_user_preferences(self.request)
            vector = user_prefs.features_as_vector_centered
        except UserPreferences.DoesNotExist:
            pass

        vector = update_preferences_vector_from_request(vector, self.request.query_params)

        return vector

    def need_scores_for_username(self):
        return search_username_from_request(self.request)

    def get_queryset(self, pk=None):
        """All videos except for null ones."""
        queryset = Video.objects.filter(is_unlisted=False).values()
        request = self.request

        fields = [x.name for x in Video._meta.fields]
        for f in VIDEO_FIELDS:
            fields.remove(f)

        def get_score_annotation(user_preferences_vector):
            """Returns an sql object annotating queries with the video ratings (sclar product)."""
            return sum(
                [F(f) * v for f, v in zip(VIDEO_FIELDS, user_preferences_vector)])

        features = self.get_features_from_request()
        default_features = [constants['DEFAULT_PREFS_VAL'] for _ in VIDEO_FIELDS]
        search_username = self.need_scores_for_username()

        # computing score inside the database
        if search_username:
            fields_exclude = set(Video.COMPUTED_PROPERTIES)
            fields = [f for f in fields if f not in fields_exclude]

            queryset = queryset.values(*fields)
            queryset = queryset.annotate(**{key: F(f'videorating__{key}') for key in VIDEO_FIELDS},
                                         user=F(
                                             'videorating__user__user__username')).filter(
                user=search_username)

            # for myself, allow showing public/non-public videos
            if search_username == request.user.username:
                is_public = request.query_params.get('show_all_my_videos', 'true') == 'false'
                print(is_public)
            else:  # for other people, only show public videos
                is_public = True

            # keeping only public videos
            if is_public:
                queryset = VideoRatingPrivacy._annotate_privacy(
                    queryset, prefix='videoratingprivacy', field_user=None,
                    filter_add={'videoratingprivacy__user__user__username': search_username}
                )
                queryset = queryset.filter(_is_public=True)

            queryset = queryset.annotate(rating_n_experts=Value(1, IntegerField()))

            q1 = Q(expertrating_video_1__user__user__username=search_username)
            q2 = Q(expertrating_video_2__user__user__username=search_username)

            c1 = Count('expertrating_video_1', q1, distinct=True)
            c2 = Count('expertrating_video_2', q2, distinct=True)

            queryset = queryset.annotate(rating_n_ratings=c1 + c2)

            queryset = queryset.annotate(n_public_experts=Value(1, IntegerField()))
            queryset = queryset.annotate(n_private_experts=Value(0, IntegerField()))

            # TODO: a hack. improve this
            queryset = queryset.annotate(
                    public_experts=Value("", CharField()))

            # logging model usage in search
            if self.request.user.is_authenticated:
                RepresentativeModelUsage.objects.get_or_create(
                    viewer=UserPreferences.objects.get(user__username=self.request.user.username),
                    model=UserPreferences.objects.get(user__username=search_username)
                )

        queryset = queryset.annotate(
            score_preferences_term=get_score_annotation(features))

        queryset = queryset.annotate(
            tournesol_score=get_score_annotation(default_features))

        queryset = queryset.annotate(
            score_search_term_=Value(
                0.0, FloatField()))

        if request.query_params.get('search'):
            # computing the postgres score for search
            if connection.vendor.startswith('postgres'):
                s_query = request.query_params.get('search', '')

                def word_to_query(w):
                    """Convert one word into a query."""
                    queries = []

                    queries.append(SearchQuery(w, search_type='raw'))
                    queries.append(SearchQuery(w + ':*', search_type='raw'))

                    return reduce(lambda x, y: x | y, queries)

                def words_to_query(s_query, max_len=100, max_word_len=20):
                    """Convert a string with words into a SearchQuery."""
                    s_query = s_query[:max_len]
                    s_query = s_query.split(' ')
                    s_query = [''.join(filter(str.isalnum, x)) for x in s_query]
                    s_query = [x for x in s_query if 1 <= len(x) <= max_word_len]
                    s_query = [word_to_query(x) for x in s_query]
                    if not s_query:
                        return SearchQuery('')
                    return reduce(lambda x, y: x & y, s_query)

                s_query = words_to_query(s_query)

                s_vectors = [SearchVector(f, weight=w) for f, w in zip(self.search_fields,
                                                                       self.search_weights)]
                s_vector = reduce(lambda x, y: x + y, s_vectors)

                queryset = queryset.annotate(
                    score_search_term_=SearchRank(s_vector, s_query))
            else:
                # in other databases, using basic filtering
                queryset = filters_.SearchFilter().filter_queryset(self.request, queryset, self)
                queryset = queryset.annotate(
                    score_search_term_=Value(
                        1.0, FloatField()))

        queryset = queryset.annotate(
            score_search_term=F('score_search_term_') *
            VideoSearchEngine.VIDEO_SEARCH_COEFF)
        queryset = queryset.annotate(
            score=F('score_preferences_term') +
            F('score_search_term'))

        return queryset

    def return_queryset(self, queryset):
        page = self.paginate_queryset(queryset)
        if page is not None:
            serializer = self.get_serializer(page, many=True)
            ser_data = serializer.data
            data = self.get_paginated_response(ser_data)
            return data

        serializer = self.get_serializer(queryset, many=True)
        data = Response(serializer.data)
        return data

    @extend_schema(
        parameters=[
            OpenApiParameter(
                name='search',
                description="Youtube search phrase",
                required=True,
                type=str)],
        responses={
            200: VSMany,
            400: None,
            404: None},
        operation_id="api_v2_video_search_youtube")
    @action(methods=['GET'], detail=False, name="Search using YouTube")
    def search_youtube(self, request):
        """Search videos using the YouTube algorithm."""
        filter = self.filterset_class(request=request)
        queryset = filter.filter_empty(
            self.filter_queryset(
                self.get_queryset()))
        queryset = search_yt_intersect_tournesol(
            request.query_params.get(
                'search', ""), queryset=queryset)
        return self.return_queryset(queryset)

    @extend_schema(responses={
                              200: VSMany,
                              400: None,
                              403: None,
                              404: None
            })
    @action(methods=['GET'], detail=False, name="List of rated videos")
    def rated_videos(self, request):
        filter = self.filterset_class(request=request)
        queryset = self.get_queryset()
        queryset = queryset.order_by('-score')
        queryset = filter.filter_empty(self.filter_queryset(queryset))
        queryset = queryset.filter(
                Q(expertrating_video_1__user__user__username=request.user.username) |
                Q(expertrating_video_1__user__user__username=request.user.username)).\
            distinct()
        return self.return_queryset(queryset)

    @extend_schema(operation_id="api_v2_video_search_tournesol",
                   responses={200: VSMany,
                              400: None,
                              403: None,
                              404: None},
                   parameters=[OpenApiParameter(name=k,
                                                description=v + " [preference override]",
                                                required=False,
                                                type=float) for k,
                               v in VIDEO_FIELDS_DICT.items()] + [
                       OpenApiParameter(name='search_model',
                                        description="Use this user's algorithmic representative",
                                        required=False,
                                        type=str)])
    @action(methods=['GET'], detail=False, name="Search using Tournesol")
    def search_tournesol(self, request):
        """Search videos using the Tournesol algorithm."""
        filter = self.filterset_class(request=request)
        queryset = self.get_queryset()
        queryset = queryset.order_by('-score')
        queryset = queryset.filter(~Q(tournesol_score=0))
        queryset = filter.filter_empty(self.filter_queryset(queryset))
        data = self.return_queryset(queryset)
        return data

    @extend_schema(operation_id="n_thanks",
                   responses={200: inline_serializer(
                       "NumberOfThanks", {'n_thanks': serializers.IntegerField(
                           required=True,
                           help_text="Number of people I thanked for this video")}),
                              400: None,
                              404: None},
                   parameters=[OpenApiParameter(name="video_id",
                                                description="Youtube Video ID",
                                                required=True,
                                                type=str)])
    @action(methods=['GET'], detail=False, name="Get number of people I thanked for a video")
    def n_thanks(self, request):
        """Get number of people I thanked for a video."""

        video = get_object_or_404(Video, video_id=request.query_params.get('video_id', ''))
        user = get_object_or_404(UserPreferences, user__username=request.user.username)

        n_thanks = VideoRatingThankYou.objects.filter(thanks_from=user,
                                                      video=video).count()
        return Response({'n_thanks': n_thanks}, status=200)

    @extend_schema(operation_id="my_ratings_are_private",
                   responses={200: inline_serializer(
                       "PrivateOrPublic", {'my_ratings_are_private': serializers.BooleanField(
                           required=True,
                           help_text="Are my ratings private?"),
                        'entry_found': serializers.BooleanField(
                               required=True,
                               help_text="Privacy entry found?")
                       }),
                       400: None,
                       404: None},
                   parameters=[OpenApiParameter(name="video_id",
                                                description="Youtube Video ID",
                                                required=True,
                                                type=str)])
    @action(methods=['GET'], detail=False, name="Are my ratings private?")
    def my_ratings_are_private(self, request):
        """Are my ratings private?"""

        video = get_object_or_404(Video, video_id=request.query_params.get('video_id', ''))
        user = get_object_or_404(UserPreferences, user__username=request.user.username)

        qs = VideoRatingPrivacy.objects.filter(user=user,
                                               video=video)

        if qs.count():
            value = qs.get().is_public
        else:
            value = VideoRatingPrivacy.DEFAULT_VALUE_IS_PUBLIC

        return Response({'my_ratings_are_private': not value,
                         'entry_found': qs.count() > 0},
                        status=200)

    # possible actions for thnk_contributors
    thank_actions = ['thank', 'unthank']

    @extend_schema(operation_id="thank_contributors",
                   request=inline_serializer("EmptyThank", fields={}),
                   responses={201: None,
                              400: None,
                              404: None},
                   parameters=[OpenApiParameter(name="video_id",
                                                description="Youtube Video ID",
                                                required=True,
                                                type=str),
                               OpenApiParameter(name="action",
                                                description="Set/unset",
                                                type=str,
                                                required=True,
                                                enum=thank_actions)])
    @action(methods=['PATCH'], detail=False, name="Thank contributors for the video")
    def thank_contributors(self, request):
        """Thank contributors for the video."""

        video = get_object_or_404(Video, video_id=request.query_params.get('video_id', ''))
        action = request.query_params.get('action', "")

        user = get_object_or_404(UserPreferences, user__username=request.user.username)

        if action == 'unthank':
            n_deleted, _ = VideoRatingThankYou.objects.filter(thanks_from=user,
                                                              video=video).delete()
            return Response({'status': 'deleted', 'n_deleted': n_deleted}, status=201)
        elif action == 'thank':
            qs = UserPreferences.objects.all()

            # only keeping people who rated the video
            qs = qs.annotate(n_video=Count(
                'expertrating', Q(expertrating__video_1=video) | Q(expertrating__video_2=video)))
            qs = qs.filter(n_video__gte=1)

            # and who are certified
            qs = UserInformation._annotate_is_certified(qs, prefix="user__userinformation__")
            qs = qs.filter(_is_certified=True)

            # removing yourself...
            qs = qs.exclude(id=user.id)

            contributors = qs.distinct()

            entries = [VideoRatingThankYou(thanks_from=user,
                                           video=video,
                                           thanks_to=contributor)
                       for contributor in contributors]
            VideoRatingThankYou.objects.bulk_create(entries, ignore_conflicts=True)
        else:
            return Response({'reason': f'Wrong action [{action}]'}, status=400)

        return Response({'status': 'success'}, status=201)

    @extend_schema(operation_id="set_rating_privacy",
                   request=inline_serializer("EmptySetPrivacy", {}),
                   responses={201: None,
                              400: None,
                              404: None},
                   parameters=[OpenApiParameter(name="video_id",
                                                description="Youtube Video ID",
                                                required=True,
                                                type=str),
                               OpenApiParameter(name="is_public",
                                                description="Should the rating be public",
                                                required=True,
                                                type=bool)])
    @action(methods=['PATCH'], detail=False, name="Set video rating privacy")
    def set_rating_privacy(self, request):
        """Set video rating privacy."""

        video = get_object_or_404(Video, video_id=request.query_params.get('video_id', ''))
        user = get_object_or_404(UserPreferences, user__username=request.user.username)

        obj, _ = VideoRatingPrivacy.objects.get_or_create(user=user, video=video)
        obj.is_public = request.query_params.get('is_public', 'true') == 'true'
        obj.save()

        return Response({'status': 'success',
                         'is_public': obj.is_public}, status=201)

    @extend_schema(operation_id="set_all_rating_privacy",
                   request=inline_serializer("EmptySetAllPrivacy", {}),
                   responses={201: None,
                              400: None,
                              404: None},
                   parameters=[OpenApiParameter(name="is_public",
                                                description="Should all ratings be public",
                                                required=True,
                                                type=bool)])
    @action(methods=['PATCH'], detail=False, name="Set all video rating privacy")
    def set_all_rating_privacy(self, request):
        """Set all video rating privacy."""

        user = get_object_or_404(UserPreferences, user__username=request.user.username)

        # videos rated by the user
        videos = Video.objects.filter(
            Q(expertrating_video_1__user=user) | Q(expertrating_video_2__user=user)).distinct()

        # creating privacy objects if they don't exist...
        if videos:
            VideoRatingPrivacy.objects.bulk_create(
                [VideoRatingPrivacy(user=user, video=v) for v in videos],
                ignore_conflicts=True)

        is_public = request.query_params.get('is_public', 'true') == 'true'

        VideoRatingPrivacy.objects.filter(user=user).update(is_public=is_public)

        # updating video properties
        update_user_username(user.user.username)

        return Response({'status': 'success'}, status=201)
Beispiel #26
0
class SurveyViewSet(
    mixins.RetrieveModelMixin,
    mixins.UpdateModelMixin,
    mixins.DestroyModelMixin,
    mixins.ListModelMixin,
    viewsets.GenericViewSet,
):
    serializer_class = SurveySerializer
    permission_classes = [CanWriteSurveyOrReadOnly]
    filterset_class = SurveyFilter

    def get_queryset(self):
        # if self.action value is identifier then queryset can be none since
        # that api can be accessed by anyone.
        # Not setting it to none will cause survey/identifier/<survey_identifier>
        # API to fail with 500 error code for non authenticated user
        if self.action == "identifier":
            return Survey.objects.none()
        current_user = self.request.user
        projects = read_allowed_project_for_user(current_user)
        return Survey.objects.filter(
            Q(project__in=projects) | Q(created_by=current_user)
        )

    @extend_schema(
        responses=inline_serializer(
            name="SurveyShareLinkResponseSerializer",
            fields={"shared_link_identifier": serializers.CharField()},
        )
    )
    @action(
        methods=["post"],
        detail=True,
        permission_classes=[CanWriteSurvey],
        serializer_class=serializers.Serializer,
    )
    def share_link(self, request, *args, **kwargs):
        survey = self.get_object()
        survey.is_shared_publicly = True
        if not survey.shared_link_identifier:
            shared_link_identifier = gen_random_string(10)
            while Survey.objects.filter(
                shared_link_identifier=shared_link_identifier
            ).first():
                shared_link_identifier = gen_random_string(10)
            survey.shared_link_identifier = shared_link_identifier
        else:
            shared_link_identifier = survey.shared_link_identifier
        survey.save()
        return Response({"shared_link_identifier": shared_link_identifier})

    @extend_schema(
        responses=inline_serializer(
            name="SurveyUpdateLinkResponseSerializer",
            fields={"shared_link_identifier": serializers.CharField()},
        )
    )
    @action(
        methods=["post"],
        detail=True,
        permission_classes=[CanWriteSurvey],
        serializer_class=serializers.Serializer,
    )
    def update_link(self, request, *args, **kwargs):
        survey = self.get_object()
        if survey.is_shared_publicly:
            shared_link_identifier = gen_random_string(10)
            while Survey.objects.filter(
                shared_link_identifier=shared_link_identifier
            ).first():
                shared_link_identifier = gen_random_string(10)
            survey.shared_link_identifier = shared_link_identifier
            survey.save()
            return Response({"shared_link_identifier": shared_link_identifier})
        else:
            return Response(
                {"error": _("Cannot update link of not shared survey")},
                status=status.HTTP_400_BAD_REQUEST,
            )

    @extend_schema(
        responses=inline_serializer(
            name="SurveyUnShareLinkResponseSerializer",
            fields={"detail": _("Successfully unshared survey")},
        )
    )
    @action(
        methods=["post"],
        detail=True,
        permission_classes=[CanWriteSurvey],
        serializer_class=serializers.Serializer,
    )
    def unshare_link(self, request, *args, **kwargs):
        survey = self.get_object()
        survey.is_shared_publicly = False
        survey.shared_link_identifier = None
        survey.save()
        return Response({"detail": _("Successfully unshared survey")})

    @action(
        methods=["get"],
        detail=False,
        permission_classes=[permissions.AllowAny],
        serializer_class=SharedSurveySerializer,
        url_path=r"identifier/(?P<shared_link_identifier>[^/.]+)",
    )
    def identifier(self, request, *args, **kwargs):
        identifier = kwargs.get("shared_link_identifier")
        survey = Survey.objects.filter(
            shared_link_identifier=identifier, is_shared_publicly=True
        ).first()
        if survey:
            serializer = self.get_serializer(survey)
            return Response(serializer.data)
        else:
            return Response(
                {"error": _("Identifier not found")}, status=status.HTTP_404_NOT_FOUND
            )

    @extend_schema(
        request=WritableSurveyAnswerSerializer(many=True),
        responses=inline_serializer(
            name="AddSurveyAnswerResponseSerializer",
            fields={
                "detail": serializers.CharField(
                    default=_("Successfully added survey answers")
                )
            },
        ),
    )
    @action(
        methods=["post"],
        detail=True,
        permission_classes=[CanWriteSurvey],
        serializer_class=WritableSurveyAnswerSerializer,
    )
    def add_answers(self, request, *args, **kwargs):
        survey = self.get_object()
        user = self.request.user
        serializer = self.get_serializer(data=request.data, many=True)
        if not serializer.is_valid():
            return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

        try:
            with transaction.atomic():
                for validated_datum in serializer.validated_data:
                    options = validated_datum.pop("options", None)
                    question = validated_datum.pop("question")
                    survey_answer, created = SurveyAnswer.objects.update_or_create(
                        survey=survey, question=question, defaults=validated_datum
                    )
                    if created:
                        survey_answer.created_by = user
                    else:
                        survey_answer.updated_by = user
                    survey_answer.save()
                    if options:
                        survey_answer.options.set(options)
        except Exception:
            return Response(
                {"error": _("Failed to add answers for survey")},
                status=status.HTTP_400_BAD_REQUEST,
            )
        return Response(
            {"detail": _("Successfully added survey answers")},
            status=status.HTTP_201_CREATED,
        )

    @extend_schema(
        request=SurveyResultSerializer(many=True),
        responses=inline_serializer(
            name="AddSurveyResultResponseSerializer",
            fields={
                "detail": serializers.CharField(
                    default=_("Successfully added survey results")
                )
            },
        ),
    )
    @action(
        methods=["post"],
        detail=True,
        permission_classes=[CanWriteSurvey],
        serializer_class=SurveyResultSerializer,
    )
    def add_results(self, request, *args, **kwargs):
        survey = self.get_object()
        user = self.request.user
        serializer = self.get_serializer(data=request.data, many=True)
        if not serializer.is_valid():
            return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

        try:
            with transaction.atomic():
                for validated_datum in serializer.validated_data:
                    statement = validated_datum.pop("statement")
                    module = validated_datum.pop("module")
                    question_group = validated_datum.pop("question_group", None)
                    survey_result, created = SurveyResult.objects.update_or_create(
                        survey=survey,
                        statement=statement,
                        module=module,
                        question_group=question_group,
                        defaults=validated_datum,
                    )
                    if created:
                        survey_result.created_by = user
                    else:
                        survey_result.updated_by = user
                    survey_result.save()
        except Exception:
            return Response(
                {"error": _("Failed to create survey result due to invalid data")},
                status=status.HTTP_400_BAD_REQUEST,
            )
        return Response(
            {"detail": _("Successfully added survey results")},
            status=status.HTTP_201_CREATED,
        )
Beispiel #27
0
from django.urls import reverse
from django_filters import rest_framework as filters
from drf_spectacular.utils import extend_schema, inline_serializer
from packageurl import PackageURL

from rest_framework import serializers, viewsets
from rest_framework.decorators import action
from rest_framework.response import Response
from vulnerabilities.models import Package
from vulnerabilities.models import Vulnerability
from vulnerabilities.models import VulnerabilityReference
from vulnerabilities.models import VulnerabilitySeverity

# This serializer is used for the bulk apis, to prevent wrong auto documentation
# TODO: Fix the swagger documentation for bulk apis
placeholder_serializer = inline_serializer(name="Placeholder", fields={})


class VulnerabilitySeveritySerializer(serializers.ModelSerializer):
    class Meta:
        model = VulnerabilitySeverity
        fields = ["value", "scoring_system"]


class VulnerabilityReferenceSerializer(serializers.ModelSerializer):
    scores = VulnerabilitySeveritySerializer(many=True)

    class Meta:
        model = VulnerabilityReference
        fields = ["reference_id", "url", "scores"]
Beispiel #28
0
class UserSelfSerializer(ModelSerializer):
    """User Serializer for information a user can retrieve about themselves and
    update about themselves"""

    is_superuser = BooleanField(read_only=True)
    avatar = CharField(read_only=True)
    groups = SerializerMethodField()
    uid = CharField(read_only=True)
    settings = DictField(source="attributes.settings", default=dict)

    @extend_schema_field(
        ListSerializer(child=inline_serializer(
            "UserSelfGroups",
            {
                "name": CharField(read_only=True),
                "pk": CharField(read_only=True)
            },
        )))
    def get_groups(self, _: User):
        """Return only the group names a user is member of"""
        for group in self.instance.ak_groups.all():
            yield {
                "name": group.name,
                "pk": group.pk,
            }

    def validate_email(self, email: str):
        """Check if the user is allowed to change their email"""
        if self.instance.group_attributes().get(
                USER_ATTRIBUTE_CHANGE_EMAIL,
                CONFIG.y_bool("default_user_change_email", True)):
            return email
        if email != self.instance.email:
            raise ValidationError("Not allowed to change email.")
        return email

    def validate_name(self, name: str):
        """Check if the user is allowed to change their name"""
        if self.instance.group_attributes().get(
                USER_ATTRIBUTE_CHANGE_NAME,
                CONFIG.y_bool("default_user_change_name", True)):
            return name
        if name != self.instance.name:
            raise ValidationError("Not allowed to change name.")
        return name

    def validate_username(self, username: str):
        """Check if the user is allowed to change their username"""
        if self.instance.group_attributes().get(
                USER_ATTRIBUTE_CHANGE_USERNAME,
                CONFIG.y_bool("default_user_change_username", True)):
            return username
        if username != self.instance.username:
            raise ValidationError("Not allowed to change username.")
        return username

    def save(self, **kwargs):
        if self.instance:
            attributes: dict = self.instance.attributes
            attributes.update(self.validated_data.get("attributes", {}))
            self.validated_data["attributes"] = attributes
        return super().save(**kwargs)

    class Meta:

        model = User
        fields = [
            "pk",
            "username",
            "name",
            "is_active",
            "is_superuser",
            "groups",
            "email",
            "avatar",
            "uid",
            "settings",
        ]
        extra_kwargs = {
            "is_active": {
                "read_only": True
            },
            "name": {
                "allow_blank": True
            },
        }
Beispiel #29
0
class OrganizationSCIMMemberIndex(SCIMEndpoint):
    permission_classes = (OrganizationSCIMMemberPermission, )
    public = {"GET", "POST"}

    @extend_schema(
        operation_id="List an Organization's Members",
        parameters=[GLOBAL_PARAMS.ORG_SLUG, SCIMQueryParamSerializer],
        request=None,
        responses={
            200:
            scim_response_envelope("SCIMMemberIndexResponse",
                                   OrganizationMemberSCIMSerializerResponse),
            401:
            RESPONSE_UNAUTHORIZED,
            403:
            RESPONSE_FORBIDDEN,
            404:
            RESPONSE_NOTFOUND,
        },
        examples=[  # TODO: see if this can go on serializer object instead
            OpenApiExample(
                "List an Organization's Members",
                value={
                    "schemas":
                    ["urn:ietf:params:scim:api:messages:2.0:ListResponse"],
                    "totalResults":
                    1,
                    "startIndex":
                    1,
                    "itemsPerPage":
                    1,
                    "Resources": [{
                        "schemas":
                        ["urn:ietf:params:scim:schemas:core:2.0:User"],
                        "id":
                        "102",
                        "userName":
                        "******",
                        "emails": [{
                            "primary": True,
                            "value": "*****@*****.**",
                            "type": "work"
                        }],
                        "name": {
                            "familyName": "N/A",
                            "givenName": "N/A"
                        },
                        "active":
                        True,
                        "meta": {
                            "resourceType": "User"
                        },
                    }],
                },
                status_codes=["200"],
            ),
        ],
    )
    def get(self, request: Request, organization) -> Response:
        """
        Returns a paginated list of members bound to a organization with a SCIM Users GET Request.
        """
        # note that SCIM doesn't care about changing results as they're queried

        query_params = self.get_query_parameters(request)

        queryset = (OrganizationMember.objects.filter(
            Q(invite_status=InviteStatus.APPROVED.value),
            Q(user__is_active=True) | Q(user__isnull=True),
            organization=organization,
        ).select_related("user").order_by("email", "user__email"))
        if query_params["filter"]:
            queryset = queryset.filter(
                Q(email__iexact=query_params["filter"])
                | Q(user__email__iexact=query_params["filter"])
            )  # not including secondary email vals (dups, etc.)

        def data_fn(offset, limit):
            return list(queryset[offset:offset + limit])

        def on_results(results):
            results = serialize(
                results,
                None,
                _scim_member_serializer_with_expansion(organization),
            )
            return self.list_api_format(results, queryset.count(),
                                        query_params["start_index"])

        return self.paginate(
            request=request,
            on_results=on_results,
            paginator=GenericOffsetPaginator(data_fn=data_fn),
            default_per_page=query_params["count"],
            queryset=queryset,
            cursor_cls=SCIMCursor,
        )

    @extend_schema(
        operation_id="Provision a New Organization Member",
        parameters=[GLOBAL_PARAMS.ORG_SLUG],
        request=inline_serializer(
            "SCIMMemberProvision",
            fields={"userName": serializers.EmailField()}),
        responses={
            201: OrganizationMemberSCIMSerializer,
            401: RESPONSE_UNAUTHORIZED,
            403: RESPONSE_FORBIDDEN,
            404: RESPONSE_NOTFOUND,
        },
        examples=[  # TODO: see if this can go on serializer object instead
            OpenApiExample(
                "Provision new member",
                response_only=True,
                value={
                    "schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
                    "id":
                    "242",
                    "userName":
                    "******",
                    "emails": [{
                        "primary": True,
                        "value": "*****@*****.**",
                        "type": "work"
                    }],
                    "active":
                    True,
                    "name": {
                        "familyName": "N/A",
                        "givenName": "N/A"
                    },
                    "meta": {
                        "resourceType": "User"
                    },
                },
                status_codes=["201"],
            ),
        ],
    )
    def post(self, request: Request, organization) -> Response:
        """
        Create a new Organization Member via a SCIM Users POST Request.
        - `userName` should be set to the SAML field used for email, and active should be set to `true`.
        - Sentry's SCIM API doesn't currently support setting users to inactive,
        and the member will be deleted if inactive is set to `false`.
        - The API also does not support setting secondary emails.
        """

        serializer = OrganizationMemberSerializer(
            data={
                "email": request.data.get("userName"),
                "role": roles.get(organization.default_role).id,
            },
            context={
                "organization": organization,
                "allowed_roles": [roles.get(organization.default_role)],
                "allow_existing_invite_request": True,
            },
        )

        if not serializer.is_valid():
            if "email" in serializer.errors and any(
                ("is already a member" in error)
                    for error in serializer.errors["email"]):
                # we include conflict logic in the serializer, check to see if that was
                # our error and if so, return a 409 so the scim IDP knows how to handle
                raise ConflictError(detail=SCIM_409_USER_EXISTS)
            return Response(serializer.errors, status=400)

        result = serializer.validated_data
        with transaction.atomic():
            member = OrganizationMember(
                organization=organization,
                email=result["email"],
                role=result["role"],
                inviter=request.user,
            )

            # TODO: are invite tokens needed for SAML orgs?
            if settings.SENTRY_ENABLE_INVITES:
                member.token = member.generate_token()
            member.save()

        self.create_audit_entry(
            request=request,
            organization_id=organization.id,
            target_object=member.id,
            data=member.get_audit_log_data(),
            event=AuditLogEntryEvent.MEMBER_INVITE if
            settings.SENTRY_ENABLE_INVITES else AuditLogEntryEvent.MEMBER_ADD,
        )

        if settings.SENTRY_ENABLE_INVITES and result.get("sendInvite"):
            member.send_invite_email()
            member_invited.send_robust(
                member=member,
                user=request.user,
                sender=self,
                referrer=request.data.get("referrer"),
            )

        context = serialize(
            member,
            serializer=_scim_member_serializer_with_expansion(organization),
        )
        return Response(context, status=201)
Beispiel #30
0
class UserViewSet(UsedByMixin, ModelViewSet):
    """User Viewset"""

    queryset = User.objects.none()
    ordering = ["username"]
    serializer_class = UserSerializer
    search_fields = ["username", "name", "is_active", "email"]
    filterset_class = UsersFilter

    def get_queryset(self):  # pragma: no cover
        return User.objects.all().exclude(pk=get_anonymous_user().pk)

    def _create_recovery_link(self) -> tuple[Optional[str], Optional[Token]]:
        """Create a recovery link (when the current tenant has a recovery flow set),
        that can either be shown to an admin or sent to the user directly"""
        tenant: Tenant = self.request._request.tenant
        # Check that there is a recovery flow, if not return an error
        flow = tenant.flow_recovery
        if not flow:
            LOGGER.debug("No recovery flow set")
            return None, None
        user: User = self.get_object()
        token, __ = Token.objects.get_or_create(
            identifier=f"{user.uid}-password-reset",
            user=user,
            intent=TokenIntents.INTENT_RECOVERY,
        )
        querystring = urlencode({"token": token.key})
        link = self.request.build_absolute_uri(
            reverse_lazy("authentik_core:if-flow",
                         kwargs={"flow_slug": flow.slug}) + f"?{querystring}")
        return link, token

    @permission_required(
        None, ["authentik_core.add_user", "authentik_core.add_token"])
    @extend_schema(
        request=inline_serializer(
            "UserServiceAccountSerializer",
            {
                "name": CharField(required=True),
                "create_group": BooleanField(default=False),
            },
        ),
        responses={
            200:
            inline_serializer(
                "UserServiceAccountResponse",
                {
                    "username": CharField(required=True),
                    "token": CharField(required=True),
                },
            )
        },
    )
    @action(detail=False,
            methods=["POST"],
            pagination_class=None,
            filter_backends=[])
    def service_account(self, request: Request) -> Response:
        """Create a new user account that is marked as a service account"""
        username = request.data.get("name")
        create_group = request.data.get("create_group", False)
        with atomic():
            try:
                user = User.objects.create(
                    username=username,
                    name=username,
                    attributes={
                        USER_ATTRIBUTE_SA: True,
                        USER_ATTRIBUTE_TOKEN_EXPIRING: False
                    },
                )
                if create_group and self.request.user.has_perm(
                        "authentik_core.add_group"):
                    group = Group.objects.create(name=username, )
                    group.users.add(user)
                token = Token.objects.create(
                    identifier=slugify(f"service-account-{username}-password"),
                    intent=TokenIntents.INTENT_APP_PASSWORD,
                    user=user,
                    expires=now() + timedelta(days=360),
                )
                return Response({
                    "username": user.username,
                    "token": token.key
                })
            except (IntegrityError) as exc:
                return Response(data={"non_field_errors": [str(exc)]},
                                status=400)

    @extend_schema(responses={200: SessionUserSerializer(many=False)})
    @action(detail=False, pagination_class=None, filter_backends=[])
    # pylint: disable=invalid-name
    def me(self, request: Request) -> Response:
        """Get information about current user"""
        serializer = SessionUserSerializer(
            data={"user": UserSelfSerializer(instance=request.user).data})
        if SESSION_IMPERSONATE_USER in request._request.session:
            serializer.initial_data["original"] = UserSelfSerializer(
                instance=request._request.
                session[SESSION_IMPERSONATE_ORIGINAL_USER]).data
        return Response(serializer.initial_data)

    @permission_required("authentik_core.reset_user_password")
    @extend_schema(
        request=inline_serializer(
            "UserPasswordSetSerializer",
            {
                "password": CharField(required=True),
            },
        ),
        responses={
            204: "",
            400: "",
        },
    )
    @action(detail=True, methods=["POST"])
    # pylint: disable=invalid-name, unused-argument
    def set_password(self, request: Request, pk: int) -> Response:
        """Set password for user"""
        user: User = self.get_object()
        try:
            user.set_password(request.data.get("password"))
            user.save()
        except (ValidationError, IntegrityError) as exc:
            LOGGER.debug("Failed to set password", exc=exc)
            return Response(status=400)
        if user.pk == request.user.pk and SESSION_IMPERSONATE_USER not in self.request.session:
            LOGGER.debug("Updating session hash after password change")
            update_session_auth_hash(self.request, user)
        return Response(status=204)

    @extend_schema(request=UserSelfSerializer,
                   responses={200: SessionUserSerializer(many=False)})
    @action(
        methods=["PUT"],
        detail=False,
        pagination_class=None,
        filter_backends=[],
        permission_classes=[IsAuthenticated],
    )
    def update_self(self, request: Request) -> Response:
        """Allow users to change information on their own profile"""
        data = UserSelfSerializer(
            instance=User.objects.get(pk=request.user.pk), data=request.data)
        if not data.is_valid():
            return Response(data.errors, status=400)
        new_user = data.save()
        # If we're impersonating, we need to update that user object
        # since it caches the full object
        if SESSION_IMPERSONATE_USER in request.session:
            request.session[SESSION_IMPERSONATE_USER] = new_user
        return Response({"user": data.data})

    @permission_required("authentik_core.view_user",
                         ["authentik_events.view_event"])
    @extend_schema(responses={200: UserMetricsSerializer(many=False)})
    @action(detail=True, pagination_class=None, filter_backends=[])
    # pylint: disable=invalid-name, unused-argument
    def metrics(self, request: Request, pk: int) -> Response:
        """User metrics per 1h"""
        user: User = self.get_object()
        serializer = UserMetricsSerializer(True)
        serializer.context["user"] = user
        return Response(serializer.data)

    @permission_required("authentik_core.reset_user_password")
    @extend_schema(
        responses={
            "200": LinkSerializer(many=False),
            "404": LinkSerializer(many=False),
        }, )
    @action(detail=True, pagination_class=None, filter_backends=[])
    # pylint: disable=invalid-name, unused-argument
    def recovery(self, request: Request, pk: int) -> Response:
        """Create a temporary link that a user can use to recover their accounts"""
        link, _ = self._create_recovery_link()
        if not link:
            LOGGER.debug("Couldn't create token")
            return Response({"link": ""}, status=404)
        return Response({"link": link})

    @permission_required("authentik_core.reset_user_password")
    @extend_schema(
        parameters=[
            OpenApiParameter(
                name="email_stage",
                location=OpenApiParameter.QUERY,
                type=OpenApiTypes.STR,
                required=True,
            )
        ],
        responses={
            "204": Serializer(),
            "404": Serializer(),
        },
    )
    @action(detail=True, pagination_class=None, filter_backends=[])
    # pylint: disable=invalid-name, unused-argument
    def recovery_email(self, request: Request, pk: int) -> Response:
        """Create a temporary link that a user can use to recover their accounts"""
        for_user = self.get_object()
        if for_user.email == "":
            LOGGER.debug("User doesn't have an email address")
            return Response(status=404)
        link, token = self._create_recovery_link()
        if not link:
            LOGGER.debug("Couldn't create token")
            return Response(status=404)
        # Lookup the email stage to assure the current user can access it
        stages = get_objects_for_user(
            request.user, "authentik_stages_email.view_emailstage").filter(
                pk=request.query_params.get("email_stage"))
        if not stages.exists():
            LOGGER.debug("Email stage does not exist/user has no permissions")
            return Response(status=404)
        email_stage: EmailStage = stages.first()
        message = TemplateEmailMessage(
            subject=_(email_stage.subject),
            template_name=email_stage.template,
            to=[for_user.email],
            template_context={
                "url": link,
                "user": for_user,
                "expires": token.expires,
            },
        )
        send_mails(email_stage, message)
        return Response(status=204)

    def _filter_queryset_for_list(self, queryset: QuerySet) -> QuerySet:
        """Custom filter_queryset method which ignores guardian, but still supports sorting"""
        for backend in list(self.filter_backends):
            if backend == ObjectPermissionsFilter:
                continue
            queryset = backend().filter_queryset(self.request, queryset, self)
        return queryset

    def filter_queryset(self, queryset):
        if self.request.user.has_perm("authentik_core.view_user"):
            return self._filter_queryset_for_list(queryset)
        return super().filter_queryset(queryset)