From e39c460e3af5b0ce055d4c63c3b1b75b89186cb5 Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Thu, 16 Feb 2023 12:38:16 +0100 Subject: [PATCH] initial interfaces Signed-off-by: Jens Langhammer --- authentik/api/v3/urls.py | 3 + authentik/core/urls.py | 27 +--- authentik/core/views/interface.py | 36 ----- authentik/interfaces/__init__.py | 0 authentik/interfaces/api.py | 20 +++ authentik/interfaces/apps.py | 12 ++ .../interfaces/migrations/0001_initial.py | 36 +++++ authentik/interfaces/migrations/__init__.py | 0 authentik/interfaces/models.py | 33 +++++ authentik/interfaces/urls.py | 16 ++ authentik/interfaces/views.py | 64 ++++++++ authentik/root/settings.py | 1 + blueprints/schema.json | 1 + blueprints/system/interfaces.yaml | 139 ++++++++++++++++++ 14 files changed, 333 insertions(+), 55 deletions(-) delete mode 100644 authentik/core/views/interface.py create mode 100644 authentik/interfaces/__init__.py create mode 100644 authentik/interfaces/api.py create mode 100644 authentik/interfaces/apps.py create mode 100644 authentik/interfaces/migrations/0001_initial.py create mode 100644 authentik/interfaces/migrations/__init__.py create mode 100644 authentik/interfaces/models.py create mode 100644 authentik/interfaces/urls.py create mode 100644 authentik/interfaces/views.py create mode 100644 blueprints/system/interfaces.yaml diff --git a/authentik/api/v3/urls.py b/authentik/api/v3/urls.py index 591f1adee..2a432d780 100644 --- a/authentik/api/v3/urls.py +++ b/authentik/api/v3/urls.py @@ -33,6 +33,7 @@ from authentik.flows.api.flows import FlowViewSet from authentik.flows.api.stages import StageViewSet from authentik.flows.views.executor import FlowExecutorView from authentik.flows.views.inspector import FlowInspectorView +from authentik.interfaces.api import InterfaceViewSet from authentik.outposts.api.outposts import OutpostViewSet from authentik.outposts.api.service_connections import ( DockerServiceConnectionViewSet, @@ -123,6 +124,8 @@ router.register("core/user_consent", UserConsentViewSet) router.register("core/tokens", TokenViewSet) router.register("core/tenants", TenantViewSet) +router.register("interfaces", InterfaceViewSet) + router.register("outposts/instances", OutpostViewSet) router.register("outposts/service_connections/all", ServiceConnectionViewSet) router.register("outposts/service_connections/docker", DockerServiceConnectionViewSet) diff --git a/authentik/core/urls.py b/authentik/core/urls.py index 8dc412465..d2c3d262c 100644 --- a/authentik/core/urls.py +++ b/authentik/core/urls.py @@ -6,14 +6,18 @@ from django.contrib.auth.decorators import login_required from django.urls import path from django.views.decorators.csrf import ensure_csrf_cookie from django.views.generic import RedirectView - +from django.http import HttpRequest, HttpResponse from authentik.core.views import apps, impersonate from authentik.core.views.debug import AccessDeniedView -from authentik.core.views.interface import FlowInterfaceView, InterfaceView from authentik.core.views.session import EndSessionView from authentik.root.asgi_middleware import SessionMiddleware from authentik.root.messages.consumer import MessageConsumer + +def placeholder_view(request: HttpRequest, *args, **kwargs) -> HttpResponse: + return HttpResponse(status_code=200) + + urlpatterns = [ path( "", @@ -40,31 +44,16 @@ urlpatterns = [ name="impersonate-end", ), # Interfaces - path( - "if/admin/", - ensure_csrf_cookie(InterfaceView.as_view(template_name="if/admin.html")), - name="if-admin", - ), - path( - "if/user/", - ensure_csrf_cookie(InterfaceView.as_view(template_name="if/user.html")), - name="if-user", - ), - path( - "if/flow//", - ensure_csrf_cookie(FlowInterfaceView.as_view()), - name="if-flow", - ), path( "if/session-end//", ensure_csrf_cookie(EndSessionView.as_view()), name="if-session-end", ), # Fallback for WS - path("ws/outpost//", InterfaceView.as_view(template_name="if/admin.html")), + path("ws/outpost//", placeholder_view), path( "ws/client/", - InterfaceView.as_view(template_name="if/admin.html"), + placeholder_view, ), ] diff --git a/authentik/core/views/interface.py b/authentik/core/views/interface.py deleted file mode 100644 index 82f09752c..000000000 --- a/authentik/core/views/interface.py +++ /dev/null @@ -1,36 +0,0 @@ -"""Interface views""" -from json import dumps -from typing import Any - -from django.shortcuts import get_object_or_404 -from django.views.generic.base import TemplateView -from rest_framework.request import Request - -from authentik import get_build_hash -from authentik.admin.tasks import LOCAL_VERSION -from authentik.api.v3.config import ConfigView -from authentik.flows.models import Flow -from authentik.tenants.api import CurrentTenantSerializer - - -class InterfaceView(TemplateView): - """Base interface view""" - - def get_context_data(self, **kwargs: Any) -> dict[str, Any]: - kwargs["config_json"] = dumps(ConfigView(request=Request(self.request)).get_config().data) - kwargs["tenant_json"] = dumps(CurrentTenantSerializer(self.request.tenant).data) - kwargs["version_family"] = f"{LOCAL_VERSION.major}.{LOCAL_VERSION.minor}" - kwargs["version_subdomain"] = f"version-{LOCAL_VERSION.major}-{LOCAL_VERSION.minor}" - kwargs["build"] = get_build_hash() - return super().get_context_data(**kwargs) - - -class FlowInterfaceView(InterfaceView): - """Flow interface""" - - template_name = "if/flow.html" - - def get_context_data(self, **kwargs: Any) -> dict[str, Any]: - kwargs["flow"] = get_object_or_404(Flow, slug=self.kwargs.get("flow_slug")) - kwargs["inspector"] = "inspector" in self.request.GET - return super().get_context_data(**kwargs) diff --git a/authentik/interfaces/__init__.py b/authentik/interfaces/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/authentik/interfaces/api.py b/authentik/interfaces/api.py new file mode 100644 index 000000000..43fe038f4 --- /dev/null +++ b/authentik/interfaces/api.py @@ -0,0 +1,20 @@ +from rest_framework.serializers import ModelSerializer +from rest_framework.viewsets import ModelViewSet + +from authentik.interfaces.models import Interface + + +class InterfaceSerializer(ModelSerializer): + class Meta: + model = Interface + fields = [ + "interface_uuid", + "url_name", + "type", + "template", + ] + + +class InterfaceViewSet(ModelViewSet): + queryset = Interface.objects.all() + serializer_class = InterfaceSerializer diff --git a/authentik/interfaces/apps.py b/authentik/interfaces/apps.py new file mode 100644 index 000000000..3a3ba7a51 --- /dev/null +++ b/authentik/interfaces/apps.py @@ -0,0 +1,12 @@ +"""authentik interfaces app config""" +from authentik.blueprints.apps import ManagedAppConfig + + +class AuthentikInterfacesConfig(ManagedAppConfig): + """authentik interfaces app config""" + + name = "authentik.interfaces" + label = "authentik_interfaces" + verbose_name = "authentik Interfaces" + mountpoint = "if/" + default = True diff --git a/authentik/interfaces/migrations/0001_initial.py b/authentik/interfaces/migrations/0001_initial.py new file mode 100644 index 000000000..088745545 --- /dev/null +++ b/authentik/interfaces/migrations/0001_initial.py @@ -0,0 +1,36 @@ +# Generated by Django 4.1.7 on 2023-02-16 11:01 + +import uuid + +from django.db import migrations, models + + +class Migration(migrations.Migration): + initial = True + + dependencies = [] + + operations = [ + migrations.CreateModel( + name="Interface", + fields=[ + ( + "interface_uuid", + models.UUIDField( + default=uuid.uuid4, editable=False, primary_key=True, serialize=False + ), + ), + ("url_name", models.SlugField()), + ( + "type", + models.TextField( + choices=[("user", "User"), ("admin", "Admin"), ("flow", "Flow")] + ), + ), + ("template", models.TextField()), + ], + options={ + "abstract": False, + }, + ), + ] diff --git a/authentik/interfaces/migrations/__init__.py b/authentik/interfaces/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/authentik/interfaces/models.py b/authentik/interfaces/models.py new file mode 100644 index 000000000..cba2897eb --- /dev/null +++ b/authentik/interfaces/models.py @@ -0,0 +1,33 @@ +"""Interface models""" +from typing import Type +from uuid import uuid4 + +from django.db import models +from rest_framework.serializers import BaseSerializer + +from authentik.lib.models import SerializerModel + + +class InterfaceType(models.TextChoices): + """Interface types""" + + USER = "user" + ADMIN = "admin" + FLOW = "flow" + + +class Interface(SerializerModel): + """Interface""" + + interface_uuid = models.UUIDField(primary_key=True, editable=False, default=uuid4) + + url_name = models.SlugField() + + type = models.TextField(choices=InterfaceType.choices) + template = models.TextField() + + @property + def serializer(self) -> Type[BaseSerializer]: + from authentik.interfaces.api import InterfaceSerializer + + return InterfaceSerializer diff --git a/authentik/interfaces/urls.py b/authentik/interfaces/urls.py new file mode 100644 index 000000000..041ded9fc --- /dev/null +++ b/authentik/interfaces/urls.py @@ -0,0 +1,16 @@ +"""Interface urls""" +from django.urls import path + +from authentik.interfaces.views import InterfaceView + +urlpatterns = [ + path( + "/", + InterfaceView.as_view(), + kwargs={"flow_slug": None}, + name="if", + ), + path( + "//", InterfaceView.as_view(), name="if" + ), +] diff --git a/authentik/interfaces/views.py b/authentik/interfaces/views.py new file mode 100644 index 000000000..85171d0e6 --- /dev/null +++ b/authentik/interfaces/views.py @@ -0,0 +1,64 @@ +"""Interface views""" +from json import dumps +from typing import Any + +from django.http import Http404, HttpRequest, HttpResponse +from django.shortcuts import get_object_or_404 +from django.template import Template, TemplateSyntaxError, engines +from django.template.response import TemplateResponse +from django.views import View +from rest_framework.request import Request +from django.views.decorators.cache import cache_page +from django.utils.decorators import method_decorator +from django.views.decorators.csrf import ensure_csrf_cookie + +from authentik import get_build_hash +from authentik.admin.tasks import LOCAL_VERSION +from authentik.api.v3.config import ConfigView +from authentik.flows.models import Flow +from authentik.interfaces.models import Interface, InterfaceType +from authentik.tenants.api import CurrentTenantSerializer + + +def template_from_string(template_string: str) -> Template: + """Render template from string""" + chain = [] + engine_list = engines.all() + for engine in engine_list: + try: + return engine.from_string(template_string) + except TemplateSyntaxError as exc: + chain.append(exc) + raise TemplateSyntaxError(template_string, chain=chain) + + +@method_decorator(ensure_csrf_cookie, name="dispatch") +@method_decorator(cache_page(60 * 10), name="dispatch") +class InterfaceView(View): + """General interface view""" + + def get_context_data(self) -> dict[str, Any]: + """Get template context""" + return { + "config_json": dumps(ConfigView(request=Request(self.request)).get_config().data), + "tenant_json": dumps(CurrentTenantSerializer(self.request.tenant).data), + "version_family": f"{LOCAL_VERSION.major}.{LOCAL_VERSION.minor}", + "version_subdomain": f"version-{LOCAL_VERSION.major}-{LOCAL_VERSION.minor}", + "build": get_build_hash(), + } + + def type_flow(self, context: dict[str, Any]): + """Special handling for flow interfaces""" + if self.kwargs.get("flow_slug", None) is None: + raise Http404() + context["flow"] = get_object_or_404(Flow, slug=self.kwargs.get("flow_slug")) + context["inspector"] = "inspector" in self.request.GET + + def dispatch(self, request: HttpRequest, if_name: str, **kwargs: Any) -> HttpResponse: + context = self.get_context_data() + # TODO: Cache + interface: Interface = get_object_or_404(Interface, url_name=if_name) + if interface.type == InterfaceType.FLOW: + self.type_flow(context) + template = template_from_string(interface.template) + return TemplateResponse(request, template, context) diff --git a/authentik/root/settings.py b/authentik/root/settings.py index 36c815d49..c02847366 100644 --- a/authentik/root/settings.py +++ b/authentik/root/settings.py @@ -65,6 +65,7 @@ INSTALLED_APPS = [ "authentik.admin", "authentik.api", "authentik.crypto", + "authentik.interfaces", "authentik.events", "authentik.flows", "authentik.lib", diff --git a/blueprints/schema.json b/blueprints/schema.json index 90cec2985..d90c71d11 100644 --- a/blueprints/schema.json +++ b/blueprints/schema.json @@ -61,6 +61,7 @@ "authentik_events.notificationwebhookmapping", "authentik_flows.flow", "authentik_flows.flowstagebinding", + "authentik_interfaces.interface", "authentik_outposts.dockerserviceconnection", "authentik_outposts.kubernetesserviceconnection", "authentik_outposts.outpost", diff --git a/blueprints/system/interfaces.yaml b/blueprints/system/interfaces.yaml new file mode 100644 index 000000000..e8ba685a4 --- /dev/null +++ b/blueprints/system/interfaces.yaml @@ -0,0 +1,139 @@ +version: 1 +metadata: + labels: + blueprints.goauthentik.io/system: "true" + name: System - Interfaces +entries: + - model: authentik_interfaces.interface + identifiers: + url_name: user + type: user + attrs: + template: | + {% extends "base/skeleton.html" %} + + {% load static %} + {% load i18n %} + + {% block head %} + + + + + + {% include "base/header_js.html" %} + {% endblock %} + + {% block body %} + + +
+
+
+ + + + + +

+ {% trans "Loading..." %} +

+
+
+
+
+ {% endblock %} + - model: authentik_interfaces.interface + identifiers: + url_name: admin + type: admin + attrs: + template: | + {% extends "base/skeleton.html" %} + + {% load static %} + {% load i18n %} + + {% block head %} + + + + + + {% include "base/header_js.html" %} + {% endblock %} + + {% block body %} + + +
+
+
+ + + + + +

+ {% trans "Loading..." %} +

+
+
+
+
+ {% endblock %} + - model: authentik_interfaces.interface + identifiers: + url_name: flow + type: flow + attrs: + template: | + {% extends "base/skeleton.html" %} + + {% load static %} + {% load i18n %} + + {% block head_before %} + {{ block.super }} + + + + {% if flow.compatibility_mode and not inspector %} + + {% endif %} + {% include "base/header_js.html" %} + + {% endblock %} + + {% block head %} + + + {% endblock %} + + {% block body %} + + +
+
+
+ + + + + +

+ {% trans "Loading..." %} +

+
+
+
+
+ {% endblock %}