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
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)
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))})
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)
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, )
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, )
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)
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))
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)
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})
"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(),
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()})
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)
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})) })
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")})
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(), }, )
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)
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)
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")})
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)
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, )
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)
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, )
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"]
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 }, }
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)
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)