예제 #1
0
 class Fixed(self.target_class):
     @extend_schema(
         operation_id="reset_password_confirm",
         request={"application/json": PasswordResetConfirmSerializer},
         responses={
             status.HTTP_200_OK: OpenApiResponse(
                 DetailResponseSerializer,
                 description="Success",
                 examples=[
                     OpenApiExample(
                         "Successful password reset",
                         value={"detail": "Password has been reset with the new password."},
                     )
                 ],
             ),
             status.HTTP_400_BAD_REQUEST: OpenApiResponse(
                 PasswordResetConfirmSerializer,
                 description="Invalid input",
                 examples=[
                     OpenApiExample(
                         "Invalid uid",
                         value={"uid": "Invalid value"},
                         status_codes=[f"{status.HTTP_400_BAD_REQUEST}"],
                     )
                 ],
             ),
         },
         tags=["auth"],
     )
     def post(self, request, *args, **kwargs):
         pass
예제 #2
0
class AuthenticatorDuoStageViewSet(ModelViewSet):
    """AuthenticatorDuoStage Viewset"""

    queryset = AuthenticatorDuoStage.objects.all()
    serializer_class = AuthenticatorDuoStageSerializer

    @extend_schema(
        request=OpenApiTypes.NONE,
        responses={
            204: OpenApiResponse(description="Enrollment successful"),
            420: OpenApiResponse(description="Enrollment pending/failed"),
        },
    )
    @action(methods=["POST"], detail=True, permission_classes=[])
    # pylint: disable=invalid-name,unused-argument
    def enrollment_status(self, request: Request, pk: str) -> Response:
        """Check enrollment status of user details in current session"""
        stage: AuthenticatorDuoStage = self.get_object()
        client = stage.client
        user_id = self.request.session.get(SESSION_KEY_DUO_USER_ID)
        activation_code = self.request.session.get(
            SESSION_KEY_DUO_ACTIVATION_CODE)
        status = client.enroll_status(user_id, activation_code)
        if status == "success":
            return Response(status=204)
        return Response(status=420)
예제 #3
0
class TaskViewSet(ViewSet):
    """Read-only view set that returns all background tasks"""

    permission_classes = [IsAdminUser]
    serializer_class = TaskSerializer

    @extend_schema(
        responses={
            200: TaskSerializer(many=False),
            404: OpenApiResponse(description="Task not found"),
        }
    )
    # pylint: disable=invalid-name
    def retrieve(self, request: Request, pk=None) -> Response:
        """Get a single system task"""
        task = TaskInfo.by_name(pk)
        if not task:
            raise Http404
        return Response(TaskSerializer(task, many=False).data)

    @extend_schema(responses={200: TaskSerializer(many=True)})
    def list(self, request: Request) -> Response:
        """List system tasks"""
        tasks = sorted(TaskInfo.all().values(), key=lambda task: task.task_name)
        return Response(TaskSerializer(tasks, many=True).data)

    @extend_schema(
        request=OpenApiTypes.NONE,
        responses={
            204: OpenApiResponse(description="Task retried successfully"),
            404: OpenApiResponse(description="Task not found"),
            500: OpenApiResponse(description="Failed to retry task"),
        },
    )
    @action(detail=True, methods=["post"])
    # pylint: disable=invalid-name
    def retry(self, request: Request, pk=None) -> Response:
        """Retry task"""
        task = TaskInfo.by_name(pk)
        if not task:
            raise Http404
        try:
            task_module = import_module(task.task_call_module)
            task_func = getattr(task_module, task.task_call_func)
            task_func.delay(*task.task_call_args, **task.task_call_kwargs)
            messages.success(
                self.request,
                _(
                    "Successfully re-scheduled Task %(name)s!"
                    % {"name": task.task_name}
                ),
            )
            return Response(status=204)
        except ImportError:  # pragma: no cover
            # if we get an import error, the module path has probably changed
            task.delete()
            return Response(status=500)
예제 #4
0
class BGPGroupViewSet(ModelViewSet):
    queryset = BGPGroup.objects.all()
    serializer_class = BGPGroupSerializer
    filterset_class = BGPGroupFilterSet

    @extend_schema(
        operation_id="peering_bgp_groups_poll_bgp_sessions",
        request=None,
        responses={
            202:
            OpenApiResponse(
                response=JobResultSerializer(many=True),
                description="Jobs scheduled to poll BGP sessions.",
            ),
            403:
            OpenApiResponse(
                response=OpenApiTypes.NONE,
                description=
                "The user does not have the permission to poll BGP sessions state.",
            ),
            404:
            OpenApiResponse(
                response=OpenApiTypes.OBJECT,
                description="The BGP group does not exist.",
            ),
        },
    )
    @action(detail=True, methods=["post"], url_path="poll-bgp-sessions")
    def poll_bgp_sessions(self, request, pk=None):
        # Check user permission first
        if not request.user.has_perm("peering.change_directpeeringsession"):
            return Response(status=status.HTTP_403_FORBIDDEN)

        job_results = []
        for router in self.get_object().get_routers():
            job_results.append(
                JobResult.enqueue_job(
                    poll_bgp_sessions,
                    "peering.router.poll_bgp_sessions",
                    Router,
                    request.user,
                    router,
                ))
        return Response(
            data=[
                JobResultSerializer(instance=job_result,
                                    context={
                                        "request": request
                                    }).data for job_result in job_results
            ],
            status=status.HTTP_202_ACCEPTED,
        )
예제 #5
0
 class Fixed(self.target_class):
     @extend_schema(
         operation_id="register",
         request={"application/json": RegisterSerializer},
         responses={
             status.HTTP_200_OK: OpenApiResponse(
                 response_serializer,
                 description="successful operation",
                 examples=[
                     OpenApiExample(
                         "Successful registration",
                         value={"detail": "Verification e-mail sent."},
                         status_codes=[f"{status.HTTP_200_OK}"],
                     )
                 ],
             ),
             status.HTTP_400_BAD_REQUEST: OpenApiResponse(
                 RegisterSerializer,
                 description="Invalid input",
                 examples=[
                     OpenApiExample(
                         "Invalid email",
                         value={"email": "Enter a valid email address."},
                         status_codes=[f"{status.HTTP_400_BAD_REQUEST}"],
                     ),
                     OpenApiExample(
                         "Invalid password",
                         value={"password2": "This password is too common."},
                         status_codes=[f"{status.HTTP_400_BAD_REQUEST}"],
                     ),
                 ],
             ),
             status.HTTP_401_UNAUTHORIZED: OpenApiResponse(
                 DetailResponseSerializer,
                 description="Unauthorized",
                 examples=[
                     OpenApiExample(
                         "Invalid auth header",
                         description="Attempting to register with the Authorization header already set",
                         value={"detail": "Invalid token."},
                         status_codes=[f"{status.HTTP_401_UNAUTHORIZED}"],
                     )
                 ],
             ),
         },
         tags=["auth"],
     )
     def post(self, request, *args, **kwargs):
         pass
예제 #6
0
class RestrictionsViewSet(viewsets.ViewSet):
    serializer_class = None
    permission_classes = [AllowAny]
    authentication_classes = []
    iam_organization_field = None

    # To get nice documentation about ServerViewSet actions it is necessary
    # to implement the method. By default, ViewSet doesn't provide it.
    def get_serializer(self, *args, **kwargs):
        pass

    @staticmethod
    @extend_schema(summary='Method provides user agreements that the user must accept to register',
                  responses={'200': UserAgreementSerializer})
    @action(detail=False, methods=['GET'], serializer_class=UserAgreementSerializer, url_path='user-agreements')
    def user_agreements(request):
        user_agreements = settings.RESTRICTIONS['user_agreements']
        serializer = UserAgreementSerializer(data=user_agreements, many=True)
        serializer.is_valid(raise_exception=True)
        return Response(data=serializer.data)

    @staticmethod
    @extend_schema(summary='Method provides CVAT terms of use',
                responses={'200': OpenApiResponse(description='CVAT terms of use')})
    @action(detail=False, methods=['GET'], renderer_classes=(TemplateHTMLRenderer,),
        url_path='terms-of-use')
    def terms_of_use(request):
        return Response(template_name='restrictions/terms_of_use.html')
