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 PolicyViewSet( mixins.RetrieveModelMixin, mixins.DestroyModelMixin, mixins.ListModelMixin, GenericViewSet, ): """Policy Viewset""" queryset = Policy.objects.all() serializer_class = PolicySerializer filterset_fields = { "bindings": ["isnull"], "promptstage": ["isnull"], } search_fields = ["name"] def get_queryset(self): return Policy.objects.select_subclasses().prefetch_related( "bindings", "promptstage_set") @extend_schema(responses={200: TypeCreateSerializer(many=True)}) @action(detail=False, pagination_class=None, filter_backends=[]) def types(self, request: Request) -> Response: """Get all creatable policy types""" data = [] for subclass in all_subclasses(self.queryset.model): subclass: Policy data.append({ "name": subclass._meta.verbose_name, "description": subclass.__doc__, "component": subclass().component, "model_name": subclass._meta.model_name, }) return Response(TypeCreateSerializer(data, many=True).data) @permission_required(None, ["authentik_policies.view_policy_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 policies""" return Response(data={"count": len(cache.keys("policy_*"))}) @permission_required(None, ["authentik_policies.clear_policy_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 policy cache""" keys = cache.keys("policy_*") cache.delete_many(keys) LOGGER.debug("Cleared Policy cache", keys=len(keys)) # Also delete user application cache keys = cache.keys(user_app_cache_key("*")) cache.delete_many(keys) return Response(status=204) @permission_required("authentik_policies.view_policy") @extend_schema( request=PolicyTestSerializer(), responses={ 200: PolicyTestResultSerializer(), 400: OpenApiResponse(description="Invalid parameters"), }, ) @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 policy""" policy = self.get_object() test_params = PolicyTestSerializer(data=request.data) if not test_params.is_valid(): return Response(test_params.errors, status=400) # User permission check, only allow policy 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() p_request = PolicyRequest(users.first()) p_request.debug = True p_request.set_http_request(self.request) p_request.context = test_params.validated_data.get("context", {}) proc = PolicyProcess(PolicyBinding(policy=policy), p_request, None) result = proc.execute() response = PolicyTestResultSerializer(result) return Response(response.data)