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