예제 #7
0
class NotificationTransportViewSet(ModelViewSet):
    """NotificationTransport Viewset"""

    queryset = NotificationTransport.objects.all()
    serializer_class = NotificationTransportSerializer

    @permission_required("authentik_events.change_notificationtransport")
    @extend_schema(
        responses={
            200: NotificationTransportTestSerializer(many=False),
            500: OpenApiResponse(description="Failed to test transport"),
        },
        request=OpenApiTypes.NONE,
    )
    @action(detail=True,
            pagination_class=None,
            filter_backends=[],
            methods=["post"])
    # pylint: disable=invalid-name, unused-argument
    def test(self, request: Request, pk=None) -> Response:
        """Send example notification using selected transport. Requires
        Modify permissions."""
        transport: NotificationTransport = self.get_object()
        notification = Notification(
            severity=NotificationSeverity.NOTICE,
            body=f"Test Notification from transport {transport.name}",
            user=request.user,
        )
        try:
            response = NotificationTransportTestSerializer(
                data={"messages": transport.send(notification)})
            response.is_valid()
            return Response(response.data)
        except NotificationTransportError as exc:
            return Response(str(exc.__cause__ or None), status=500)
예제 #8
0
class IXAPIViewSet(ModelViewSet):
    queryset = IXAPI.objects.all()
    serializer_class = IXAPISerializer

    @extend_schema(
        operation_id="extras_ix_api_accounts",
        request=IXAPICustomerSerializer,
        responses={
            200:
            OpenApiResponse(
                response=OpenApiTypes.OBJECT,
                description="The list of accounts returned by IX-API.",
            )
        },
    )
    @action(detail=False, methods=["get"], url_path="accounts")
    def accounts(self, request, pk=None):
        # Make sure request is valid
        serializer = IXAPICustomerSerializer(data=request.query_params)
        serializer.is_valid(raise_exception=True)

        # Query IX-API with given parameters
        api_url = serializer.validated_data.get("url")
        c = Client(
            ixapi_url=api_url,
            ixapi_key=serializer.validated_data.get("api_key"),
            ixapi_secret=serializer.validated_data.get("api_secret"),
        )
        c.auth()
        _, accounts = c.get("accounts" if "v1" not in api_url else "customers")

        return Response(data=accounts)
예제 #9
0
 class Fixed(self.target_class):
     @extend_schema(
         operation_id="reset_password",
         request={"application/json": PasswordResetSerializer},
         responses={
             status.HTTP_200_OK: OpenApiResponse(
                 DetailResponseSerializer,
                 description="Success",
                 examples=[
                     OpenApiExample(
                         "Success",
                         value={"detail": "Password reset e-mail has been sent."},
                         status_codes=[f"{status.HTTP_200_OK}"],
                     )
                 ],
             ),
             status.HTTP_400_BAD_REQUEST: OpenApiResponse(
                 PasswordResetSerializer,
                 description="Invalid input",
                 examples=[
                     OpenApiExample(
                         "User with email doesn't exist",
                         value={"email": "The e-mail address is not assigned to any user account"},
                         status_codes=[f"{status.HTTP_400_BAD_REQUEST}"],
                     )
                 ],
             ),
             status.HTTP_401_UNAUTHORIZED: OpenApiResponse(
                 DetailResponseSerializer,
                 description="Unauthorized",
                 examples=[
                     OpenApiExample(
                         "Invalid auth header",
                         description="Attempting to reset password with the Authorization header already set",
                         value={"detail": "Invalid token."},
                         status_codes=[f"{status.HTTP_401_UNAUTHORIZED}"],
                     )
                 ],
             ),
         },
         tags=["auth"],
     )
     def post(self, request, *args, **kwargs):
         pass
예제 #10
0
 class Fixed(self.target_class):
     @extend_schema(
         operation_id="login",
         request={"application/json": LoginSerializer},
         responses={
             status.HTTP_200_OK: OpenApiResponse(
                 get_token_serializer_class(),
                 description="Successful login",
                 examples=[
                     OpenApiExample(
                         "Success",
                         value={"key": "491484b928d4e497ef3359a789af8ac204fc96db"},
                         status_codes=[f"{status.HTTP_200_OK}"],
                     )
                 ],
             ),
             status.HTTP_400_BAD_REQUEST: OpenApiResponse(
                 NonFieldErrorResponseSerializer,
                 description="Invalid input",
                 examples=[
                     OpenApiExample(
                         "Invalid credentials",
                         value={"non_field_errors": "Unable to log in with provided credentials"},
                         status_codes=[f"{status.HTTP_400_BAD_REQUEST}"],
                     )
                 ],
             ),
             status.HTTP_401_UNAUTHORIZED: OpenApiResponse(
                 DetailResponseSerializer,
                 description="Unauthorized",
                 examples=[
                     OpenApiExample(
                         "Invalid auth header",
                         description="Attempting to login with the Authorization header already set",
                         value={"detail": "Invalid Token."},
                         status_codes=[f"{status.HTTP_401_UNAUTHORIZED}"],
                     )
                 ],
             ),
         },
         tags=["auth"],
     )
     def post(self, request, *args, **kwargs):
         pass
예제 #11
0
 class Fixed(self.target_class):
     @extend_schema(
         operation_id="change_password",
         request={"application/json": PasswordChangeSerializer},
         responses={
             status.HTTP_200_OK: OpenApiResponse(
                 DetailResponseSerializer,
                 description="Success",
                 examples=[
                     OpenApiExample(
                         "Successful password change",
                         value={"detail": "New password has been saved."},
                         status_codes=[f"{status.HTTP_200_OK}"],
                     )
                 ],
             ),
             status.HTTP_400_BAD_REQUEST: OpenApiResponse(
                 PasswordChangeSerializer,
                 description="Invalid input",
                 examples=[
                     OpenApiExample(
                         "Password too common",
                         value={"new_password2": "This password is too common."},
                         status_codes=[f"{status.HTTP_400_BAD_REQUEST}"],
                     )
                 ],
             ),
             status.HTTP_401_UNAUTHORIZED: OpenApiResponse(
                 DetailResponseSerializer,
                 description="Unauthorized",
                 examples=[
                     OpenApiExample(
                         "No token",
                         description="Invalid or missing Authorization header",
                         value={"detail": "Authentication credentials were not provided."},
                         status_codes=[f"{status.HTTP_401_UNAUTHORIZED}"],
                     )
                 ],
             ),
         },
         tags=["auth"],
     )
     def post(self, request, *args, **kwargs):
         pass
예제 #12
0
 class Fixed(self.target_class):
     @extend_schema(
         operation_id="verify_email",
         request={"application/json": VerifyEmailSerializer},
         responses={
             status.HTTP_200_OK: OpenApiResponse(
                 DetailResponseSerializer,
                 description="Success",
                 examples=[OpenApiExample("Successful email verification", value={"detail": "ok"})],
             ),
             status.HTTP_404_NOT_FOUND: OpenApiResponse(
                 DetailResponseSerializer,
                 description="Not found",
                 examples=[OpenApiExample("Invalid key", value={"detail": "Not found."})],
             ),
         },
         tags=["auth"],
     )
     def post(self, request, *args, **kwargs):
         pass
