initial interfaces

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
This commit is contained in:
Jens Langhammer 2023-02-16 12:38:16 +01:00
parent bb92c4a967
commit e39c460e3a
No known key found for this signature in database
14 changed files with 333 additions and 55 deletions

View File

@ -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)

View File

@ -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/<slug:flow_slug>/",
ensure_csrf_cookie(FlowInterfaceView.as_view()),
name="if-flow",
),
path(
"if/session-end/<slug:application_slug>/",
ensure_csrf_cookie(EndSessionView.as_view()),
name="if-session-end",
),
# Fallback for WS
path("ws/outpost/<uuid:pk>/", InterfaceView.as_view(template_name="if/admin.html")),
path("ws/outpost/<uuid:pk>/", placeholder_view),
path(
"ws/client/",
InterfaceView.as_view(template_name="if/admin.html"),
placeholder_view,
),
]

View File

@ -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)

View File

View File

@ -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

View File

@ -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

View File

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

View File

@ -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

View File

@ -0,0 +1,16 @@
"""Interface urls"""
from django.urls import path
from authentik.interfaces.views import InterfaceView
urlpatterns = [
path(
"<slug:if_name>/",
InterfaceView.as_view(),
kwargs={"flow_slug": None},
name="if",
),
path(
"<slug:if_name>/<slug:flow_slug>/", InterfaceView.as_view(), name="if"
),
]

View File

@ -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)

View File

@ -65,6 +65,7 @@ INSTALLED_APPS = [
"authentik.admin",
"authentik.api",
"authentik.crypto",
"authentik.interfaces",
"authentik.events",
"authentik.flows",
"authentik.lib",

View File

@ -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",

View File

@ -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 %}
<script src="{% static 'dist/user/UserInterface.js' %}?version={{ version }}" type="module"></script>
<meta name="theme-color" content="#151515" media="(prefers-color-scheme: light)">
<meta name="theme-color" content="#151515" media="(prefers-color-scheme: dark)">
<link rel="icon" href="{{ tenant.branding_favicon }}">
<link rel="shortcut icon" href="{{ tenant.branding_favicon }}">
{% include "base/header_js.html" %}
{% endblock %}
{% block body %}
<ak-message-container></ak-message-container>
<ak-interface-user>
<section class="ak-static-page pf-c-page__main-section pf-m-no-padding-mobile pf-m-xl">
<div class="pf-c-empty-state" style="height: 100vh;">
<div class="pf-c-empty-state__content">
<span class="pf-c-spinner pf-m-xl pf-c-empty-state__icon" role="progressbar" aria-valuetext="{% trans 'Loading...' %}">
<span class="pf-c-spinner__clipper"></span>
<span class="pf-c-spinner__lead-ball"></span>
<span class="pf-c-spinner__tail-ball"></span>
</span>
<h1 class="pf-c-title pf-m-lg">
{% trans "Loading..." %}
</h1>
</div>
</div>
</section>
</ak-interface-user>
{% endblock %}
- model: authentik_interfaces.interface
identifiers:
url_name: admin
type: admin
attrs:
template: |
{% extends "base/skeleton.html" %}
{% load static %}
{% load i18n %}
{% block head %}
<script src="{% static 'dist/admin/AdminInterface.js' %}?version={{ version }}" type="module"></script>
<meta name="theme-color" content="#18191a" media="(prefers-color-scheme: dark)">
<meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)">
<link rel="icon" href="{{ tenant.branding_favicon }}">
<link rel="shortcut icon" href="{{ tenant.branding_favicon }}">
{% include "base/header_js.html" %}
{% endblock %}
{% block body %}
<ak-message-container></ak-message-container>
<ak-interface-admin>
<section class="ak-static-page pf-c-page__main-section pf-m-no-padding-mobile pf-m-xl">
<div class="pf-c-empty-state" style="height: 100vh;">
<div class="pf-c-empty-state__content">
<span class="pf-c-spinner pf-m-xl pf-c-empty-state__icon" role="progressbar" aria-valuetext="{% trans 'Loading...' %}">
<span class="pf-c-spinner__clipper"></span>
<span class="pf-c-spinner__lead-ball"></span>
<span class="pf-c-spinner__tail-ball"></span>
</span>
<h1 class="pf-c-title pf-m-lg">
{% trans "Loading..." %}
</h1>
</div>
</div>
</section>
</ak-interface-admin>
{% 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 }}
<link rel="prefetch" href="{{ flow.background_url }}" />
<link rel="icon" href="{{ tenant.branding_favicon }}">
<link rel="shortcut icon" href="{{ tenant.branding_favicon }}">
{% if flow.compatibility_mode and not inspector %}
<script>ShadyDOM = { force: !navigator.webdriver };</script>
{% endif %}
{% include "base/header_js.html" %}
<script>
window.authentik.flow = {
"layout": "{{ flow.layout }}",
};
</script>
{% endblock %}
{% block head %}
<script src="{% static 'dist/flow/FlowInterface.js' %}?version={{ version }}" type="module"></script>
<style>
:root {
--ak-flow-background: url("{{ flow.background_url }}");
}
</style>
{% endblock %}
{% block body %}
<ak-message-container></ak-message-container>
<ak-flow-executor>
<section class="ak-static-page pf-c-page__main-section pf-m-no-padding-mobile pf-m-xl">
<div class="pf-c-empty-state" style="height: 100vh;">
<div class="pf-c-empty-state__content">
<span class="pf-c-spinner pf-m-xl pf-c-empty-state__icon" role="progressbar" aria-valuetext="{% trans 'Loading...' %}">
<span class="pf-c-spinner__clipper"></span>
<span class="pf-c-spinner__lead-ball"></span>
<span class="pf-c-spinner__tail-ball"></span>
</span>
<h1 class="pf-c-title pf-m-lg">
{% trans "Loading..." %}
</h1>
</div>
</div>
</section>
</ak-flow-executor>
{% endblock %}