initial interfaces
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
This commit is contained in:
parent
bb92c4a967
commit
e39c460e3a
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
]
|
||||
|
||||
|
|
|
@ -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)
|
0
authentik/interfaces/__init__.py
Normal file
0
authentik/interfaces/__init__.py
Normal file
20
authentik/interfaces/api.py
Normal file
20
authentik/interfaces/api.py
Normal 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
|
12
authentik/interfaces/apps.py
Normal file
12
authentik/interfaces/apps.py
Normal 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
|
36
authentik/interfaces/migrations/0001_initial.py
Normal file
36
authentik/interfaces/migrations/0001_initial.py
Normal 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,
|
||||
},
|
||||
),
|
||||
]
|
0
authentik/interfaces/migrations/__init__.py
Normal file
0
authentik/interfaces/migrations/__init__.py
Normal file
33
authentik/interfaces/models.py
Normal file
33
authentik/interfaces/models.py
Normal 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
|
16
authentik/interfaces/urls.py
Normal file
16
authentik/interfaces/urls.py
Normal 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"
|
||||
),
|
||||
]
|
64
authentik/interfaces/views.py
Normal file
64
authentik/interfaces/views.py
Normal 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)
|
|
@ -65,6 +65,7 @@ INSTALLED_APPS = [
|
|||
"authentik.admin",
|
||||
"authentik.api",
|
||||
"authentik.crypto",
|
||||
"authentik.interfaces",
|
||||
"authentik.events",
|
||||
"authentik.flows",
|
||||
"authentik.lib",
|
||||
|
|
|
@ -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",
|
||||
|
|
139
blueprints/system/interfaces.yaml
Normal file
139
blueprints/system/interfaces.yaml
Normal 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 %}
|
Reference in a new issue