예제 #13
0
class TokenViewSet(UsedByMixin, ModelViewSet):
    """Token Viewset"""

    lookup_field = "identifier"
    queryset = Token.objects.all()
    serializer_class = TokenSerializer
    search_fields = [
        "identifier",
        "intent",
        "user__username",
        "description",
    ]
    filterset_fields = [
        "identifier",
        "intent",
        "user__username",
        "description",
        "expires",
        "expiring",
        "managed",
    ]
    ordering = ["identifier", "expires"]
    permission_classes = [OwnerSuperuserPermissions]
    filter_backends = [DjangoFilterBackend, OrderingFilter, SearchFilter]

    def get_queryset(self):
        user = self.request.user if self.request else get_anonymous_user()
        if user.is_superuser:
            return super().get_queryset()
        return super().get_queryset().filter(user=user.pk)

    def perform_create(self, serializer: TokenSerializer):
        if not self.request.user.is_superuser:
            return serializer.save(
                user=self.request.user,
                expiring=self.request.user.attributes.get(
                    USER_ATTRIBUTE_TOKEN_EXPIRING, True),
            )
        return super().perform_create(serializer)

    @permission_required("authentik_core.view_token_key")
    @extend_schema(
        responses={
            200: TokenViewSerializer(many=False),
            404: OpenApiResponse(description="Token not found or expired"),
        })
    @action(detail=True, pagination_class=None, filter_backends=[])
    # pylint: disable=unused-argument
    def view_key(self, request: Request, identifier: str) -> Response:
        """Return token key and log access"""
        token: Token = self.get_object()
        Event.new(EventAction.SECRET_VIEW,
                  secret=token).from_http(request)  # noqa # nosec
        return Response(TokenViewSerializer({"key": token.key}).data)
예제 #14
0
class FlowInspectorView(APIView):
    """Flow inspector API"""

    permission_classes = [IsAdminUser]

    flow: Flow
    _logger: BoundLogger

    def setup(self, request: HttpRequest, flow_slug: str):
        super().setup(request, flow_slug=flow_slug)
        self.flow = get_object_or_404(Flow.objects.select_related(), slug=flow_slug)
        self._logger = get_logger().bind(flow_slug=flow_slug)

    # pylint: disable=unused-argument, too-many-return-statements
    def dispatch(self, request: HttpRequest, flow_slug: str) -> HttpResponse:
        if SESSION_KEY_HISTORY not in self.request.session:
            return HttpResponse(status=400)
        return super().dispatch(request, flow_slug=flow_slug)

    @extend_schema(
        responses={
            200: FlowInspectionSerializer(),
            400: OpenApiResponse(description="No flow plan in session."),
        },
        request=OpenApiTypes.NONE,
        operation_id="flows_inspector_get",
    )
    def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
        """Get current flow state and record it"""
        plans = []
        for plan in request.session[SESSION_KEY_HISTORY]:
            plan_serializer = FlowInspectorPlanSerializer(
                instance=plan, context={"request": request}
            )
            plans.append(plan_serializer.data)
        is_completed = False
        if SESSION_KEY_PLAN in request.session:
            current_plan: FlowPlan = request.session[SESSION_KEY_PLAN]
        else:
            try:
                current_plan = request.session[SESSION_KEY_HISTORY][-1]
            except IndexError:
                return Response(status=400)
            is_completed = True
        current_serializer = FlowInspectorPlanSerializer(
            instance=current_plan, context={"request": request}
        )
        response = {
            "plans": plans,
            "current_plan": current_serializer.data,
            "is_completed": is_completed,
        }
        return Response(response)
예제 #15
0
class OAuth2ProviderViewSet(ModelViewSet):
    """OAuth2Provider Viewset"""

    queryset = OAuth2Provider.objects.all()
    serializer_class = OAuth2ProviderSerializer

    @extend_schema(
        responses={
            200:
            OAuth2ProviderSetupURLs,
            404:
            OpenApiResponse(
                description="Provider has no application assigned"),
        })
    @action(methods=["GET"], detail=True)
    # pylint: disable=invalid-name
    def setup_urls(self, request: Request, pk: int) -> str:
        """Get Providers setup URLs"""
        provider = get_object_or_404(OAuth2Provider, pk=pk)
        data = {
            "issuer":
            provider.get_issuer(request),
            "authorize":
            request.build_absolute_uri(
                reverse("authentik_providers_oauth2:authorize", )),
            "token":
            request.build_absolute_uri(
                reverse("authentik_providers_oauth2:token", )),
            "user_info":
            request.build_absolute_uri(
                reverse("authentik_providers_oauth2:userinfo", )),
            "provider_info":
            None,
            "logout":
            None,
        }
        try:
            data["provider_info"] = request.build_absolute_uri(
                reverse(
                    "authentik_providers_oauth2:provider-info",
                    kwargs={"application_slug": provider.application.slug},
                ))
            data["logout"] = request.build_absolute_uri(
                reverse(
                    "authentik_providers_oauth2:end-session",
                    kwargs={"application_slug": provider.application.slug},
                ))
        except Provider.application.RelatedObjectDoesNotExist:  # pylint: disable=no-member
            pass
        return Response(data)
예제 #16
0
        class Fixed(self.target_class):
            @extend_schema(operation_id="logout", **get_schema_params, tags=["auth"])
            def get(self, request, *args, **kwargs):
                pass

            @extend_schema(
                operation_id="logout",
                request=None,
                responses={
                    status.HTTP_200_OK: OpenApiResponse(
                        DetailResponseSerializer,
                        description="Success",
                        examples=[
                            OpenApiExample(
                                "Success",
                                value={"detail": "Successfully logged out."},
                                status_codes=[f"{status.HTTP_200_OK}"],
                            )
                        ],
                    ),
                    status.HTTP_401_UNAUTHORIZED: OpenApiResponse(
                        DetailResponseSerializer,
                        description="Unauthorized",
                        examples=[
                            OpenApiExample(
                                "No token",
                                value={"detail": "Invalid token header. No credentials provided."},
                                status_codes=[f"{status.HTTP_401_UNAUTHORIZED}"],
                            )
                        ],
                    ),
                },
                tags=["auth"],
            )
            def post(self, request, *args, **kwargs):
                pass
예제 #17
0
class StatusView(APIView):
    """
    A lightweight read-only endpoint for conveying Peering Manager's current
    operational status.
    """

    permission_classes = [IsAuthenticatedOrLoginNotRequired]

    @extend_schema(
        request=None,
        responses={
            200:
            OpenApiResponse(
                response=OpenApiTypes.OBJECT,
                description="Details regarding Peering Manager status.",
            )
        },
    )
    def get(self, request):
        # Gather the version numbers from all installed Django apps
        installed_apps = {}
        for app_config in apps.get_app_configs():
            app = app_config.module
            version = getattr(app, "VERSION", getattr(app, "__version__",
                                                      None))
            if version:
                if type(version) is tuple:
                    version = ".".join(str(n) for n in version)
                installed_apps[app_config.name] = version
        installed_apps = {k: v for k, v in sorted(installed_apps.items())}

        return Response({
            "django-version":
            DJANGO_VERSION,
            "installed-apps":
            installed_apps,
            "peering-manager-version":
            settings.VERSION,
            "python-version":
            platform.python_version(),
            "rq-workers-running":
            Worker.count(get_connection("default")),
        })
예제 #18
0
class TokenViewSet(ModelViewSet):
    """Token Viewset"""

    lookup_field = "identifier"
    queryset = Token.filter_not_expired()
    serializer_class = TokenSerializer
    search_fields = [
        "identifier",
        "intent",
        "user__username",
        "description",
    ]
    filterset_fields = [
        "identifier",
        "intent",
        "user__username",
        "description",
    ]
    ordering = ["expires"]

    def perform_create(self, serializer: TokenSerializer):
        serializer.save(user=self.request.user, intent=TokenIntents.INTENT_API)

    @permission_required("authentik_core.view_token_key")
    @extend_schema(
        responses={
            200: TokenViewSerializer(many=False),
            404: OpenApiResponse(description="Token not found or expired"),
        })
    @action(detail=True, pagination_class=None, filter_backends=[])
    # pylint: disable=unused-argument
    def view_key(self, request: Request, identifier: str) -> Response:
        """Return token key and log access"""
        token: Token = self.get_object()
        if token.is_expired:
            raise Http404
        Event.new(EventAction.SECRET_VIEW,
                  secret=token).from_http(  # noqa # nosec
                      request)
        return Response(TokenViewSerializer({"key": token.key}).data)
예제 #19
0
파일: api.py 프로젝트: toboshii/authentik
class LDAPSourceViewSet(ModelViewSet):
    """LDAP Source Viewset"""

    queryset = LDAPSource.objects.all()
    serializer_class = LDAPSourceSerializer
    lookup_field = "slug"

    @extend_schema(
        responses={
            200: TaskSerializer(many=False),
            404: OpenApiResponse(description="Task not found"),
        }
    )
    @action(methods=["GET"], detail=True)
    # pylint: disable=unused-argument
    def sync_status(self, request: Request, slug: str) -> Response:
        """Get source's sync status"""
        source = self.get_object()
        task = TaskInfo.by_name(f"ldap_sync_{slugify(source.name)}")
        if not task:
            raise Http404
        return Response(TaskSerializer(task, many=False).data)
예제 #20
0
class NotificationViewSet(
        mixins.RetrieveModelMixin,
        mixins.UpdateModelMixin,
        mixins.DestroyModelMixin,
        UsedByMixin,
        mixins.ListModelMixin,
        GenericViewSet,
):
    """Notification Viewset"""

    queryset = Notification.objects.all()
    serializer_class = NotificationSerializer
    filterset_fields = [
        "severity",
        "body",
        "created",
        "event",
        "seen",
    ]
    permission_classes = [OwnerPermissions]
    filter_backends = [
        OwnerFilter, DjangoFilterBackend, OrderingFilter, SearchFilter
    ]

    @extend_schema(
        request=OpenApiTypes.NONE,
        responses={
            204:
            OpenApiResponse(description="Marked tasks as read successfully."),
        },
    )
    @action(detail=False, methods=["post"])
    def mark_all_seen(self, request: Request) -> Response:
        """Mark all the user's notifications as seen"""
        notifications = Notification.objects.filter(user=request.user)
        for notification in notifications:
            notification.seen = True
        Notification.objects.bulk_update(notifications, ["seen"])
        return Response({}, status=204)
예제 #21
0
        '200': OrganizationWriteSerializer,
    },
    tags=['organizations'],
    versions=['2.0']))
@extend_schema_view(create=extend_schema(
    summary='Method creates an organization',
    responses={
        '201': OrganizationWriteSerializer,
    },
    tags=['organizations'],
    versions=['2.0']))
@extend_schema_view(destroy=extend_schema(
    summary='Method deletes an organization',
    responses={
        '204':
        OpenApiResponse(description='The organization has been deleted'),
    },
    tags=['organizations'],
    versions=['2.0']))
class OrganizationViewSet(viewsets.ModelViewSet):
    queryset = Organization.objects.all()
    search_fields = ('name', 'owner')
    filter_fields = list(search_fields) + ['id', 'slug']
    lookup_fields = {'owner': 'owner__username'}
    ordering_fields = filter_fields
    ordering = '-id'
    http_method_names = ['get', 'post', 'patch', 'delete', 'head', 'options']
    pagination_class = None
    iam_organization_field = None

    def get_queryset(self):
예제 #22
0
class PropertyMappingViewSet(
        mixins.RetrieveModelMixin,
        mixins.DestroyModelMixin,
        UsedByMixin,
        mixins.ListModelMixin,
        GenericViewSet,
):
    """PropertyMapping Viewset"""

    queryset = PropertyMapping.objects.none()
    serializer_class = PropertyMappingSerializer
    search_fields = [
        "name",
    ]
    filterset_fields = {"managed": ["isnull"]}
    ordering = ["name"]

    def get_queryset(self):  # pragma: no cover
        return PropertyMapping.objects.select_subclasses()

    @extend_schema(responses={200: TypeCreateSerializer(many=True)})
    @action(detail=False, pagination_class=None, filter_backends=[])
    def types(self, request: Request) -> Response:
        """Get all creatable property-mapping types"""
        data = []
        for subclass in all_subclasses(self.queryset.model):
            subclass: PropertyMapping
            data.append({
                "name": subclass._meta.verbose_name,
                "description": subclass.__doc__,
                # pyright: reportGeneralTypeIssues=false
                "component": subclass().component,
                "model_name": subclass._meta.model_name,
            })
        return Response(TypeCreateSerializer(data, many=True).data)

    @permission_required("authentik_core.view_propertymapping")
    @extend_schema(
        request=PolicyTestSerializer(),
        responses={
            200: PropertyMappingTestResultSerializer,
            400: OpenApiResponse(description="Invalid parameters"),
        },
        parameters=[
            OpenApiParameter(
                name="format_result",
                location=OpenApiParameter.QUERY,
                type=OpenApiTypes.BOOL,
            )
        ],
    )
    @action(detail=True,
            pagination_class=None,
            filter_backends=[],
            methods=["POST"])
    # pylint: disable=unused-argument, invalid-name
    def test(self, request: Request, pk: str) -> Response:
        """Test Property Mapping"""
        mapping: PropertyMapping = self.get_object()
        test_params = PolicyTestSerializer(data=request.data)
        if not test_params.is_valid():
            return Response(test_params.errors, status=400)

        format_result = str(request.GET.get("format_result",
                                            "false")).lower() == "true"

        # User permission check, only allow mapping testing for users that are readable
        users = get_objects_for_user(
            request.user, "authentik_core.view_user").filter(
                pk=test_params.validated_data["user"].pk)
        if not users.exists():
            raise PermissionDenied()

        response_data = {"successful": True, "result": ""}
        try:
            result = mapping.evaluate(
                users.first(),
                self.request,
                **test_params.validated_data.get("context", {}),
            )
            response_data["result"] = dumps(
                result, indent=(4 if format_result else None))
        except Exception as exc:  # pylint: disable=broad-except
            response_data["result"] = str(exc)
            response_data["successful"] = False
        response = PropertyMappingTestResultSerializer(response_data)
        return Response(response.data)
예제 #23
0
class AutonomousSystemViewSet(ModelViewSet):
    queryset = AutonomousSystem.objects.defer("prefixes")
    serializer_class = AutonomousSystemSerializer
    filterset_class = AutonomousSystemFilterSet

    @extend_schema(
        operation_id="peering_autonomous_systems_sync_with_peeringdb",
        request=None,
        responses={
            200:
            OpenApiResponse(
                response=OpenApiTypes.NONE,
                description="The synchronization has been done.",
            ),
            204:
            OpenApiResponse(
                response=OpenApiTypes.NONE,
                description="The synchronization cannot be done.",
            ),
            403:
            OpenApiResponse(
                response=OpenApiTypes.NONE,
                description=
                "The user does not have the permission update the AS.",
            ),
            404:
            OpenApiResponse(response=OpenApiTypes.OBJECT,
                            description="The AS does not exist."),
        },
    )
    @action(detail=True, methods=["post"], url_path="sync-with-peeringdb")
    def sync_with_peeringdb(self, request, pk=None):
        # Check user permission first
        if not request.user.has_perm("peering.change_autonomoussystem"):
            return Response(status=status.HTTP_403_FORBIDDEN)

        success = self.get_object().synchronize_with_peeringdb()
        return Response(status=status.HTTP_200_OK if success else status.
                        HTTP_204_NO_CONTENT)

    @extend_schema(
        operation_id="peering_autonomous_systems_as_set_prefixes",
        request=None,
        responses={
            200:
            OpenApiResponse(
                response=OpenApiTypes.OBJECT,
                description="Retrieves the prefix list for the AS.",
            ),
            404:
            OpenApiResponse(response=OpenApiTypes.OBJECT,
                            description="The AS does not exist."),
        },
    )
    @action(detail=True, methods=["get"], url_path="as-set-prefixes")
    def as_set_prefixes(self, request, pk=None):
        return Response(data=self.get_object().get_irr_as_set_prefixes())

    @extend_schema(
        operation_id="peering_autonomous_systems_shared_ixps",
        request=None,
        responses={
            200:
            OpenApiResponse(
                response=NestedInternetExchangeSerializer(many=True),
                description="Retrieves the shared IXPs with the AS.",
            ),
            404:
            OpenApiResponse(response=OpenApiTypes.OBJECT,
                            description="The AS does not exist."),
            503:
            OpenApiResponse(
                response=OpenApiTypes.OBJECT,
                description="The user has no affiliated AS.",
            ),
        },
    )
    @action(detail=True, methods=["get"], url_path="shared-ixps")
    def shared_ixps(self, request, pk=None):
        try:
            affiliated = AutonomousSystem.objects.get(
                pk=request.user.preferences.get("context.as"))
        except AutonomousSystem.DoesNotExist:
            raise ServiceUnavailable("User did not choose an affiliated AS.")

        return Response(data=NestedInternetExchangeSerializer(
            self.get_object().get_shared_internet_exchange_points(affiliated),
            many=True,
            context={
                "request": request
            },
        ).data)

    @extend_schema(
        operation_id="peering_autonomous_systems_generate_email",
        request=None,
        responses={
            200:
            OpenApiResponse(response=OpenApiTypes.OBJECT,
                            description="Renders the e-mail template."),
            404:
            OpenApiResponse(
                response=OpenApiTypes.NONE,
                description="The AS or e-mail template does not exist.",
            ),
        },
    )
    @action(detail=True, methods=["post"], url_path="generate-email")
    def generate_email(self, request, pk=None):
        # Make sure request is valid
        serializer = AutonomousSystemGenerateEmailSerializer(data=request.data)
        serializer.is_valid(raise_exception=True)

        try:
            template = Email.objects.get(
                pk=serializer.validated_data.get("email"))
            rendered = self.get_object().generate_email(template)
            return Response(data={"subject": rendered[0], "body": rendered[1]})
        except Email.DoesNotExist:
            raise Response(status=status.HTTP_404_NOT_FOUND)
예제 #24
0
class RouterViewSet(ModelViewSet):
    queryset = Router.objects.all()
    serializer_class = RouterSerializer
    filterset_class = RouterFilterSet

    @extend_schema(
        operation_id="peering_routers_configuration",
        request=None,
        responses={
            202:
            OpenApiResponse(
                response=JobResultSerializer,
                description=
                "Job scheduled to generate the router configuration.",
            ),
            403:
            OpenApiResponse(
                response=OpenApiTypes.NONE,
                description=
                "The user does not have the permission to generate a configuration.",
            ),
            404:
            OpenApiResponse(
                response=OpenApiTypes.OBJECT,
                description="The router does not exist.",
            ),
        },
    )
    @action(detail=True, methods=["get"], url_path="configuration")
    def configuration(self, request, pk=None):
        # Check user permission first
        if not request.user.has_perm("peering.view_router_configuration"):
            return Response(status=status.HTTP_403_FORBIDDEN)

        job_result = JobResult.enqueue_job(
            generate_configuration,
            "peering.router.generate_configuration",
            Router,
            request.user,
            self.get_object(),
        )
        return Response(
            JobResultSerializer(instance=job_result,
                                context={
                                    "request": request
                                }).data,
            status=status.HTTP_202_ACCEPTED,
        )

    @extend_schema(
        operation_id="peering_routers_configure",
        request=RouterConfigureSerializer,
        responses={
            202:
            OpenApiResponse(
                response=JobResultSerializer,
                description="Job scheduled to generate configure routers.",
            ),
            400:
            OpenApiResponse(
                response=OpenApiTypes.NONE,
                description="Invalid list of routers provided.",
            ),
            403:
            OpenApiResponse(
                response=OpenApiTypes.NONE,
                description=
                "The user does not have the permission to configure routers.",
            ),
            404:
            OpenApiResponse(
                response=OpenApiTypes.OBJECT,
                description="The router does not exist.",
            ),
        },
    )
    @action(detail=False, methods=["post"], url_path="configure")
    def configure(self, request):
        # Check user permission first
        if not request.user.has_perm("peering.deploy_router_configuration"):
            return Response(None, status=status.HTTP_403_FORBIDDEN)

        # Make sure request is valid
        serializer = RouterConfigureSerializer(data=request.data)
        serializer.is_valid(raise_exception=True)

        router_ids = serializer.validated_data.get("routers")
        if len(router_ids) < 1:
            raise ValidationError("routers list must not be empty")
        commit = serializer.validated_data.get("commit")

        routers = Router.objects.filter(pk__in=router_ids)
        if not routers:
            return Response(status=status.HTTP_404_NOT_FOUND)

        job_results = []
        for router in routers:
            job_result = JobResult.enqueue_job(
                set_napalm_configuration,
                "peering.router.set_napalm_configuration",
                Router,
                request.user,
                router,
                commit,
            )
            job_results.append(job_result)

        return Response(
            JobResultSerializer(job_results,
                                many=True,
                                context={
                                    "request": request
                                }).data,
            status=status.HTTP_202_ACCEPTED,
        )

    @extend_schema(
        operation_id="peering_routers_test_napalm_connection",
        request=RouterConfigureSerializer,
        responses={
            202:
            OpenApiResponse(
                response=JobResultSerializer,
                description=
                "Job scheduled to test the router NAPALM connection.",
            ),
            404:
            OpenApiResponse(
                response=OpenApiTypes.OBJECT,
                description="The router does not exist.",
            ),
        },
    )
    @action(detail=True, methods=["get"], url_path="test-napalm-connection")
    def test_napalm_connection(self, request, pk=None):
        job_result = JobResult.enqueue_job(
            test_napalm_connection,
            "peering.router.test_napalm_connection",
            Router,
            request.user,
            self.get_object(),
        )
        return Response(
            JobResultSerializer(instance=job_result,
                                context={
                                    "request": request
                                }).data,
            status=status.HTTP_202_ACCEPTED,
        )
예제 #25
0
class InternetExchangePeeringSessionViewSet(ModelViewSet):
    queryset = InternetExchangePeeringSession.objects.all()
    serializer_class = InternetExchangePeeringSessionSerializer
    filterset_class = InternetExchangePeeringSessionFilterSet

    @extend_schema(
        operation_id=
        "peering_internet_exchange_peering_sessions_encrypt_password",
        request=None,
        responses={
            200:
            OpenApiResponse(
                response=OpenApiTypes.NONE,
                description="The session password has been encrypted.",
            ),
            403:
            OpenApiResponse(
                response=OpenApiTypes.NONE,
                description=
                "The user does not have the permission to encrypt the password.",
            ),
            404:
            OpenApiResponse(
                response=OpenApiTypes.OBJECT,
                description=
                "The Internet exchange peering session does not exist.",
            ),
            503:
            OpenApiResponse(
                response=OpenApiTypes.NONE,
                description="The session has not been encrypted.",
            ),
        },
    )
    @action(detail=True, methods=["post"], url_path="encrypt-password")
    def encrypt_password(self, request, pk=None):
        # Check user permission first
        if not request.user.has_perm(
                "peering.change_internetexchangepeeringsession"):
            return Response(status=status.HTTP_403_FORBIDDEN)

        success = self.get_object().encrypt_password(commit=True)
        return Response(status=status.HTTP_200_OK if success else status.
                        HTTP_503_SERVICE_UNAVAILABLE)

    @extend_schema(
        operation_id="peering_internet_exchange_peering_sessions_poll",
        request=None,
        responses={
            200:
            OpenApiResponse(
                response=OpenApiTypes.NONE,
                description="The session status has been polled.",
            ),
            403:
            OpenApiResponse(
                response=OpenApiTypes.NONE,
                description=
                "The user does not have the permission to poll session status.",
            ),
            404:
            OpenApiResponse(
                response=OpenApiTypes.OBJECT,
                description=
                "The Internet exchange peering session does not exist.",
            ),
            503:
            OpenApiResponse(
                response=OpenApiTypes.NONE,
                description="The session status has not been polled.",
            ),
        },
    )
    @action(detail=True, methods=["post"], url_path="poll")
    def poll(self, request, pk=None):
        # Check user permission first
        if not request.user.has_perm(
                "peering.change_internetexchangepeeringsession"):
            return Response(status=status.HTTP_403_FORBIDDEN)

        success = self.get_object().poll()
        return Response(status=status.HTTP_200_OK if success else status.
                        HTTP_503_SERVICE_UNAVAILABLE)
예제 #26
0
class InternetExchangeViewSet(ModelViewSet):
    queryset = InternetExchange.objects.all()
    serializer_class = InternetExchangeSerializer
    filterset_class = InternetExchangeFilterSet

    @extend_schema(
        operation_id="peering_internet_exchange_link_to_peeringdb",
        request=None,
        responses={
            200:
            OpenApiResponse(
                response=OpenApiTypes.NONE,
                description="The IXP is linked with a PeeringDB record.",
            ),
            403:
            OpenApiResponse(
                response=OpenApiTypes.NONE,
                description=
                "The user does not have the permission to update the IXP.",
            ),
            404:
            OpenApiResponse(
                response=OpenApiTypes.OBJECT,
                description="The IXP does not exist.",
            ),
            503:
            OpenApiResponse(
                response=OpenApiTypes.NONE,
                description="The IXP is not linked with a PeeringDB record.",
            ),
        },
    )
    @action(detail=True, methods=["post"], url_path="link-to-peeringdb")
    def link_to_peeringdb(self, request, pk=None):
        # Check user permission first
        if not request.user.has_perm("peering.change_internetexchange"):
            return Response(status=status.HTTP_403_FORBIDDEN)

        ixlan = self.get_object().link_to_peeringdb()
        return Response(status=status.HTTP_200_OK if ixlan is not None else
                        status.HTTP_503_SERVICE_UNAVAILABLE)

    @extend_schema(
        operation_id="peering_internet_exchange_available_peers",
        request=None,
        responses={
            200:
            OpenApiResponse(
                response=NetworkIXLanSerializer(many=True),
                description="The PeeringDB records of available peers.",
            ),
            404:
            OpenApiResponse(
                response=OpenApiTypes.OBJECT,
                description="The IXP does not exist.",
            ),
        },
    )
    @action(detail=True, methods=["get"], url_path="available-peers")
    def available_peers(self, request, pk=None):
        return Response(data=NetworkIXLanSerializer(
            self.get_object().get_available_peers(),
            many=True,
            context={
                "request": request
            },
        ).data)

    @extend_schema(
        operation_id="peering_internet_exchanges_import_sessions",
        request=None,
        responses={
            202:
            OpenApiResponse(
                response=JobResultSerializer,
                description="Session import job is scheduled.",
            ),
            403:
            OpenApiResponse(
                response=OpenApiTypes.NONE,
                description=
                "The user does not have the permission to update the IXP sessions.",
            ),
            404:
            OpenApiResponse(
                response=OpenApiTypes.OBJECT,
                description="The IXP does not exist.",
            ),
        },
    )
    @action(detail=True, methods=["post"], url_path="import-sessions")
    def import_sessions(self, request, pk=None):
        if not request.user.has_perm(
                "peering.add_internetexchangepeeringsession"):
            return Response(status=status.HTTP_403_FORBIDDEN)

        job_result = JobResult.enqueue_job(
            import_sessions_to_internet_exchange,
            "peering.internet_exchange.import_sessions",
            InternetExchange,
            request.user,
            self.get_object(),
        )
        return Response(
            data=JobResultSerializer(instance=job_result,
                                     context={
                                         "request": request
                                     }).data,
            status=status.HTTP_202_ACCEPTED,
        )

    @extend_schema(
        operation_id="peering_internet_exchanges_prefixes",
        request=None,
        responses={
            200:
            OpenApiResponse(
                response=OpenApiTypes.OBJECT,
                description=
                "The prefixes attached to the IXP sorted by address family.",
            ),
            404:
            OpenApiResponse(
                response=OpenApiTypes.OBJECT,
                description="The IXP does not exist.",
            ),
        },
    )
    @action(detail=True, methods=["get"], url_path="prefixes")
    def prefixes(self, request, pk=None):
        prefixes = {}
        for p in self.get_object().get_prefixes():
            if p.prefix.version == 6:
                ipv6 = prefixes.setdefault("ipv6", [])
                ipv6.append(str(p.prefix))
            if p.prefix.version == 4:
                ipv4 = prefixes.setdefault("ipv4", [])
                ipv4.append(str(p.prefix))

        return Response(data=prefixes)

    @extend_schema(
        operation_id="peering_internet_exchanges_poll_sessions",
        request=None,
        responses={
            202:
            OpenApiResponse(
                response=JobResultSerializer,
                description="Job scheduled to poll sessions.",
            ),
            403:
            OpenApiResponse(
                response=OpenApiTypes.NONE,
                description=
                "The user does not have the permission to poll session status.",
            ),
            404:
            OpenApiResponse(
                response=OpenApiTypes.OBJECT,
                description="The IXP does not exist.",
            ),
        },
    )
    @action(detail=True, methods=["post"], url_path="poll-sessions")
    def poll_sessions(self, request, pk=None):
        # Check user permission first
        if not request.user.has_perm(
                "peering.change_internetexchangepeeringsession"):
            return Response(status=status.HTTP_403_FORBIDDEN)

        job_result = JobResult.enqueue_job(
            poll_peering_sessions,
            "peering.internetexchange.poll_peering_sessions",
            InternetExchange,
            request.user,
            self.get_object(),
        )
        return Response(
            data=JobResultSerializer(instance=job_result,
                                     context={
                                         "request": request
                                     }).data,
            status=status.HTTP_202_ACCEPTED,
        )
예제 #27
0
class FlowViewSet(UsedByMixin, ModelViewSet):
    """Flow Viewset"""

    queryset = Flow.objects.all()
    serializer_class = FlowSerializer
    lookup_field = "slug"
    ordering = ["slug", "name"]
    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": FileUploadSerializer},
        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 HttpResponseBadRequest()
        return Response(status=204)

    @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": FileUploadSerializer,
        },
        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()
        background = request.FILES.get("file", None)
        clear = request.data.get("clear", "false").lower() == "true"
        if clear:
            if flow.background_url.startswith("/media"):
                # .delete() saves the model by default
                flow.background.delete()
            else:
                flow.background = None
                flow.save()
            return Response({})
        if background:
            flow.background = background
            flow.save()
            return Response({})
        return HttpResponseBadRequest()

    @permission_required("authentik_core.change_application")
    @extend_schema(
        request=FilePathSerializer,
        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.name = 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"""
        # Because we pre-plan the flow here, and not in the planner, we need to manually clear
        # the history of the inspector
        request.session[SESSION_KEY_HISTORY] = []
        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})
                )
            }
        )
예제 #28
0
class AuthenticatorDuoStageViewSet(UsedByMixin, ModelViewSet):
    """AuthenticatorDuoStage Viewset"""

    queryset = AuthenticatorDuoStage.objects.all()
    serializer_class = AuthenticatorDuoStageSerializer
    filterset_fields = [
        "name",
        "configure_flow",
        "client_id",
        "api_hostname",
    ]
    ordering = ["name"]

    @extend_schema(
        request=OpenApiTypes.NONE,
        responses={
            204: OpenApiResponse(description="Enrollment successful"),
            420: OpenApiResponse(description="Enrollment pending/failed"),
        },
    )
    @action(methods=["POST"], detail=True, permission_classes=[])
    # pylint: disable=invalid-name,unused-argument
    def enrollment_status(self, request: Request, pk: str) -> Response:
        """Check enrollment status of user details in current session"""
        stage: AuthenticatorDuoStage = self.get_object()
        client = stage.client
        user_id = self.request.session.get(SESSION_KEY_DUO_USER_ID)
        activation_code = self.request.session.get(
            SESSION_KEY_DUO_ACTIVATION_CODE)
        status = client.enroll_status(user_id, activation_code)
        if status == "success":
            return Response(status=204)
        return Response(status=420)

    @permission_required("", [
        "authentik_stages_authenticator_duo.add_duodevice",
        "authentik_core.view_user"
    ])
    @extend_schema(
        parameters=[
            OpenApiParameter(name="duo_user_id",
                             type=OpenApiTypes.STR,
                             location=OpenApiParameter.QUERY),
            OpenApiParameter(name="username",
                             type=OpenApiTypes.STR,
                             location=OpenApiParameter.QUERY),
        ],
        responses={
            204: OpenApiResponse(description="Enrollment successful"),
            400: OpenApiResponse(description="Device exists already"),
        },
    )
    @action(methods=["POST"], detail=True)
    # pylint: disable=invalid-name,unused-argument
    def import_devices(self, request: Request, pk: str) -> Response:
        """Import duo devices into authentik"""
        stage: AuthenticatorDuoStage = self.get_object()
        users = get_objects_for_user(
            request.user, "authentik_core.view_user").filter(
                username=request.query_params.get("username", ""))
        if not users.exists():
            return Response(data={"non_field_errors": ["user does not exist"]},
                            status=400)
        devices = DuoDevice.objects.filter(
            duo_user_id=request.query_params.get("duo_user_id"),
            user=users.first(),
            stage=stage)
        if devices.exists():
            return Response(
                data={"non_field_errors": ["device exists already"]},
                status=400)
        DuoDevice.objects.create(
            duo_user_id=request.query_params.get("duo_user_id"),
            user=users.first(),
            stage=stage)
        return Response(status=204)
예제 #29
0
파일: api.py 프로젝트: toboshii/authentik
class SAMLProviderViewSet(ModelViewSet):
    """SAMLProvider Viewset"""

    queryset = SAMLProvider.objects.all()
    serializer_class = SAMLProviderSerializer

    @extend_schema(
        responses={
            200: SAMLMetadataSerializer(many=False),
            404: OpenApiResponse(description="Provider has no application assigned"),
        },
        parameters=[
            OpenApiParameter(
                name="download",
                location=OpenApiParameter.QUERY,
                type=OpenApiTypes.BOOL,
            )
        ],
    )
    @action(methods=["GET"], detail=True, permission_classes=[AllowAny])
    # pylint: disable=invalid-name, unused-argument
    def metadata(self, request: Request, pk: int) -> Response:
        """Return metadata as XML string"""
        # We don't use self.get_object() on purpose as this view is un-authenticated
        provider = get_object_or_404(SAMLProvider, pk=pk)
        try:
            metadata = MetadataProcessor(provider, request).build_entity_descriptor()
            if "download" in request._request.GET:
                response = HttpResponse(metadata, content_type="application/xml")
                response[
                    "Content-Disposition"
                ] = f'attachment; filename="{provider.name}_authentik_meta.xml"'
                return response
            return Response({"metadata": metadata})
        except Provider.application.RelatedObjectDoesNotExist:  # pylint: disable=no-member
            return Response({"metadata": ""})

    @permission_required(
        None,
        [
            "authentik_providers_saml.add_samlprovider",
            "authentik_crypto.add_certificatekeypair",
        ],
    )
    @extend_schema(
        request={
            "multipart/form-data": SAMLProviderImportSerializer,
        },
        responses={
            204: OpenApiResponse(description="Successfully imported provider"),
            400: OpenApiResponse(description="Bad request"),
        },
    )
    @action(detail=False, methods=["POST"], parser_classes=(MultiPartParser,))
    def import_metadata(self, request: Request) -> Response:
        """Create provider from SAML Metadata"""
        data = SAMLProviderImportSerializer(data=request.data)
        if not data.is_valid():
            raise ValidationError(data.errors)
        file = data.validated_data["file"]
        # Validate syntax first
        try:
            fromstring(file.read())
        except ParseError:
            raise ValidationError(_("Invalid XML Syntax"))
        file.seek(0)
        try:
            metadata = ServiceProviderMetadataParser().parse(file.read().decode())
            metadata.to_provider(
                data.validated_data["name"], data.validated_data["authorization_flow"]
            )
        except ValueError as exc:  # pragma: no cover
            LOGGER.warning(str(exc))
            return ValidationError(
                _("Failed to import Metadata: %(message)s" % {"message": str(exc)}),
            )
        return Response(status=204)
예제 #30
0
파일: insight.py 프로젝트: PostHog/posthog
class InsightViewSet(TaggedItemViewSetMixin, StructuredViewSetMixin,
                     viewsets.ModelViewSet):
    queryset = Insight.objects.all().prefetch_related(
        "dashboard", "dashboard__team", "dashboard__team__organization",
        "created_by")
    serializer_class = InsightSerializer
    permission_classes = [
        IsAuthenticated, ProjectMembershipNecessaryPermissions,
        TeamMemberAccessPermission
    ]
    renderer_classes = tuple(
        api_settings.DEFAULT_RENDERER_CLASSES) + (csvrenderers.CSVRenderer, )
    filter_backends = [DjangoFilterBackend]
    filterset_fields = ["short_id", "created_by"]
    include_in_docs = True

    def get_serializer_class(self) -> Type[serializers.BaseSerializer]:

        if (self.action == "list"
                or self.action == "retrieve") and str_to_bool(
                    self.request.query_params.get("basic", "0"), ):
            return InsightBasicSerializer
        return super().get_serializer_class()

    def get_queryset(self) -> QuerySet:
        queryset = super().get_queryset()
        if self.action == "list":
            queryset = queryset.filter(deleted=False)
            queryset = self._filter_request(self.request, queryset)

        order = self.request.GET.get("order", None)
        if order:
            queryset = queryset.order_by(order)
        else:
            queryset = queryset.order_by("order")

        return queryset

    def _filter_request(self, request: request.Request,
                        queryset: QuerySet) -> QuerySet:
        filters = request.GET.dict()

        for key in filters:
            if key == "saved":
                if str_to_bool(request.GET["saved"]):
                    queryset = queryset.filter(
                        Q(saved=True) | Q(dashboard__isnull=False))
                else:
                    queryset = queryset.filter(Q(saved=False))
            elif key == "user":
                queryset = queryset.filter(created_by=request.user)
            elif key == "favorited":
                queryset = queryset.filter(Q(favorited=True))
            elif key == "date_from":
                queryset = queryset.filter(
                    last_modified_at__gt=relative_date_parse(
                        request.GET["date_from"]))
            elif key == "date_to":
                queryset = queryset.filter(
                    last_modified_at__lt=relative_date_parse(
                        request.GET["date_to"]))
            elif key == INSIGHT:
                queryset = queryset.filter(
                    filters__insight=request.GET[INSIGHT])
            elif key == "search":
                queryset = queryset.filter(
                    Q(name__icontains=request.GET["search"])
                    | Q(derived_name__icontains=request.GET["search"]))
        return queryset

    @action(methods=["patch"], detail=False)
    def layouts(self, request, **kwargs):
        """Dashboard item layouts."""
        queryset = self.get_queryset()
        for data in request.data["items"]:
            queryset.filter(pk=data["id"]).update(layouts=data["layouts"])
        serializer = self.get_serializer(queryset.all(), many=True)
        return Response(serializer.data)

    # ******************************************
    # Calculated Insight Endpoints
    # /projects/:id/insights/trend
    # /projects/:id/insights/funnel
    # /projects/:id/insights/retention
    # /projects/:id/insights/path
    #
    # Request parameteres and caching are handled here and passed onto respective .queries classes
    # ******************************************

    # ******************************************
    # /projects/:id/insights/trend
    #
    # params:
    # - from_dashboard: (string) determines trend is being retrieved from dashboard item to update dashboard_item metadata
    # - shown_as: (string: Volume, Stickiness) specifies the trend aggregation type
    # - **shared filter types
    # ******************************************
    @extend_schema(
        request=TrendSerializer,
        methods=["POST"],
        tags=["trend"],
        operation_id="Trends",
        responses=TrendResultsSerializer,
    )
    @action(methods=["GET", "POST"], detail=False)
    def trend(self, request: request.Request, *args: Any, **kwargs: Any):
        try:
            serializer = TrendSerializer(request=request)
            serializer.is_valid(raise_exception=True)
        except Exception as e:
            capture_exception(e)

        result = self.calculate_trends(request)
        filter = Filter(request=request, team=self.team)
        next = (format_paginated_url(request, filter.offset,
                                     BREAKDOWN_VALUES_LIMIT)
                if len(result["result"]) >= BREAKDOWN_VALUES_LIMIT else None)
        if self.request.accepted_renderer.format == "csv":
            csvexport = []
            for item in result["result"]:
                line = {"series": item["label"]}
                for index, data in enumerate(item["data"]):
                    line[item["labels"][index]] = data
                csvexport.append(line)
            renderer = csvrenderers.CSVRenderer()
            renderer.header = csvexport[0].keys()
            export = renderer.render(csvexport)
            if request.GET.get("export_insight_id"):
                export = "{}/insights/{}/\n".format(
                    SITE_URL,
                    request.GET["export_insight_id"]).encode() + export

            response = HttpResponse(export)
            response[
                "Content-Disposition"] = 'attachment; filename="{name} ({date_from} {date_to}) from PostHog.csv"'.format(
                    name=slugify(request.GET.get("export_name", "export")),
                    date_from=filter.date_from.strftime("%Y-%m-%d -")
                    if filter.date_from else "up until",
                    date_to=filter.date_to.strftime("%Y-%m-%d"),
                )
            return response
        return Response({**result, "next": next})

    @cached_function
    def calculate_trends(self, request: request.Request) -> Dict[str, Any]:
        team = self.team
        filter = Filter(request=request, team=self.team)

        if filter.insight == INSIGHT_STICKINESS or filter.shown_as == TRENDS_STICKINESS:
            stickiness_filter = StickinessFilter(
                request=request,
                team=team,
                get_earliest_timestamp=get_earliest_timestamp)
            result = ClickhouseStickiness().run(stickiness_filter, team)
        else:
            trends_query = ClickhouseTrends()
            result = trends_query.run(filter, team)

        self._refresh_dashboard(request=request)
        return {"result": result}

    # ******************************************
    # /projects/:id/insights/funnel
    # The funnel endpoint is asynchronously processed. When a request is received, the endpoint will
    # call an async task with an id that can be continually polled for 3 minutes.
    #
    # params:
    # - refresh: (dict) specifies cache to force refresh or poll
    # - from_dashboard: (dict) determines funnel is being retrieved from dashboard item to update dashboard_item metadata
    # - **shared filter types
    # ******************************************
    @extend_schema(
        request=FunnelSerializer,
        responses=OpenApiResponse(
            response=FunnelStepsResultsSerializer,
            description=
            "Note, if funnel_viz_type is set the response will be different.",
        ),
        methods=["POST"],
        tags=["funnel"],
        operation_id="Funnels",
    )
    @action(methods=["GET", "POST"], detail=False)
    def funnel(self, request: request.Request, *args: Any,
               **kwargs: Any) -> Response:
        try:
            serializer = FunnelSerializer(request=request)
            serializer.is_valid(raise_exception=True)
        except Exception as e:
            capture_exception(e)

        funnel = self.calculate_funnel(request)

        funnel["result"] = protect_old_clients_from_multi_property_default(
            request.data, funnel["result"])

        return Response(funnel)

    @cached_function
    def calculate_funnel(self, request: request.Request) -> Dict[str, Any]:
        team = self.team
        filter = Filter(request=request,
                        data={"insight": INSIGHT_FUNNELS},
                        team=self.team)

        if filter.funnel_viz_type == FunnelVizType.TRENDS:
            return {
                "result": ClickhouseFunnelTrends(team=team,
                                                 filter=filter).run()
            }
        elif filter.funnel_viz_type == FunnelVizType.TIME_TO_CONVERT:
            return {
                "result":
                ClickhouseFunnelTimeToConvert(team=team, filter=filter).run()
            }
        else:
            funnel_order_class = get_funnel_order_class(filter)
            return {
                "result": funnel_order_class(team=team, filter=filter).run()
            }

    # ******************************************
    # /projects/:id/insights/retention
    # params:
    # - start_entity: (dict) specifies id and type of the entity to focus retention on
    # - **shared filter types
    # ******************************************
    @action(methods=["GET"], detail=False)
    def retention(self, request: request.Request, *args: Any,
                  **kwargs: Any) -> Response:
        result = self.calculate_retention(request)
        return Response(result)

    @cached_function
    def calculate_retention(self, request: request.Request) -> Dict[str, Any]:
        team = self.team
        data = {}
        if not request.GET.get("date_from"):
            data.update({"date_from": "-11d"})
        filter = RetentionFilter(data=data, request=request, team=self.team)
        base_uri = request.build_absolute_uri("/")
        result = ClickhouseRetention(base_uri=base_uri).run(filter, team)
        return {"result": result}

    # ******************************************
    # /projects/:id/insights/path
    # params:
    # - start: (string) specifies the name of the starting property or element
    # - request_type: (string: $pageview, $autocapture, $screen, custom_event) specifies the path type
    # - **shared filter types
    # ******************************************
    @action(methods=["GET", "POST"], detail=False)
    def path(self, request: request.Request, *args: Any,
             **kwargs: Any) -> Response:
        result = self.calculate_path(request)
        return Response(result)

    @cached_function
    def calculate_path(self, request: request.Request) -> Dict[str, Any]:
        team = self.team
        filter = PathFilter(request=request,
                            data={"insight": INSIGHT_PATHS},
                            team=self.team)

        funnel_filter = None
        funnel_filter_data = request.GET.get(
            "funnel_filter") or request.data.get("funnel_filter")
        if funnel_filter_data:
            if isinstance(funnel_filter_data, str):
                funnel_filter_data = json.loads(funnel_filter_data)
            funnel_filter = Filter(data={
                "insight": INSIGHT_FUNNELS,
                **funnel_filter_data
            },
                                   team=self.team)

        #  backwards compatibility
        if filter.path_type:
            filter = filter.with_data(
                {PATHS_INCLUDE_EVENT_TYPES: [filter.path_type]})
        resp = ClickhousePaths(filter=filter,
                               team=team,
                               funnel_filter=funnel_filter).run()

        return {"result": resp}

    # Checks if a dashboard id has been set and if so, update the refresh date
    def _refresh_dashboard(self, request) -> None:
        dashboard_id = request.GET.get(FROM_DASHBOARD, None)
        if dashboard_id:
            Insight.objects.filter(pk=dashboard_id).update(last_refresh=now())