Compare commits

...
This repository has been archived on 2024-05-31. You can view files and clone it, but cannot push or open issues or pull requests.

10 commits

Author SHA1 Message Date
Jens Langhammer a1c1c3a27c
fix lint
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-04-21 14:40:20 +03:00
Jens Langhammer c0262f0802
use wrapper for get_tenant, give fallback interfaces
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-04-21 14:37:34 +03:00
Jens Langhammer c6f8290ca1
hmm fallback tenant
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-04-21 14:35:48 +03:00
Jens Langhammer 905ae00e02
more fixes
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-04-21 14:35:48 +03:00
Jens Langhammer 3ec477d58d
fix more tests
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-04-21 14:35:48 +03:00
Jens Langhammer ff996f798f
start fixing tests
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-04-21 14:35:48 +03:00
Jens Langhammer 1889e82309
start fixing tests
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-04-21 14:35:32 +03:00
Jens Langhammer 48a4080699
add api and webui
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-04-21 14:35:32 +03:00
Jens Langhammer 246a6c7384
add tenant migration, migrate default urls and redirects
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-04-21 14:35:32 +03:00
Jens Langhammer e39c460e3a
initial interfaces
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-04-21 14:34:41 +03:00
60 changed files with 1478 additions and 170 deletions

View file

@ -18,6 +18,7 @@ from authentik.core.api.utils import PassiveSerializer
from authentik.lib.utils.reflection import get_env from authentik.lib.utils.reflection import get_env
from authentik.outposts.apps import MANAGED_OUTPOST from authentik.outposts.apps import MANAGED_OUTPOST
from authentik.outposts.models import Outpost from authentik.outposts.models import Outpost
from authentik.tenants.utils import get_tenant
class RuntimeDict(TypedDict): class RuntimeDict(TypedDict):
@ -77,7 +78,7 @@ class SystemSerializer(PassiveSerializer):
def get_tenant(self, request: Request) -> str: def get_tenant(self, request: Request) -> str:
"""Currently active tenant""" """Currently active tenant"""
return str(request._request.tenant) return str(get_tenant(request))
def get_server_time(self, request: Request) -> datetime: def get_server_time(self, request: Request) -> datetime:
"""Current server time""" """Current server time"""

View file

@ -33,6 +33,7 @@ from authentik.flows.api.flows import FlowViewSet
from authentik.flows.api.stages import StageViewSet from authentik.flows.api.stages import StageViewSet
from authentik.flows.views.executor import FlowExecutorView from authentik.flows.views.executor import FlowExecutorView
from authentik.flows.views.inspector import FlowInspectorView from authentik.flows.views.inspector import FlowInspectorView
from authentik.interfaces.api import InterfaceViewSet
from authentik.outposts.api.outposts import OutpostViewSet from authentik.outposts.api.outposts import OutpostViewSet
from authentik.outposts.api.service_connections import ( from authentik.outposts.api.service_connections import (
DockerServiceConnectionViewSet, DockerServiceConnectionViewSet,
@ -123,6 +124,8 @@ router.register("core/user_consent", UserConsentViewSet)
router.register("core/tokens", TokenViewSet) router.register("core/tokens", TokenViewSet)
router.register("core/tenants", TenantViewSet) router.register("core/tenants", TenantViewSet)
router.register("interfaces", InterfaceViewSet)
router.register("outposts/instances", OutpostViewSet) router.register("outposts/instances", OutpostViewSet)
router.register("outposts/service_connections/all", ServiceConnectionViewSet) router.register("outposts/service_connections/all", ServiceConnectionViewSet)
router.register("outposts/service_connections/docker", DockerServiceConnectionViewSet) router.register("outposts/service_connections/docker", DockerServiceConnectionViewSet)

View file

@ -10,7 +10,6 @@ from django.db.models.functions import ExtractHour
from django.db.models.query import QuerySet from django.db.models.query import QuerySet
from django.db.transaction import atomic from django.db.transaction import atomic
from django.db.utils import IntegrityError from django.db.utils import IntegrityError
from django.urls import reverse_lazy
from django.utils.http import urlencode from django.utils.http import urlencode
from django.utils.text import slugify from django.utils.text import slugify
from django.utils.timezone import now from django.utils.timezone import now
@ -72,10 +71,12 @@ from authentik.flows.exceptions import FlowNonApplicableException
from authentik.flows.models import FlowToken from authentik.flows.models import FlowToken
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlanner from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlanner
from authentik.flows.views.executor import QS_KEY_TOKEN from authentik.flows.views.executor import QS_KEY_TOKEN
from authentik.interfaces.models import InterfaceType
from authentik.interfaces.views import reverse_interface
from authentik.stages.email.models import EmailStage from authentik.stages.email.models import EmailStage
from authentik.stages.email.tasks import send_mails from authentik.stages.email.tasks import send_mails
from authentik.stages.email.utils import TemplateEmailMessage from authentik.stages.email.utils import TemplateEmailMessage
from authentik.tenants.models import Tenant from authentik.tenants.utils import get_tenant
LOGGER = get_logger() LOGGER = get_logger()
@ -321,7 +322,7 @@ class UserViewSet(UsedByMixin, ModelViewSet):
def _create_recovery_link(self) -> tuple[Optional[str], Optional[Token]]: def _create_recovery_link(self) -> tuple[Optional[str], Optional[Token]]:
"""Create a recovery link (when the current tenant has a recovery flow set), """Create a recovery link (when the current tenant has a recovery flow set),
that can either be shown to an admin or sent to the user directly""" that can either be shown to an admin or sent to the user directly"""
tenant: Tenant = self.request._request.tenant tenant = get_tenant(self.request)
# Check that there is a recovery flow, if not return an error # Check that there is a recovery flow, if not return an error
flow = tenant.flow_recovery flow = tenant.flow_recovery
if not flow: if not flow:
@ -350,8 +351,12 @@ class UserViewSet(UsedByMixin, ModelViewSet):
) )
querystring = urlencode({QS_KEY_TOKEN: token.key}) querystring = urlencode({QS_KEY_TOKEN: token.key})
link = self.request.build_absolute_uri( link = self.request.build_absolute_uri(
reverse_lazy("authentik_core:if-flow", kwargs={"flow_slug": flow.slug}) reverse_interface(
+ f"?{querystring}" self.request,
InterfaceType.FLOW,
flow_slug=flow.slug,
),
+f"?{querystring}",
) )
return link, token return link, token

View file

@ -33,6 +33,7 @@ from authentik.lib.models import (
) )
from authentik.lib.utils.http import get_client_ip from authentik.lib.utils.http import get_client_ip
from authentik.policies.models import PolicyBindingModel from authentik.policies.models import PolicyBindingModel
from authentik.tenants.utils import get_tenant
LOGGER = get_logger() LOGGER = get_logger()
USER_ATTRIBUTE_DEBUG = "goauthentik.io/user/debug" USER_ATTRIBUTE_DEBUG = "goauthentik.io/user/debug"
@ -168,7 +169,7 @@ class User(SerializerModel, GuardianUserMixin, AbstractUser):
including the users attributes""" including the users attributes"""
final_attributes = {} final_attributes = {}
if request and hasattr(request, "tenant"): if request and hasattr(request, "tenant"):
always_merger.merge(final_attributes, request.tenant.attributes) always_merger.merge(final_attributes, get_tenant(request).attributes)
for group in self.ak_groups.all().order_by("name"): for group in self.ak_groups.all().order_by("name"):
always_merger.merge(final_attributes, group.attributes) always_merger.merge(final_attributes, group.attributes)
always_merger.merge(final_attributes, self.attributes) always_merger.merge(final_attributes, self.attributes)
@ -227,7 +228,7 @@ class User(SerializerModel, GuardianUserMixin, AbstractUser):
except Exception as exc: except Exception as exc:
LOGGER.warning("Failed to get default locale", exc=exc) LOGGER.warning("Failed to get default locale", exc=exc)
if request: if request:
return request.tenant.locale return get_tenant(request).default_locale
return "" return ""
@property @property

View file

@ -25,7 +25,8 @@ from authentik.flows.planner import (
) )
from authentik.flows.stage import StageView from authentik.flows.stage import StageView
from authentik.flows.views.executor import NEXT_ARG_NAME, SESSION_KEY_GET, SESSION_KEY_PLAN from authentik.flows.views.executor import NEXT_ARG_NAME, SESSION_KEY_GET, SESSION_KEY_PLAN
from authentik.lib.utils.urls import redirect_with_qs from authentik.interfaces.models import InterfaceType
from authentik.interfaces.views import redirect_to_default_interface
from authentik.lib.views import bad_request_message from authentik.lib.views import bad_request_message
from authentik.policies.denied import AccessDeniedResponse from authentik.policies.denied import AccessDeniedResponse
from authentik.policies.utils import delete_none_keys from authentik.policies.utils import delete_none_keys
@ -226,7 +227,7 @@ class SourceFlowManager:
# Ensure redirect is carried through when user was trying to # Ensure redirect is carried through when user was trying to
# authorize application # authorize application
final_redirect = self.request.session.get(SESSION_KEY_GET, {}).get( final_redirect = self.request.session.get(SESSION_KEY_GET, {}).get(
NEXT_ARG_NAME, "authentik_core:if-user" NEXT_ARG_NAME, "authentik_core:root-redirect"
) )
kwargs.update( kwargs.update(
{ {
@ -253,9 +254,9 @@ class SourceFlowManager:
for stage in stages: for stage in stages:
plan.append_stage(stage) plan.append_stage(stage)
self.request.session[SESSION_KEY_PLAN] = plan self.request.session[SESSION_KEY_PLAN] = plan
return redirect_with_qs( return redirect_to_default_interface(
"authentik_core:if-flow", self.request,
self.request.GET, InterfaceType.FLOW,
flow_slug=flow.slug, flow_slug=flow.slug,
) )
@ -299,8 +300,9 @@ class SourceFlowManager:
_("Successfully linked %(source)s!" % {"source": self.source.name}), _("Successfully linked %(source)s!" % {"source": self.source.name}),
) )
return redirect( return redirect(
# Not ideal that we don't directly redirect to the configured user interface
reverse( reverse(
"authentik_core:if-user", "authentik_core:root-redirect",
) )
+ f"#/settings;page-{self.source.slug}" + f"#/settings;page-{self.source.slug}"
) )

View file

@ -59,4 +59,6 @@ class TestImpersonation(TestCase):
self.client.force_login(self.other_user) self.client.force_login(self.other_user)
response = self.client.get(reverse("authentik_core:impersonate-end")) response = self.client.get(reverse("authentik_core:impersonate-end"))
self.assertRedirects(response, reverse("authentik_core:if-user")) self.assertRedirects(
response, reverse("authentik_interfaces:if", kwargs={"if_name": "user"})
)

View file

@ -3,23 +3,30 @@ from channels.auth import AuthMiddleware
from channels.sessions import CookieMiddleware from channels.sessions import CookieMiddleware
from django.conf import settings from django.conf import settings
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.http import HttpRequest, HttpResponse
from django.urls import path from django.urls import path
from django.views.decorators.csrf import ensure_csrf_cookie from django.views.decorators.csrf import ensure_csrf_cookie
from django.views.generic import RedirectView
from authentik.core.views import apps, impersonate from authentik.core.views import apps, impersonate
from authentik.core.views.debug import AccessDeniedView from authentik.core.views.debug import AccessDeniedView
from authentik.core.views.interface import FlowInterfaceView, InterfaceView
from authentik.core.views.session import EndSessionView from authentik.core.views.session import EndSessionView
from authentik.interfaces.models import InterfaceType
from authentik.interfaces.views import RedirectToInterface
from authentik.root.asgi_middleware import SessionMiddleware from authentik.root.asgi_middleware import SessionMiddleware
from authentik.root.messages.consumer import MessageConsumer from authentik.root.messages.consumer import MessageConsumer
def placeholder_view(request: HttpRequest, *args, **kwargs) -> HttpResponse:
"""Empty view used as placeholder
(Mounted to websocket endpoints and used by e2e tests)"""
return HttpResponse(status_code=200)
urlpatterns = [ urlpatterns = [
path( path(
"", "",
login_required( login_required(RedirectToInterface.as_view(type=InterfaceType.USER)),
RedirectView.as_view(pattern_name="authentik_core:if-user", query_string=True)
),
name="root-redirect", name="root-redirect",
), ),
path( path(
@ -40,31 +47,16 @@ urlpatterns = [
name="impersonate-end", name="impersonate-end",
), ),
# Interfaces # 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( path(
"if/session-end/<slug:application_slug>/", "if/session-end/<slug:application_slug>/",
ensure_csrf_cookie(EndSessionView.as_view()), ensure_csrf_cookie(EndSessionView.as_view()),
name="if-session-end", name="if-session-end",
), ),
# Fallback for WS # Fallback for WS
path("ws/outpost/<uuid:pk>/", InterfaceView.as_view(template_name="if/admin.html")), path("ws/outpost/<uuid:pk>/", placeholder_view),
path( path(
"ws/client/", "ws/client/",
InterfaceView.as_view(template_name="if/admin.html"), placeholder_view,
), ),
] ]

View file

@ -20,11 +20,13 @@ from authentik.flows.views.executor import (
SESSION_KEY_PLAN, SESSION_KEY_PLAN,
ToDefaultFlow, ToDefaultFlow,
) )
from authentik.lib.utils.urls import redirect_with_qs from authentik.interfaces.models import InterfaceType
from authentik.interfaces.views import redirect_to_default_interface
from authentik.stages.consent.stage import ( from authentik.stages.consent.stage import (
PLAN_CONTEXT_CONSENT_HEADER, PLAN_CONTEXT_CONSENT_HEADER,
PLAN_CONTEXT_CONSENT_PERMISSIONS, PLAN_CONTEXT_CONSENT_PERMISSIONS,
) )
from authentik.tenants.utils import get_tenant
class RedirectToAppLaunch(View): class RedirectToAppLaunch(View):
@ -59,7 +61,7 @@ class RedirectToAppLaunch(View):
raise Http404 raise Http404
plan.insert_stage(in_memory_stage(RedirectToAppStage)) plan.insert_stage(in_memory_stage(RedirectToAppStage))
request.session[SESSION_KEY_PLAN] = plan request.session[SESSION_KEY_PLAN] = plan
return redirect_with_qs("authentik_core:if-flow", request.GET, flow_slug=flow.slug) return redirect_to_default_interface(request, InterfaceType.FLOW, flow_slug=flow.slug)
class RedirectToAppStage(ChallengeStageView): class RedirectToAppStage(ChallengeStageView):

View file

@ -35,7 +35,7 @@ class ImpersonateInitView(View):
Event.new(EventAction.IMPERSONATION_STARTED).from_http(request, user_to_be) Event.new(EventAction.IMPERSONATION_STARTED).from_http(request, user_to_be)
return redirect("authentik_core:if-user") return redirect("authentik_core:root-redirect")
class ImpersonateEndView(View): class ImpersonateEndView(View):
@ -48,7 +48,7 @@ class ImpersonateEndView(View):
or SESSION_KEY_IMPERSONATE_ORIGINAL_USER not in request.session or SESSION_KEY_IMPERSONATE_ORIGINAL_USER not in request.session
): ):
LOGGER.debug("Can't end impersonation", user=request.user) LOGGER.debug("Can't end impersonation", user=request.user)
return redirect("authentik_core:if-user") return redirect("authentik_core:root-redirect")
original_user = request.session[SESSION_KEY_IMPERSONATE_ORIGINAL_USER] original_user = request.session[SESSION_KEY_IMPERSONATE_ORIGINAL_USER]

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

@ -41,8 +41,7 @@ from authentik.lib.utils.http import get_client_ip, get_http_session
from authentik.lib.utils.time import timedelta_from_string from authentik.lib.utils.time import timedelta_from_string
from authentik.policies.models import PolicyBindingModel from authentik.policies.models import PolicyBindingModel
from authentik.stages.email.utils import TemplateEmailMessage from authentik.stages.email.utils import TemplateEmailMessage
from authentik.tenants.models import Tenant from authentik.tenants.utils import get_fallback_tenant, get_tenant
from authentik.tenants.utils import DEFAULT_TENANT
LOGGER = get_logger() LOGGER = get_logger()
if TYPE_CHECKING: if TYPE_CHECKING:
@ -57,7 +56,7 @@ def default_event_duration():
def default_tenant(): def default_tenant():
"""Get a default value for tenant""" """Get a default value for tenant"""
return sanitize_dict(model_to_dict(DEFAULT_TENANT)) return sanitize_dict(model_to_dict(get_fallback_tenant()))
class NotificationTransportError(SentryIgnoredException): class NotificationTransportError(SentryIgnoredException):
@ -227,7 +226,7 @@ class Event(SerializerModel, ExpiringModel):
wrapped = self.context["http_request"]["args"][QS_QUERY] wrapped = self.context["http_request"]["args"][QS_QUERY]
self.context["http_request"]["args"] = QueryDict(wrapped) self.context["http_request"]["args"] = QueryDict(wrapped)
if hasattr(request, "tenant"): if hasattr(request, "tenant"):
tenant: Tenant = request.tenant tenant = get_tenant(request)
# Because self.created only gets set on save, we can't use it's value here # Because self.created only gets set on save, we can't use it's value here
# hence we set self.created to now and then use it # hence we set self.created to now and then use it
self.created = now() self.created = now()

View file

@ -25,6 +25,8 @@ from authentik.flows.exceptions import FlowNonApplicableException
from authentik.flows.models import Flow from authentik.flows.models import Flow
from authentik.flows.planner import CACHE_PREFIX, PLAN_CONTEXT_PENDING_USER, FlowPlanner, cache_key from authentik.flows.planner import CACHE_PREFIX, PLAN_CONTEXT_PENDING_USER, FlowPlanner, cache_key
from authentik.flows.views.executor import SESSION_KEY_HISTORY, SESSION_KEY_PLAN from authentik.flows.views.executor import SESSION_KEY_HISTORY, SESSION_KEY_PLAN
from authentik.interfaces.models import InterfaceType
from authentik.interfaces.views import reverse_interface
from authentik.lib.utils.file import ( from authentik.lib.utils.file import (
FilePathSerializer, FilePathSerializer,
FileUploadSerializer, FileUploadSerializer,
@ -294,7 +296,11 @@ class FlowViewSet(UsedByMixin, ModelViewSet):
return Response( return Response(
{ {
"link": request._request.build_absolute_uri( "link": request._request.build_absolute_uri(
reverse("authentik_core:if-flow", kwargs={"flow_slug": flow.slug}) reverse_interface(
request,
InterfaceType.FLOW,
flow_slug=flow.slug,
),
) )
} }
) )

View file

@ -7,6 +7,8 @@ from authentik.core.tests.utils import create_test_flow
from authentik.flows.models import Flow, FlowDesignation from authentik.flows.models import Flow, FlowDesignation
from authentik.flows.planner import FlowPlan from authentik.flows.planner import FlowPlan
from authentik.flows.views.executor import SESSION_KEY_APPLICATION_PRE, SESSION_KEY_PLAN from authentik.flows.views.executor import SESSION_KEY_APPLICATION_PRE, SESSION_KEY_PLAN
from authentik.interfaces.models import InterfaceType
from authentik.interfaces.tests import reverse_interface
from authentik.lib.generators import generate_id from authentik.lib.generators import generate_id
from authentik.providers.oauth2.models import OAuth2Provider from authentik.providers.oauth2.models import OAuth2Provider
@ -21,7 +23,10 @@ class TestHelperView(TestCase):
response = self.client.get( response = self.client.get(
reverse("authentik_flows:default-invalidation"), reverse("authentik_flows:default-invalidation"),
) )
expected_url = reverse("authentik_core:if-flow", kwargs={"flow_slug": flow.slug}) expected_url = reverse_interface(
InterfaceType.FLOW,
flow_slug=flow.slug,
)
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, expected_url) self.assertEqual(response.url, expected_url)
@ -72,6 +77,9 @@ class TestHelperView(TestCase):
response = self.client.get( response = self.client.get(
reverse("authentik_flows:default-invalidation"), reverse("authentik_flows:default-invalidation"),
) )
expected_url = reverse("authentik_core:if-flow", kwargs={"flow_slug": flow.slug}) expected_url = reverse_interface(
InterfaceType.FLOW,
flow_slug=flow.slug,
)
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, expected_url) self.assertEqual(response.url, expected_url)

View file

@ -53,12 +53,14 @@ from authentik.flows.planner import (
FlowPlanner, FlowPlanner,
) )
from authentik.flows.stage import AccessDeniedChallengeView, StageView from authentik.flows.stage import AccessDeniedChallengeView, StageView
from authentik.interfaces.models import InterfaceType
from authentik.interfaces.views import redirect_to_default_interface
from authentik.lib.sentry import SentryIgnoredException from authentik.lib.sentry import SentryIgnoredException
from authentik.lib.utils.errors import exception_to_string from authentik.lib.utils.errors import exception_to_string
from authentik.lib.utils.reflection import all_subclasses, class_to_path from authentik.lib.utils.reflection import all_subclasses, class_to_path
from authentik.lib.utils.urls import is_url_absolute, redirect_with_qs from authentik.lib.utils.urls import is_url_absolute, redirect_with_qs
from authentik.policies.engine import PolicyEngine from authentik.policies.engine import PolicyEngine
from authentik.tenants.models import Tenant from authentik.tenants.utils import get_tenant
LOGGER = get_logger() LOGGER = get_logger()
# Argument used to redirect user after login # Argument used to redirect user after login
@ -479,7 +481,7 @@ class ToDefaultFlow(View):
def get_flow(self) -> Flow: def get_flow(self) -> Flow:
"""Get a flow for the selected designation""" """Get a flow for the selected designation"""
tenant: Tenant = self.request.tenant tenant = get_tenant(self.request)
flow = None flow = None
# First, attempt to get default flow from tenant # First, attempt to get default flow from tenant
if self.designation == FlowDesignation.AUTHENTICATION: if self.designation == FlowDesignation.AUTHENTICATION:
@ -512,7 +514,7 @@ class ToDefaultFlow(View):
flow_slug=flow.slug, flow_slug=flow.slug,
) )
del self.request.session[SESSION_KEY_PLAN] del self.request.session[SESSION_KEY_PLAN]
return redirect_with_qs("authentik_core:if-flow", request.GET, flow_slug=flow.slug) return redirect_to_default_interface(request, InterfaceType.FLOW, flow_slug=flow.slug)
def to_stage_response(request: HttpRequest, source: HttpResponse) -> HttpResponse: def to_stage_response(request: HttpRequest, source: HttpResponse) -> HttpResponse:
@ -583,8 +585,8 @@ class ConfigureFlowInitView(LoginRequiredMixin, View):
LOGGER.warning("Flow not applicable to user") LOGGER.warning("Flow not applicable to user")
raise Http404 raise Http404
request.session[SESSION_KEY_PLAN] = plan request.session[SESSION_KEY_PLAN] = plan
return redirect_with_qs( return redirect_to_default_interface(
"authentik_core:if-flow", self.request,
self.request.GET, InterfaceType.FLOW,
flow_slug=stage.configure_flow.slug, flow_slug=stage.configure_flow.slug,
) )

View file

View file

@ -0,0 +1,28 @@
"""interfaces API"""
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet
from authentik.core.api.used_by import UsedByMixin
from authentik.interfaces.models import Interface
class InterfaceSerializer(ModelSerializer):
"""Interface serializer"""
class Meta:
model = Interface
fields = [
"interface_uuid",
"url_name",
"type",
"template",
]
class InterfaceViewSet(UsedByMixin, ModelViewSet):
"""Interface serializer"""
queryset = Interface.objects.all()
serializer_class = InterfaceSerializer
filterset_fields = ["url_name", "type", "template"]
search_fields = ["url_name", "type", "template"]

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(unique=True)),
(
"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(unique=True)
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,12 @@
"""Interface tests"""
from django.test import RequestFactory
from authentik.interfaces.models import InterfaceType
from authentik.interfaces.views import reverse_interface as full_reverse_interface
def reverse_interface(interface_type: InterfaceType, **kwargs):
"""reverse_interface wrapper for tests"""
factory = RequestFactory()
request = factory.get("/")
return full_reverse_interface(request, interface_type, **kwargs)

View file

@ -0,0 +1,14 @@
"""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,113 @@
"""Interface views"""
from json import dumps
from typing import Any, Optional
from urllib.parse import urlencode
from django.http import Http404, HttpRequest, HttpResponse, QueryDict
from django.shortcuts import get_object_or_404, redirect
from django.template import Template, TemplateSyntaxError, engines
from django.template.response import TemplateResponse
from django.utils.decorators import method_decorator
from django.views import View
from django.views.decorators.cache import cache_page
from django.views.decorators.csrf import ensure_csrf_cookie
from rest_framework.request import Request
from structlog.stdlib import get_logger
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.lib.utils.urls import reverse_with_qs
from authentik.tenants.api import CurrentTenantSerializer
from authentik.tenants.utils import get_tenant
LOGGER = get_logger()
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)
def redirect_to_default_interface(request: HttpRequest, interface_type: InterfaceType, **kwargs):
"""Shortcut to inline redirect to default interface,
keeping GET parameters of the passed request"""
return RedirectToInterface.as_view(type=interface_type)(request, **kwargs)
def reverse_interface(
request: HttpRequest, interface_type: InterfaceType, query: Optional[QueryDict] = None, **kwargs
):
"""Reverse URL to configured default interface"""
tenant = get_tenant(request)
interface: Interface = None
if interface_type == InterfaceType.USER:
interface = tenant.interface_user
if interface_type == InterfaceType.ADMIN:
interface = tenant.interface_admin
if interface_type == InterfaceType.FLOW:
interface = tenant.interface_flow
if not interface:
LOGGER.warning("No interface found", type=interface_type, tenant=tenant)
raise Http404()
kwargs["if_name"] = interface.url_name
return reverse_with_qs(
"authentik_interfaces:if",
query=query or request.GET,
kwargs=kwargs,
)
class RedirectToInterface(View):
"""Redirect to tenant's configured view for specified type"""
type: Optional[InterfaceType] = None
def dispatch(self, request: HttpRequest, **kwargs: Any) -> HttpResponse:
target = reverse_interface(request, self.type, **kwargs)
if self.request.GET:
target += "?" + urlencode(self.request.GET.items())
return redirect(target)
@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(get_tenant(self.request)).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

@ -12,6 +12,7 @@ from authentik.lib.utils.http import get_http_session
from authentik.policies.models import Policy from authentik.policies.models import Policy
from authentik.policies.types import PolicyRequest, PolicyResult from authentik.policies.types import PolicyRequest, PolicyResult
from authentik.stages.prompt.stage import PLAN_CONTEXT_PROMPT from authentik.stages.prompt.stage import PLAN_CONTEXT_PROMPT
from authentik.tenants.utils import get_tenant
LOGGER = get_logger() LOGGER = get_logger()
RE_LOWER = re.compile("[a-z]") RE_LOWER = re.compile("[a-z]")
@ -143,7 +144,8 @@ class PasswordPolicy(Policy):
user_inputs.append(request.user.name) user_inputs.append(request.user.name)
user_inputs.append(request.user.email) user_inputs.append(request.user.email)
if request.http_request: if request.http_request:
user_inputs.append(request.http_request.tenant.branding_title) tenant = get_tenant(request.http_request)
user_inputs.append(tenant.branding_title)
# Only calculate result for the first 100 characters, as with over 100 char # Only calculate result for the first 100 characters, as with over 100 char
# long passwords we can be reasonably sure that they'll surpass the score anyways # long passwords we can be reasonably sure that they'll surpass the score anyways
# See https://github.com/dropbox/zxcvbn#runtime-latency # See https://github.com/dropbox/zxcvbn#runtime-latency

View file

@ -39,8 +39,9 @@ class TesOAuth2DeviceInit(OAuthTestCase):
self.assertEqual( self.assertEqual(
res.url, res.url,
reverse( reverse(
"authentik_core:if-flow", "authentik_interfaces:if",
kwargs={ kwargs={
"if_name": "flow",
"flow_slug": self.device_flow.slug, "flow_slug": self.device_flow.slug,
}, },
), ),
@ -68,8 +69,9 @@ class TesOAuth2DeviceInit(OAuthTestCase):
self.assertEqual( self.assertEqual(
res.url, res.url,
reverse( reverse(
"authentik_core:if-flow", "authentik_interfaces:if",
kwargs={ kwargs={
"if_name": "flow",
"flow_slug": self.provider.authorization_flow.slug, "flow_slug": self.provider.authorization_flow.slug,
}, },
) )

View file

@ -29,8 +29,9 @@ from authentik.flows.models import in_memory_stage
from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, PLAN_CONTEXT_SSO, FlowPlanner from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, PLAN_CONTEXT_SSO, FlowPlanner
from authentik.flows.stage import StageView from authentik.flows.stage import StageView
from authentik.flows.views.executor import SESSION_KEY_PLAN from authentik.flows.views.executor import SESSION_KEY_PLAN
from authentik.interfaces.models import InterfaceType
from authentik.interfaces.views import redirect_to_default_interface
from authentik.lib.utils.time import timedelta_from_string from authentik.lib.utils.time import timedelta_from_string
from authentik.lib.utils.urls import redirect_with_qs
from authentik.lib.views import bad_request_message from authentik.lib.views import bad_request_message
from authentik.policies.types import PolicyRequest from authentik.policies.types import PolicyRequest
from authentik.policies.views import PolicyAccessView, RequestValidationError from authentik.policies.views import PolicyAccessView, RequestValidationError
@ -404,9 +405,9 @@ class AuthorizationFlowInitView(PolicyAccessView):
plan.append_stage(in_memory_stage(OAuthFulfillmentStage)) plan.append_stage(in_memory_stage(OAuthFulfillmentStage))
self.request.session[SESSION_KEY_PLAN] = plan self.request.session[SESSION_KEY_PLAN] = plan
return redirect_with_qs( return redirect_to_default_interface(
"authentik_core:if-flow", self.request,
self.request.GET, InterfaceType.FLOW,
flow_slug=self.provider.authorization_flow.slug, flow_slug=self.provider.authorization_flow.slug,
) )

View file

@ -15,7 +15,8 @@ from authentik.flows.models import in_memory_stage
from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, PLAN_CONTEXT_SSO, FlowPlanner from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, PLAN_CONTEXT_SSO, FlowPlanner
from authentik.flows.stage import ChallengeStageView from authentik.flows.stage import ChallengeStageView
from authentik.flows.views.executor import SESSION_KEY_PLAN from authentik.flows.views.executor import SESSION_KEY_PLAN
from authentik.lib.utils.urls import redirect_with_qs from authentik.interfaces.models import InterfaceType
from authentik.interfaces.views import redirect_to_default_interface
from authentik.providers.oauth2.models import DeviceToken, OAuth2Provider from authentik.providers.oauth2.models import DeviceToken, OAuth2Provider
from authentik.providers.oauth2.views.device_finish import ( from authentik.providers.oauth2.views.device_finish import (
PLAN_CONTEXT_DEVICE, PLAN_CONTEXT_DEVICE,
@ -26,7 +27,7 @@ from authentik.stages.consent.stage import (
PLAN_CONTEXT_CONSENT_HEADER, PLAN_CONTEXT_CONSENT_HEADER,
PLAN_CONTEXT_CONSENT_PERMISSIONS, PLAN_CONTEXT_CONSENT_PERMISSIONS,
) )
from authentik.tenants.models import Tenant from authentik.tenants.utils import get_tenant
LOGGER = get_logger() LOGGER = get_logger()
QS_KEY_CODE = "code" # nosec QS_KEY_CODE = "code" # nosec
@ -77,9 +78,9 @@ def validate_code(code: int, request: HttpRequest) -> Optional[HttpResponse]:
return None return None
plan.insert_stage(in_memory_stage(OAuthDeviceCodeFinishStage)) plan.insert_stage(in_memory_stage(OAuthDeviceCodeFinishStage))
request.session[SESSION_KEY_PLAN] = plan request.session[SESSION_KEY_PLAN] = plan
return redirect_with_qs( return redirect_to_default_interface(
"authentik_core:if-flow", request,
request.GET, InterfaceType.FLOW,
flow_slug=token.provider.authorization_flow.slug, flow_slug=token.provider.authorization_flow.slug,
) )
@ -88,7 +89,7 @@ class DeviceEntryView(View):
"""View used to initiate the device-code flow, url entered by endusers""" """View used to initiate the device-code flow, url entered by endusers"""
def dispatch(self, request: HttpRequest) -> HttpResponse: def dispatch(self, request: HttpRequest) -> HttpResponse:
tenant: Tenant = request.tenant tenant = get_tenant(request)
device_flow = tenant.flow_device_code device_flow = tenant.flow_device_code
if not device_flow: if not device_flow:
LOGGER.info("Tenant has no device code flow configured", tenant=tenant) LOGGER.info("Tenant has no device code flow configured", tenant=tenant)
@ -110,9 +111,9 @@ class DeviceEntryView(View):
plan.append_stage(in_memory_stage(OAuthDeviceCodeStage)) plan.append_stage(in_memory_stage(OAuthDeviceCodeStage))
self.request.session[SESSION_KEY_PLAN] = plan self.request.session[SESSION_KEY_PLAN] = plan
return redirect_with_qs( return redirect_to_default_interface(
"authentik_core:if-flow", self.request,
self.request.GET, InterfaceType.FLOW,
flow_slug=device_flow.slug, flow_slug=device_flow.slug,
) )

View file

@ -9,6 +9,7 @@ from django.views.decorators.csrf import csrf_exempt
from authentik.providers.oauth2.constants import SCOPE_GITHUB_ORG_READ, SCOPE_GITHUB_USER_EMAIL from authentik.providers.oauth2.constants import SCOPE_GITHUB_ORG_READ, SCOPE_GITHUB_USER_EMAIL
from authentik.providers.oauth2.models import RefreshToken from authentik.providers.oauth2.models import RefreshToken
from authentik.providers.oauth2.utils import protected_resource_view from authentik.providers.oauth2.utils import protected_resource_view
from authentik.tenants.utils import get_tenant
@method_decorator(csrf_exempt, name="dispatch") @method_decorator(csrf_exempt, name="dispatch")
@ -76,6 +77,7 @@ class GitHubUserTeamsView(View):
def get(self, request: HttpRequest, token: RefreshToken) -> HttpResponse: def get(self, request: HttpRequest, token: RefreshToken) -> HttpResponse:
"""Emulate GitHub's /user/teams API Endpoint""" """Emulate GitHub's /user/teams API Endpoint"""
user = token.user user = token.user
tenant = get_tenant(request)
orgs_response = [] orgs_response = []
for org in user.ak_groups.all(): for org in user.ak_groups.all():
@ -97,7 +99,7 @@ class GitHubUserTeamsView(View):
"created_at": "", "created_at": "",
"updated_at": "", "updated_at": "",
"organization": { "organization": {
"login": slugify(request.tenant.branding_title), "login": slugify(tenant.branding_title),
"id": 1, "id": 1,
"node_id": "", "node_id": "",
"url": "", "url": "",
@ -109,7 +111,7 @@ class GitHubUserTeamsView(View):
"public_members_url": "", "public_members_url": "",
"avatar_url": "", "avatar_url": "",
"description": "", "description": "",
"name": request.tenant.branding_title, "name": tenant.branding_title,
"company": "", "company": "",
"blog": "", "blog": "",
"location": "", "location": "",

View file

@ -15,7 +15,8 @@ from authentik.flows.exceptions import FlowNonApplicableException
from authentik.flows.models import in_memory_stage from authentik.flows.models import in_memory_stage
from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, PLAN_CONTEXT_SSO, FlowPlanner from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, PLAN_CONTEXT_SSO, FlowPlanner
from authentik.flows.views.executor import SESSION_KEY_PLAN, SESSION_KEY_POST from authentik.flows.views.executor import SESSION_KEY_PLAN, SESSION_KEY_POST
from authentik.lib.utils.urls import redirect_with_qs from authentik.interfaces.models import InterfaceType
from authentik.interfaces.views import redirect_to_default_interface
from authentik.lib.views import bad_request_message from authentik.lib.views import bad_request_message
from authentik.policies.views import PolicyAccessView from authentik.policies.views import PolicyAccessView
from authentik.providers.saml.exceptions import CannotHandleAssertion from authentik.providers.saml.exceptions import CannotHandleAssertion
@ -76,9 +77,9 @@ class SAMLSSOView(PolicyAccessView):
raise Http404 raise Http404
plan.append_stage(in_memory_stage(SAMLFlowFinalView)) plan.append_stage(in_memory_stage(SAMLFlowFinalView))
request.session[SESSION_KEY_PLAN] = plan request.session[SESSION_KEY_PLAN] = plan
return redirect_with_qs( return redirect_to_default_interface(
"authentik_core:if-flow", request,
request.GET, InterfaceType.FLOW,
flow_slug=self.provider.authorization_flow.slug, flow_slug=self.provider.authorization_flow.slug,
) )

View file

@ -22,4 +22,4 @@ class UseTokenView(View):
login(request, token.user, backend=BACKEND_INBUILT) login(request, token.user, backend=BACKEND_INBUILT)
token.delete() token.delete()
messages.warning(request, _("Used recovery-link to authenticate.")) messages.warning(request, _("Used recovery-link to authenticate."))
return redirect("authentik_core:if-user") return redirect("authentik_core:root-redirect")

View file

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

View file

@ -32,7 +32,8 @@ from authentik.flows.planner import (
) )
from authentik.flows.stage import ChallengeStageView from authentik.flows.stage import ChallengeStageView
from authentik.flows.views.executor import NEXT_ARG_NAME, SESSION_KEY_GET, SESSION_KEY_PLAN from authentik.flows.views.executor import NEXT_ARG_NAME, SESSION_KEY_GET, SESSION_KEY_PLAN
from authentik.lib.utils.urls import redirect_with_qs from authentik.interfaces.models import InterfaceType
from authentik.interfaces.views import redirect_to_default_interface
from authentik.lib.views import bad_request_message from authentik.lib.views import bad_request_message
from authentik.providers.saml.utils.encoding import nice64 from authentik.providers.saml.utils.encoding import nice64
from authentik.sources.saml.exceptions import MissingSAMLResponse, UnsupportedNameIDFormat from authentik.sources.saml.exceptions import MissingSAMLResponse, UnsupportedNameIDFormat
@ -72,7 +73,7 @@ class InitiateView(View):
# Ensure redirect is carried through when user was trying to # Ensure redirect is carried through when user was trying to
# authorize application # authorize application
final_redirect = self.request.session.get(SESSION_KEY_GET, {}).get( final_redirect = self.request.session.get(SESSION_KEY_GET, {}).get(
NEXT_ARG_NAME, "authentik_core:if-user" NEXT_ARG_NAME, "authentik_core:root-redirect"
) )
kwargs.update( kwargs.update(
{ {
@ -91,9 +92,9 @@ class InitiateView(View):
for stage in stages_to_append: for stage in stages_to_append:
plan.append_stage(stage) plan.append_stage(stage)
self.request.session[SESSION_KEY_PLAN] = plan self.request.session[SESSION_KEY_PLAN] = plan
return redirect_with_qs( return redirect_to_default_interface(
"authentik_core:if-flow", self.request,
self.request.GET, InterfaceType.FLOW,
flow_slug=source.pre_authentication_flow.slug, flow_slug=source.pre_authentication_flow.slug,
) )

View file

@ -17,6 +17,7 @@ from authentik.flows.challenge import (
from authentik.flows.stage import ChallengeStageView from authentik.flows.stage import ChallengeStageView
from authentik.stages.authenticator_totp.models import AuthenticatorTOTPStage from authentik.stages.authenticator_totp.models import AuthenticatorTOTPStage
from authentik.stages.authenticator_totp.settings import OTP_TOTP_ISSUER from authentik.stages.authenticator_totp.settings import OTP_TOTP_ISSUER
from authentik.tenants.utils import get_tenant
SESSION_TOTP_DEVICE = "totp_device" SESSION_TOTP_DEVICE = "totp_device"
@ -57,7 +58,7 @@ class AuthenticatorTOTPStageView(ChallengeStageView):
data={ data={
"type": ChallengeTypes.NATIVE.value, "type": ChallengeTypes.NATIVE.value,
"config_url": device.config_url.replace( "config_url": device.config_url.replace(
OTP_TOTP_ISSUER, quote(self.request.tenant.branding_title) OTP_TOTP_ISSUER, quote(get_tenant(self.request).branding_title)
), ),
} }
) )

View file

@ -33,6 +33,7 @@ from authentik.stages.authenticator_validate.models import AuthenticatorValidate
from authentik.stages.authenticator_webauthn.models import UserVerification, WebAuthnDevice from authentik.stages.authenticator_webauthn.models import UserVerification, WebAuthnDevice
from authentik.stages.authenticator_webauthn.stage import SESSION_KEY_WEBAUTHN_CHALLENGE from authentik.stages.authenticator_webauthn.stage import SESSION_KEY_WEBAUTHN_CHALLENGE
from authentik.stages.authenticator_webauthn.utils import get_origin, get_rp_id from authentik.stages.authenticator_webauthn.utils import get_origin, get_rp_id
from authentik.tenants.utils import get_tenant
LOGGER = get_logger() LOGGER = get_logger()
@ -187,7 +188,7 @@ def validate_challenge_duo(device_pk: int, stage_view: StageView, user: User) ->
type=__( type=__(
"%(brand_name)s Login request" "%(brand_name)s Login request"
% { % {
"brand_name": stage_view.request.tenant.branding_title, "brand_name": get_tenant(stage_view.request).branding_title,
} }
), ),
display_username=user.username, display_username=user.username,

View file

@ -19,7 +19,7 @@ from authentik.stages.authenticator_duo.models import AuthenticatorDuoStage, Duo
from authentik.stages.authenticator_validate.challenge import validate_challenge_duo from authentik.stages.authenticator_validate.challenge import validate_challenge_duo
from authentik.stages.authenticator_validate.models import AuthenticatorValidateStage, DeviceClasses from authentik.stages.authenticator_validate.models import AuthenticatorValidateStage, DeviceClasses
from authentik.stages.user_login.models import UserLoginStage from authentik.stages.user_login.models import UserLoginStage
from authentik.tenants.utils import get_tenant_for_request from authentik.tenants.utils import lookup_tenant_for_request
class AuthenticatorValidateStageDuoTests(FlowTestCase): class AuthenticatorValidateStageDuoTests(FlowTestCase):
@ -36,7 +36,7 @@ class AuthenticatorValidateStageDuoTests(FlowTestCase):
middleware = SessionMiddleware(dummy_get_response) middleware = SessionMiddleware(dummy_get_response)
middleware.process_request(request) middleware.process_request(request)
request.session.save() request.session.save()
setattr(request, "tenant", get_tenant_for_request(request)) setattr(request, "tenant", lookup_tenant_for_request(request))
stage = AuthenticatorDuoStage.objects.create( stage = AuthenticatorDuoStage.objects.create(
name=generate_id(), name=generate_id(),

View file

@ -29,6 +29,7 @@ from authentik.flows.challenge import (
from authentik.flows.stage import ChallengeStageView from authentik.flows.stage import ChallengeStageView
from authentik.stages.authenticator_webauthn.models import AuthenticateWebAuthnStage, WebAuthnDevice from authentik.stages.authenticator_webauthn.models import AuthenticateWebAuthnStage, WebAuthnDevice
from authentik.stages.authenticator_webauthn.utils import get_origin, get_rp_id from authentik.stages.authenticator_webauthn.utils import get_origin, get_rp_id
from authentik.tenants.utils import get_tenant
SESSION_KEY_WEBAUTHN_CHALLENGE = "authentik/stages/authenticator_webauthn/challenge" SESSION_KEY_WEBAUTHN_CHALLENGE = "authentik/stages/authenticator_webauthn/challenge"
@ -92,7 +93,7 @@ class AuthenticatorWebAuthnStageView(ChallengeStageView):
registration_options: PublicKeyCredentialCreationOptions = generate_registration_options( registration_options: PublicKeyCredentialCreationOptions = generate_registration_options(
rp_id=get_rp_id(self.request), rp_id=get_rp_id(self.request),
rp_name=self.request.tenant.branding_title, rp_name=get_tenant(self.request).branding_title,
user_id=user.uid, user_id=user.uid,
user_name=user.username, user_name=user.username,
user_display_name=user.name, user_display_name=user.name,

View file

@ -3,7 +3,6 @@ from datetime import timedelta
from django.contrib import messages from django.contrib import messages
from django.http import HttpRequest, HttpResponse from django.http import HttpRequest, HttpResponse
from django.urls import reverse
from django.utils.http import urlencode from django.utils.http import urlencode
from django.utils.text import slugify from django.utils.text import slugify
from django.utils.timezone import now from django.utils.timezone import now
@ -16,6 +15,8 @@ from authentik.flows.models import FlowToken
from authentik.flows.planner import PLAN_CONTEXT_IS_RESTORED, PLAN_CONTEXT_PENDING_USER from authentik.flows.planner import PLAN_CONTEXT_IS_RESTORED, PLAN_CONTEXT_PENDING_USER
from authentik.flows.stage import ChallengeStageView from authentik.flows.stage import ChallengeStageView
from authentik.flows.views.executor import QS_KEY_TOKEN from authentik.flows.views.executor import QS_KEY_TOKEN
from authentik.interfaces.models import InterfaceType
from authentik.interfaces.views import reverse_interface
from authentik.stages.email.models import EmailStage from authentik.stages.email.models import EmailStage
from authentik.stages.email.tasks import send_mails from authentik.stages.email.tasks import send_mails
from authentik.stages.email.utils import TemplateEmailMessage from authentik.stages.email.utils import TemplateEmailMessage
@ -47,9 +48,10 @@ class EmailStageView(ChallengeStageView):
def get_full_url(self, **kwargs) -> str: def get_full_url(self, **kwargs) -> str:
"""Get full URL to be used in template""" """Get full URL to be used in template"""
base_url = reverse( base_url = reverse_interface(
"authentik_core:if-flow", self.request,
kwargs={"flow_slug": self.executor.flow.slug}, InterfaceType.FLOW,
flow_slug=self.executor.flow.slug,
) )
relative_url = f"{base_url}?{urlencode(kwargs)}" relative_url = f"{base_url}?{urlencode(kwargs)}"
return self.request.build_absolute_uri(relative_url) return self.request.build_absolute_uri(relative_url)

View file

@ -7,6 +7,7 @@ from django.core.mail.backends.locmem import EmailBackend
from django.urls import reverse from django.urls import reverse
from rest_framework.test import APITestCase from rest_framework.test import APITestCase
from authentik.blueprints.tests import apply_blueprint
from authentik.core.tests.utils import create_test_admin_user, create_test_flow from authentik.core.tests.utils import create_test_admin_user, create_test_flow
from authentik.events.models import Event, EventAction from authentik.events.models import Event, EventAction
from authentik.flows.markers import StageMarker from authentik.flows.markers import StageMarker
@ -29,6 +30,7 @@ class TestEmailStageSending(APITestCase):
) )
self.binding = FlowStageBinding.objects.create(target=self.flow, stage=self.stage, order=2) self.binding = FlowStageBinding.objects.create(target=self.flow, stage=self.stage, order=2)
@apply_blueprint("system/interfaces.yaml")
def test_pending_user(self): def test_pending_user(self):
"""Test with pending user""" """Test with pending user"""
plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()]) plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
@ -54,6 +56,7 @@ class TestEmailStageSending(APITestCase):
self.assertEqual(event.context["to_email"], [self.user.email]) self.assertEqual(event.context["to_email"], [self.user.email])
self.assertEqual(event.context["from_email"], "system@authentik.local") self.assertEqual(event.context["from_email"], "system@authentik.local")
@apply_blueprint("system/interfaces.yaml")
def test_send_error(self): def test_send_error(self):
"""Test error during sending (sending will be retried)""" """Test error during sending (sending will be retried)"""
plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()]) plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])

View file

@ -7,6 +7,7 @@ from django.core.mail.backends.smtp import EmailBackend as SMTPEmailBackend
from django.urls import reverse from django.urls import reverse
from django.utils.http import urlencode from django.utils.http import urlencode
from authentik.blueprints.tests import apply_blueprint
from authentik.core.tests.utils import create_test_admin_user, create_test_flow from authentik.core.tests.utils import create_test_admin_user, create_test_flow
from authentik.flows.markers import StageMarker from authentik.flows.markers import StageMarker
from authentik.flows.models import FlowDesignation, FlowStageBinding, FlowToken from authentik.flows.models import FlowDesignation, FlowStageBinding, FlowToken
@ -74,6 +75,7 @@ class TestEmailStage(FlowTestCase):
response = self.client.get(url) response = self.client.get(url)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
@apply_blueprint("system/interfaces.yaml")
@patch( @patch(
"authentik.stages.email.models.EmailStage.backend_class", "authentik.stages.email.models.EmailStage.backend_class",
PropertyMock(return_value=EmailBackend), PropertyMock(return_value=EmailBackend),
@ -123,6 +125,7 @@ class TestEmailStage(FlowTestCase):
with self.settings(EMAIL_HOST=host): with self.settings(EMAIL_HOST=host):
self.assertEqual(EmailStage(use_global_settings=True).backend.host, host) self.assertEqual(EmailStage(use_global_settings=True).backend.host, host)
@apply_blueprint("system/interfaces.yaml")
def test_token(self): def test_token(self):
"""Test with token""" """Test with token"""
# Make sure token exists # Make sure token exists

View file

@ -26,8 +26,9 @@ from authentik.flows.models import FlowDesignation
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER
from authentik.flows.stage import PLAN_CONTEXT_PENDING_USER_IDENTIFIER, ChallengeStageView from authentik.flows.stage import PLAN_CONTEXT_PENDING_USER_IDENTIFIER, ChallengeStageView
from authentik.flows.views.executor import SESSION_KEY_APPLICATION_PRE, SESSION_KEY_GET from authentik.flows.views.executor import SESSION_KEY_APPLICATION_PRE, SESSION_KEY_GET
from authentik.interfaces.models import InterfaceType
from authentik.interfaces.views import reverse_interface
from authentik.lib.utils.http import get_client_ip from authentik.lib.utils.http import get_client_ip
from authentik.lib.utils.urls import reverse_with_qs
from authentik.sources.oauth.types.apple import AppleLoginChallenge from authentik.sources.oauth.types.apple import AppleLoginChallenge
from authentik.sources.plex.models import PlexAuthenticationChallenge from authentik.sources.plex.models import PlexAuthenticationChallenge
from authentik.stages.identification.models import IdentificationStage from authentik.stages.identification.models import IdentificationStage
@ -205,22 +206,25 @@ class IdentificationStageView(ChallengeStageView):
get_qs = self.request.session.get(SESSION_KEY_GET, self.request.GET) get_qs = self.request.session.get(SESSION_KEY_GET, self.request.GET)
# Check for related enrollment and recovery flow, add URL to view # Check for related enrollment and recovery flow, add URL to view
if current_stage.enrollment_flow: if current_stage.enrollment_flow:
challenge.initial_data["enroll_url"] = reverse_with_qs( challenge.initial_data["enroll_url"] = reverse_interface(
"authentik_core:if-flow", self.request,
InterfaceType.FLOW,
query=get_qs, query=get_qs,
kwargs={"flow_slug": current_stage.enrollment_flow.slug}, flow_slug=current_stage.enrollment_flow.slug,
) )
if current_stage.recovery_flow: if current_stage.recovery_flow:
challenge.initial_data["recovery_url"] = reverse_with_qs( challenge.initial_data["recovery_url"] = reverse_interface(
"authentik_core:if-flow", self.request,
InterfaceType.FLOW,
query=get_qs, query=get_qs,
kwargs={"flow_slug": current_stage.recovery_flow.slug}, flow_slug=current_stage.recovery_flow.slug,
) )
if current_stage.passwordless_flow: if current_stage.passwordless_flow:
challenge.initial_data["passwordless_url"] = reverse_with_qs( challenge.initial_data["passwordless_url"] = reverse_interface(
"authentik_core:if-flow", self.request,
InterfaceType.FLOW,
query=get_qs, query=get_qs,
kwargs={"flow_slug": current_stage.passwordless_flow.slug}, flow_slug=current_stage.passwordless_flow.slug,
) )
# Check all enabled source, add them if they have a UI Login button. # Check all enabled source, add them if they have a UI Login button.

View file

@ -5,6 +5,8 @@ from authentik.core.tests.utils import create_test_admin_user, create_test_flow
from authentik.flows.challenge import ChallengeTypes from authentik.flows.challenge import ChallengeTypes
from authentik.flows.models import FlowDesignation, FlowStageBinding from authentik.flows.models import FlowDesignation, FlowStageBinding
from authentik.flows.tests import FlowTestCase from authentik.flows.tests import FlowTestCase
from authentik.interfaces.models import InterfaceType
from authentik.interfaces.tests import reverse_interface
from authentik.sources.oauth.models import OAuthSource from authentik.sources.oauth.models import OAuthSource
from authentik.stages.identification.models import IdentificationStage, UserFields from authentik.stages.identification.models import IdentificationStage, UserFields
from authentik.stages.password import BACKEND_INBUILT from authentik.stages.password import BACKEND_INBUILT
@ -166,9 +168,9 @@ class TestIdentificationStage(FlowTestCase):
component="ak-stage-identification", component="ak-stage-identification",
user_fields=["email"], user_fields=["email"],
password_fields=False, password_fields=False,
enroll_url=reverse( enroll_url=reverse_interface(
"authentik_core:if-flow", InterfaceType.FLOW,
kwargs={"flow_slug": flow.slug}, flow_slug=flow.slug,
), ),
show_source_labels=False, show_source_labels=False,
primary_action="Log in", primary_action="Log in",
@ -204,9 +206,9 @@ class TestIdentificationStage(FlowTestCase):
component="ak-stage-identification", component="ak-stage-identification",
user_fields=["email"], user_fields=["email"],
password_fields=False, password_fields=False,
recovery_url=reverse( recovery_url=reverse_interface(
"authentik_core:if-flow", InterfaceType.FLOW,
kwargs={"flow_slug": flow.slug}, flow_slug=flow.slug,
), ),
show_source_labels=False, show_source_labels=False,
primary_action="Log in", primary_action="Log in",

View file

@ -5,7 +5,6 @@ from django.contrib.auth import _clean_credentials
from django.contrib.auth.backends import BaseBackend from django.contrib.auth.backends import BaseBackend
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from django.http import HttpRequest, HttpResponse from django.http import HttpRequest, HttpResponse
from django.urls import reverse
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from rest_framework.exceptions import ErrorDetail, ValidationError from rest_framework.exceptions import ErrorDetail, ValidationError
from rest_framework.fields import CharField from rest_framework.fields import CharField
@ -23,6 +22,8 @@ from authentik.flows.challenge import (
from authentik.flows.models import Flow, FlowDesignation, Stage from authentik.flows.models import Flow, FlowDesignation, Stage
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER
from authentik.flows.stage import ChallengeStageView from authentik.flows.stage import ChallengeStageView
from authentik.interfaces.models import InterfaceType
from authentik.interfaces.views import reverse_interface
from authentik.lib.utils.reflection import path_to_class from authentik.lib.utils.reflection import path_to_class
from authentik.stages.password.models import PasswordStage from authentik.stages.password.models import PasswordStage
@ -95,11 +96,12 @@ class PasswordStageView(ChallengeStageView):
"type": ChallengeTypes.NATIVE.value, "type": ChallengeTypes.NATIVE.value,
} }
) )
recovery_flow = Flow.objects.filter(designation=FlowDesignation.RECOVERY) recovery_flow = Flow.objects.filter(designation=FlowDesignation.RECOVERY).first()
if recovery_flow.exists(): if recovery_flow:
recover_url = reverse( recover_url = reverse_interface(
"authentik_core:if-flow", self.request,
kwargs={"flow_slug": recovery_flow.first().slug}, InterfaceType.FLOW,
flow_slug=recovery_flow.slug,
) )
challenge.initial_data["recovery_url"] = self.request.build_absolute_uri(recover_url) challenge.initial_data["recovery_url"] = self.request.build_absolute_uri(recover_url)
return challenge return challenge

View file

@ -18,6 +18,7 @@ from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import PassiveSerializer from authentik.core.api.utils import PassiveSerializer
from authentik.lib.config import CONFIG from authentik.lib.config import CONFIG
from authentik.tenants.models import Tenant from authentik.tenants.models import Tenant
from authentik.tenants.utils import get_tenant
class FooterLinkSerializer(PassiveSerializer): class FooterLinkSerializer(PassiveSerializer):
@ -54,6 +55,9 @@ class TenantSerializer(ModelSerializer):
"flow_unenrollment", "flow_unenrollment",
"flow_user_settings", "flow_user_settings",
"flow_device_code", "flow_device_code",
"interface_admin",
"interface_user",
"interface_flow",
"event_retention", "event_retention",
"web_certificate", "web_certificate",
"attributes", "attributes",
@ -120,6 +124,9 @@ class TenantViewSet(UsedByMixin, ModelViewSet):
"flow_unenrollment", "flow_unenrollment",
"flow_user_settings", "flow_user_settings",
"flow_device_code", "flow_device_code",
"interface_admin",
"interface_user",
"interface_flow",
"event_retention", "event_retention",
"web_certificate", "web_certificate",
] ]
@ -133,5 +140,4 @@ class TenantViewSet(UsedByMixin, ModelViewSet):
@action(methods=["GET"], detail=False, permission_classes=[AllowAny]) @action(methods=["GET"], detail=False, permission_classes=[AllowAny])
def current(self, request: Request) -> Response: def current(self, request: Request) -> Response:
"""Get current tenant""" """Get current tenant"""
tenant: Tenant = request._request.tenant return Response(CurrentTenantSerializer(get_tenant(request)).data)
return Response(CurrentTenantSerializer(tenant).data)

View file

@ -6,7 +6,7 @@ from django.http.response import HttpResponse
from django.utils.translation import activate from django.utils.translation import activate
from sentry_sdk.api import set_tag from sentry_sdk.api import set_tag
from authentik.tenants.utils import get_tenant_for_request from authentik.tenants.utils import lookup_tenant_for_request
class TenantMiddleware: class TenantMiddleware:
@ -19,7 +19,7 @@ class TenantMiddleware:
def __call__(self, request: HttpRequest) -> HttpResponse: def __call__(self, request: HttpRequest) -> HttpResponse:
if not hasattr(request, "tenant"): if not hasattr(request, "tenant"):
tenant = get_tenant_for_request(request) tenant = lookup_tenant_for_request(request)
setattr(request, "tenant", tenant) setattr(request, "tenant", tenant)
set_tag("authentik.tenant_uuid", tenant.tenant_uuid.hex) set_tag("authentik.tenant_uuid", tenant.tenant_uuid.hex)
set_tag("authentik.tenant_domain", tenant.domain) set_tag("authentik.tenant_domain", tenant.domain)

View file

@ -0,0 +1,78 @@
# Generated by Django 4.1.7 on 2023-02-21 14:18
import django.db.models.deletion
from django.apps.registry import Apps
from django.db import migrations, models
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
def migrate_set_default(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
Tenant = apps.get_model("authentik_tenants", "tenant")
Interface = apps.get_model("authentik_interfaces", "Interface")
db_alias = schema_editor.connection.alias
from authentik.blueprints.models import BlueprintInstance
from authentik.blueprints.v1.importer import Importer
from authentik.blueprints.v1.tasks import blueprints_discovery
from authentik.interfaces.models import InterfaceType
# If we don't have any tenants yet, we don't need wait for the default interface blueprint
if not Tenant.objects.using(db_alias).exists():
return
interface_blueprint = BlueprintInstance.objects.filter(path="system/interfaces.yaml").first()
if not interface_blueprint:
blueprints_discovery.delay().get()
interface_blueprint = BlueprintInstance.objects.filter(
path="system/interfaces.yaml"
).first()
if not interface_blueprint:
raise ValueError("Failed to apply system/interfaces.yaml blueprint")
Importer(interface_blueprint.retrieve()).apply()
for tenant in Tenant.objects.using(db_alias).all():
tenant.interface_admin = Interface.objects.filter(type=InterfaceType.ADMIN).first()
tenant.interface_user = Interface.objects.filter(type=InterfaceType.USER).first()
tenant.interface_flow = Interface.objects.filter(type=InterfaceType.FLOW).first()
tenant.save()
class Migration(migrations.Migration):
dependencies = [
("authentik_interfaces", "0001_initial"),
("authentik_tenants", "0004_tenant_flow_device_code"),
]
operations = [
migrations.AddField(
model_name="tenant",
name="interface_admin",
field=models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="tenant_admin",
to="authentik_interfaces.interface",
),
),
migrations.AddField(
model_name="tenant",
name="interface_flow",
field=models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="tenant_flow",
to="authentik_interfaces.interface",
),
),
migrations.AddField(
model_name="tenant",
name="interface_user",
field=models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="tenant_user",
to="authentik_interfaces.interface",
),
),
migrations.RunPython(migrate_set_default),
]

View file

@ -7,7 +7,6 @@ from rest_framework.serializers import Serializer
from structlog.stdlib import get_logger from structlog.stdlib import get_logger
from authentik.crypto.models import CertificateKeyPair from authentik.crypto.models import CertificateKeyPair
from authentik.flows.models import Flow
from authentik.lib.models import SerializerModel from authentik.lib.models import SerializerModel
from authentik.lib.utils.time import timedelta_string_validator from authentik.lib.utils.time import timedelta_string_validator
@ -33,22 +32,59 @@ class Tenant(SerializerModel):
branding_favicon = models.TextField(default="/static/dist/assets/icons/icon.png") branding_favicon = models.TextField(default="/static/dist/assets/icons/icon.png")
flow_authentication = models.ForeignKey( flow_authentication = models.ForeignKey(
Flow, null=True, on_delete=models.SET_NULL, related_name="tenant_authentication" "authentik_flows.Flow",
null=True,
on_delete=models.SET_NULL,
related_name="tenant_authentication",
) )
flow_invalidation = models.ForeignKey( flow_invalidation = models.ForeignKey(
Flow, null=True, on_delete=models.SET_NULL, related_name="tenant_invalidation" "authentik_flows.Flow",
null=True,
on_delete=models.SET_NULL,
related_name="tenant_invalidation",
) )
flow_recovery = models.ForeignKey( flow_recovery = models.ForeignKey(
Flow, null=True, on_delete=models.SET_NULL, related_name="tenant_recovery" "authentik_flows.Flow", null=True, on_delete=models.SET_NULL, related_name="tenant_recovery"
) )
flow_unenrollment = models.ForeignKey( flow_unenrollment = models.ForeignKey(
Flow, null=True, on_delete=models.SET_NULL, related_name="tenant_unenrollment" "authentik_flows.Flow",
null=True,
on_delete=models.SET_NULL,
related_name="tenant_unenrollment",
) )
flow_user_settings = models.ForeignKey( flow_user_settings = models.ForeignKey(
Flow, null=True, on_delete=models.SET_NULL, related_name="tenant_user_settings" "authentik_flows.Flow",
null=True,
on_delete=models.SET_NULL,
related_name="tenant_user_settings",
) )
flow_device_code = models.ForeignKey( flow_device_code = models.ForeignKey(
Flow, null=True, on_delete=models.SET_NULL, related_name="tenant_device_code" "authentik_flows.Flow",
null=True,
on_delete=models.SET_NULL,
related_name="tenant_device_code",
)
interface_flow = models.ForeignKey(
"authentik_interfaces.Interface",
default=None,
on_delete=models.SET_NULL,
null=True,
related_name="tenant_flow",
)
interface_user = models.ForeignKey(
"authentik_interfaces.Interface",
default=None,
on_delete=models.SET_NULL,
null=True,
related_name="tenant_user",
)
interface_admin = models.ForeignKey(
"authentik_interfaces.Interface",
default=None,
on_delete=models.SET_NULL,
null=True,
related_name="tenant_admin",
) )
event_retention = models.TextField( event_retention = models.TextField(

View file

@ -75,7 +75,7 @@ class TestTenants(APITestCase):
) )
factory = RequestFactory() factory = RequestFactory()
request = factory.get("/") request = factory.get("/")
request.tenant = tenant setattr(request, "tenant", tenant)
event = Event.new(action=EventAction.SYSTEM_EXCEPTION, message="test").from_http(request) event = Event.new(action=EventAction.SYSTEM_EXCEPTION, message="test").from_http(request)
self.assertEqual(event.expires.day, (event.created + timedelta_from_string("weeks=3")).day) self.assertEqual(event.expires.day, (event.created + timedelta_from_string("weeks=3")).day)
self.assertEqual( self.assertEqual(

View file

@ -4,17 +4,41 @@ from typing import Any
from django.db.models import F, Q from django.db.models import F, Q
from django.db.models import Value as V from django.db.models import Value as V
from django.http.request import HttpRequest from django.http.request import HttpRequest
from rest_framework.request import Request
from sentry_sdk.hub import Hub from sentry_sdk.hub import Hub
from authentik import get_full_version from authentik import get_full_version
from authentik.interfaces.models import Interface, InterfaceType
from authentik.lib.config import CONFIG from authentik.lib.config import CONFIG
from authentik.tenants.models import Tenant from authentik.tenants.models import Tenant
_q_default = Q(default=True) _q_default = Q(default=True)
DEFAULT_TENANT = Tenant(domain="fallback")
def get_tenant_for_request(request: HttpRequest) -> Tenant: def get_fallback_tenant():
"""Get fallback tenant"""
fallback_interface = Interface(
url_name="fallback",
type=InterfaceType.FLOW,
template="Fallback interface",
)
return Tenant(
domain="fallback",
interface_flow=fallback_interface,
interface_user=fallback_interface,
interface_admin=fallback_interface,
)
def get_tenant(request: HttpRequest | Request) -> "Tenant":
"""Get the request's tenant, falls back to a fallback tenant object"""
if isinstance(request, Request):
request = request._request
return getattr(request, "tenant", get_fallback_tenant())
def lookup_tenant_for_request(request: HttpRequest) -> "Tenant":
"""Get tenant object for current request""" """Get tenant object for current request"""
db_tenants = ( db_tenants = (
Tenant.objects.annotate(host_domain=V(request.get_host())) Tenant.objects.annotate(host_domain=V(request.get_host()))
@ -23,13 +47,13 @@ def get_tenant_for_request(request: HttpRequest) -> Tenant:
) )
tenants = list(db_tenants.all()) tenants = list(db_tenants.all())
if len(tenants) < 1: if len(tenants) < 1:
return DEFAULT_TENANT return get_fallback_tenant()
return tenants[0] return tenants[0]
def context_processor(request: HttpRequest) -> dict[str, Any]: def context_processor(request: HttpRequest) -> dict[str, Any]:
"""Context Processor that injects tenant object into every template""" """Context Processor that injects tenant object into every template"""
tenant = getattr(request, "tenant", DEFAULT_TENANT) tenant = getattr(request, "tenant", get_fallback_tenant())
trace = "" trace = ""
span = Hub.current.scope.span span = Hub.current.scope.span
if span: if span:

View file

@ -2,6 +2,11 @@ metadata:
name: Default - Tenant name: Default - Tenant
version: 1 version: 1
entries: entries:
- model: authentik_blueprints.metaapplyblueprint
attrs:
identifiers:
name: System - Interfaces
required: false
- model: authentik_blueprints.metaapplyblueprint - model: authentik_blueprints.metaapplyblueprint
attrs: attrs:
identifiers: identifiers:
@ -21,6 +26,9 @@ entries:
flow_authentication: !Find [authentik_flows.flow, [slug, default-authentication-flow]] flow_authentication: !Find [authentik_flows.flow, [slug, default-authentication-flow]]
flow_invalidation: !Find [authentik_flows.flow, [slug, default-invalidation-flow]] flow_invalidation: !Find [authentik_flows.flow, [slug, default-invalidation-flow]]
flow_user_settings: !Find [authentik_flows.flow, [slug, default-user-settings-flow]] flow_user_settings: !Find [authentik_flows.flow, [slug, default-user-settings-flow]]
interface_admin: !Find [authentik_interfaces.Interface, [type, admin]]
interface_user: !Find [authentik_interfaces.Interface, [type, user]]
interface_flow: !Find [authentik_interfaces.Interface, [type, flow]]
identifiers: identifiers:
domain: authentik-default domain: authentik-default
default: True default: True

View file

@ -61,6 +61,7 @@
"authentik_events.notificationwebhookmapping", "authentik_events.notificationwebhookmapping",
"authentik_flows.flow", "authentik_flows.flow",
"authentik_flows.flowstagebinding", "authentik_flows.flowstagebinding",
"authentik_interfaces.interface",
"authentik_outposts.dockerserviceconnection", "authentik_outposts.dockerserviceconnection",
"authentik_outposts.kubernetesserviceconnection", "authentik_outposts.kubernetesserviceconnection",
"authentik_outposts.outpost", "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 %}

View file

@ -3676,6 +3676,24 @@ paths:
description: flow_user_settings description: flow_user_settings
schema: schema:
type: string type: string
- name: interface_admin
required: false
in: query
description: interface_admin
schema:
type: string
- name: interface_flow
required: false
in: query
description: interface_flow
schema:
type: string
- name: interface_user
required: false
in: query
description: interface_user
schema:
type: string
- name: ordering - name: ordering
required: false required: false
in: query in: query
@ -7731,6 +7749,295 @@ paths:
schema: schema:
$ref: '#/components/schemas/GenericError' $ref: '#/components/schemas/GenericError'
description: '' description: ''
/interfaces/:
get:
operationId: interfaces_list
description: Interface serializer
parameters:
- name: ordering
required: false
in: query
description: Which field to use when ordering the results.
schema:
type: string
- name: page
required: false
in: query
description: A page number within the paginated result set.
schema:
type: integer
- name: page_size
required: false
in: query
description: Number of results to return per page.
schema:
type: integer
- name: search
required: false
in: query
description: A search term.
schema:
type: string
- in: query
name: template
schema:
type: string
- in: query
name: type
schema:
type: string
enum:
- admin
- flow
- user
description: |-
* `user` - User
* `admin` - Admin
* `flow` - Flow
* `user` - User
* `admin` - Admin
* `flow` - Flow
- in: query
name: url_name
schema:
type: string
tags:
- interfaces
security:
- authentik: []
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/PaginatedInterfaceList'
description: ''
'400':
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
description: ''
'403':
content:
application/json:
schema:
$ref: '#/components/schemas/GenericError'
description: ''
post:
operationId: interfaces_create
description: Interface serializer
tags:
- interfaces
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/InterfaceRequest'
required: true
security:
- authentik: []
responses:
'201':
content:
application/json:
schema:
$ref: '#/components/schemas/Interface'
description: ''
'400':
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
description: ''
'403':
content:
application/json:
schema:
$ref: '#/components/schemas/GenericError'
description: ''
/interfaces/{interface_uuid}/:
get:
operationId: interfaces_retrieve
description: Interface serializer
parameters:
- in: path
name: interface_uuid
schema:
type: string
format: uuid
description: A UUID string identifying this interface.
required: true
tags:
- interfaces
security:
- authentik: []
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/Interface'
description: ''
'400':
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
description: ''
'403':
content:
application/json:
schema:
$ref: '#/components/schemas/GenericError'
description: ''
put:
operationId: interfaces_update
description: Interface serializer
parameters:
- in: path
name: interface_uuid
schema:
type: string
format: uuid
description: A UUID string identifying this interface.
required: true
tags:
- interfaces
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/InterfaceRequest'
required: true
security:
- authentik: []
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/Interface'
description: ''
'400':
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
description: ''
'403':
content:
application/json:
schema:
$ref: '#/components/schemas/GenericError'
description: ''
patch:
operationId: interfaces_partial_update
description: Interface serializer
parameters:
- in: path
name: interface_uuid
schema:
type: string
format: uuid
description: A UUID string identifying this interface.
required: true
tags:
- interfaces
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/PatchedInterfaceRequest'
security:
- authentik: []
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/Interface'
description: ''
'400':
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
description: ''
'403':
content:
application/json:
schema:
$ref: '#/components/schemas/GenericError'
description: ''
delete:
operationId: interfaces_destroy
description: Interface serializer
parameters:
- in: path
name: interface_uuid
schema:
type: string
format: uuid
description: A UUID string identifying this interface.
required: true
tags:
- interfaces
security:
- authentik: []
responses:
'204':
description: No response body
'400':
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
description: ''
'403':
content:
application/json:
schema:
$ref: '#/components/schemas/GenericError'
description: ''
/interfaces/{interface_uuid}/used_by/:
get:
operationId: interfaces_used_by_list
description: Get a list of all objects that use this object
parameters:
- in: path
name: interface_uuid
schema:
type: string
format: uuid
description: A UUID string identifying this interface.
required: true
tags:
- interfaces
security:
- authentik: []
responses:
'200':
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/UsedBy'
description: ''
'400':
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
description: ''
'403':
content:
application/json:
schema:
$ref: '#/components/schemas/GenericError'
description: ''
/managed/blueprints/: /managed/blueprints/:
get: get:
operationId: managed_blueprints_list operationId: managed_blueprints_list
@ -26307,6 +26614,7 @@ components:
- authentik.admin - authentik.admin
- authentik.api - authentik.api
- authentik.crypto - authentik.crypto
- authentik.interfaces
- authentik.events - authentik.events
- authentik.flows - authentik.flows
- authentik.lib - authentik.lib
@ -26357,6 +26665,7 @@ components:
* `authentik.admin` - authentik Admin * `authentik.admin` - authentik Admin
* `authentik.api` - authentik API * `authentik.api` - authentik API
* `authentik.crypto` - authentik Crypto * `authentik.crypto` - authentik Crypto
* `authentik.interfaces` - authentik Interfaces
* `authentik.events` - authentik Events * `authentik.events` - authentik Events
* `authentik.flows` - authentik Flows * `authentik.flows` - authentik Flows
* `authentik.lib` - authentik lib * `authentik.lib` - authentik lib
@ -29074,6 +29383,7 @@ components:
* `authentik.admin` - authentik Admin * `authentik.admin` - authentik Admin
* `authentik.api` - authentik API * `authentik.api` - authentik API
* `authentik.crypto` - authentik Crypto * `authentik.crypto` - authentik Crypto
* `authentik.interfaces` - authentik Interfaces
* `authentik.events` - authentik Events * `authentik.events` - authentik Events
* `authentik.flows` - authentik Flows * `authentik.flows` - authentik Flows
* `authentik.lib` - authentik lib * `authentik.lib` - authentik lib
@ -29184,6 +29494,7 @@ components:
* `authentik.admin` - authentik Admin * `authentik.admin` - authentik Admin
* `authentik.api` - authentik API * `authentik.api` - authentik API
* `authentik.crypto` - authentik Crypto * `authentik.crypto` - authentik Crypto
* `authentik.interfaces` - authentik Interfaces
* `authentik.events` - authentik Events * `authentik.events` - authentik Events
* `authentik.flows` - authentik Flows * `authentik.flows` - authentik Flows
* `authentik.lib` - authentik lib * `authentik.lib` - authentik lib
@ -30297,6 +30608,55 @@ components:
* `api` - Intent Api * `api` - Intent Api
* `recovery` - Intent Recovery * `recovery` - Intent Recovery
* `app_password` - Intent App Password * `app_password` - Intent App Password
Interface:
type: object
description: Interface serializer
properties:
interface_uuid:
type: string
format: uuid
readOnly: true
url_name:
type: string
maxLength: 50
pattern: ^[-a-zA-Z0-9_]+$
type:
$ref: '#/components/schemas/InterfaceTypeEnum'
template:
type: string
required:
- interface_uuid
- template
- type
- url_name
InterfaceRequest:
type: object
description: Interface serializer
properties:
url_name:
type: string
minLength: 1
maxLength: 50
pattern: ^[-a-zA-Z0-9_]+$
type:
$ref: '#/components/schemas/InterfaceTypeEnum'
template:
type: string
minLength: 1
required:
- template
- type
- url_name
InterfaceTypeEnum:
enum:
- user
- admin
- flow
type: string
description: |-
* `user` - User
* `admin` - Admin
* `flow` - Flow
InvalidResponseActionEnum: InvalidResponseActionEnum:
enum: enum:
- retry - retry
@ -33051,6 +33411,41 @@ components:
required: required:
- pagination - pagination
- results - results
PaginatedInterfaceList:
type: object
properties:
pagination:
type: object
properties:
next:
type: number
previous:
type: number
count:
type: number
current:
type: number
total_pages:
type: number
start_index:
type: number
end_index:
type: number
required:
- next
- previous
- count
- current
- total_pages
- start_index
- end_index
results:
type: array
items:
$ref: '#/components/schemas/Interface'
required:
- pagination
- results
PaginatedInvitationList: PaginatedInvitationList:
type: object type: object
properties: properties:
@ -35870,6 +36265,7 @@ components:
* `authentik.admin` - authentik Admin * `authentik.admin` - authentik Admin
* `authentik.api` - authentik API * `authentik.api` - authentik API
* `authentik.crypto` - authentik Crypto * `authentik.crypto` - authentik Crypto
* `authentik.interfaces` - authentik Interfaces
* `authentik.events` - authentik Events * `authentik.events` - authentik Events
* `authentik.flows` - authentik Flows * `authentik.flows` - authentik Flows
* `authentik.lib` - authentik lib * `authentik.lib` - authentik lib
@ -36121,6 +36517,20 @@ components:
description: Specify which sources should be shown. description: Specify which sources should be shown.
show_source_labels: show_source_labels:
type: boolean type: boolean
PatchedInterfaceRequest:
type: object
description: Interface serializer
properties:
url_name:
type: string
minLength: 1
maxLength: 50
pattern: ^[-a-zA-Z0-9_]+$
type:
$ref: '#/components/schemas/InterfaceTypeEnum'
template:
type: string
minLength: 1
PatchedInvitationRequest: PatchedInvitationRequest:
type: object type: object
description: Invitation Serializer description: Invitation Serializer
@ -37405,6 +37815,18 @@ components:
type: string type: string
format: uuid format: uuid
nullable: true nullable: true
interface_admin:
type: string
format: uuid
nullable: true
interface_user:
type: string
format: uuid
nullable: true
interface_flow:
type: string
format: uuid
nullable: true
event_retention: event_retention:
type: string type: string
minLength: 1 minLength: 1
@ -40576,6 +40998,18 @@ components:
type: string type: string
format: uuid format: uuid
nullable: true nullable: true
interface_admin:
type: string
format: uuid
nullable: true
interface_user:
type: string
format: uuid
nullable: true
interface_flow:
type: string
format: uuid
nullable: true
event_retention: event_retention:
type: string type: string
description: 'Events will be deleted after this duration.(Format: weeks=3;days=2;hours=3,seconds=2).' description: 'Events will be deleted after this duration.(Format: weeks=3;days=2;hours=3,seconds=2).'
@ -40634,6 +41068,18 @@ components:
type: string type: string
format: uuid format: uuid
nullable: true nullable: true
interface_admin:
type: string
format: uuid
nullable: true
interface_user:
type: string
format: uuid
nullable: true
interface_flow:
type: string
format: uuid
nullable: true
event_retention: event_retention:
type: string type: string
minLength: 1 minLength: 1

View file

@ -33,7 +33,7 @@ class TestFlowsAuthenticator(SeleniumTestCase):
flow: Flow = Flow.objects.get(slug="default-authentication-flow") flow: Flow = Flow.objects.get(slug="default-authentication-flow")
self.driver.get(self.url("authentik_core:if-flow", flow_slug=flow.slug)) self.driver.get(self.url("authentik_interfaces:if", if_name="flow", flow_slug=flow.slug))
self.login() self.login()
# Get expected token # Get expected token
@ -57,7 +57,7 @@ class TestFlowsAuthenticator(SeleniumTestCase):
"""test TOTP Setup stage""" """test TOTP Setup stage"""
flow: Flow = Flow.objects.get(slug="default-authentication-flow") flow: Flow = Flow.objects.get(slug="default-authentication-flow")
self.driver.get(self.url("authentik_core:if-flow", flow_slug=flow.slug)) self.driver.get(self.url("authentik_interfaces:if", if_name="flow", flow_slug=flow.slug))
self.login() self.login()
self.wait_for_url(self.if_user_url("/library")) self.wait_for_url(self.if_user_url("/library"))
@ -103,7 +103,7 @@ class TestFlowsAuthenticator(SeleniumTestCase):
"""test Static OTP Setup stage""" """test Static OTP Setup stage"""
flow: Flow = Flow.objects.get(slug="default-authentication-flow") flow: Flow = Flow.objects.get(slug="default-authentication-flow")
self.driver.get(self.url("authentik_core:if-flow", flow_slug=flow.slug)) self.driver.get(self.url("authentik_interfaces:if", if_name="flow", flow_slug=flow.slug))
self.login() self.login()
self.wait_for_url(self.if_user_url("/library")) self.wait_for_url(self.if_user_url("/library"))

View file

@ -15,7 +15,8 @@ class TestFlowsLogin(SeleniumTestCase):
"""test default login flow""" """test default login flow"""
self.driver.get( self.driver.get(
self.url( self.url(
"authentik_core:if-flow", "authentik_interfaces:if",
if_name="flow",
flow_slug="default-authentication-flow", flow_slug="default-authentication-flow",
) )
) )

View file

@ -35,7 +35,8 @@ class TestFlowsStageSetup(SeleniumTestCase):
self.driver.get( self.driver.get(
self.url( self.url(
"authentik_core:if-flow", "authentik_interfaces:if",
if_name="flow",
flow_slug="default-authentication-flow", flow_slug="default-authentication-flow",
) )
) )

View file

@ -299,6 +299,9 @@ export class AdminInterface extends Interface {
<ak-sidebar-item path="/core/tenants"> <ak-sidebar-item path="/core/tenants">
<span slot="label">${t`Tenants`}</span> <span slot="label">${t`Tenants`}</span>
</ak-sidebar-item> </ak-sidebar-item>
<ak-sidebar-item path="/interfaces">
<span slot="label">${t`Interfaces`}</span>
</ak-sidebar-item>
<ak-sidebar-item path="/crypto/certificates"> <ak-sidebar-item path="/crypto/certificates">
<span slot="label">${t`Certificates`}</span> <span slot="label">${t`Certificates`}</span>
</ak-sidebar-item> </ak-sidebar-item>

View file

@ -132,6 +132,10 @@ export const ROUTES: Route[] = [
await import("@goauthentik/admin/blueprints/BlueprintListPage"); await import("@goauthentik/admin/blueprints/BlueprintListPage");
return html`<ak-blueprint-list></ak-blueprint-list>`; return html`<ak-blueprint-list></ak-blueprint-list>`;
}), }),
new Route(new RegExp("^/interfaces$"), async () => {
await import("@goauthentik/admin/interfaces/InterfaceListPage");
return html`<ak-interface-list></ak-interface-list>`;
}),
new Route(new RegExp("^/debug$"), async () => { new Route(new RegExp("^/debug$"), async () => {
await import("@goauthentik/admin/DebugPage"); await import("@goauthentik/admin/DebugPage");
return html`<ak-admin-debug-page></ak-admin-debug-page>`; return html`<ak-admin-debug-page></ak-admin-debug-page>`;

View file

@ -0,0 +1,90 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { first } from "@goauthentik/common/utils";
import "@goauthentik/elements/CodeMirror";
import "@goauthentik/elements/forms/HorizontalFormElement";
import { ModelForm } from "@goauthentik/elements/forms/ModelForm";
import "@goauthentik/elements/forms/Radio";
import { t } from "@lingui/macro";
import { TemplateResult, html } from "lit";
import { customElement } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import { Interface, InterfaceTypeEnum, InterfacesApi } from "@goauthentik/api";
@customElement("ak-interface-form")
export class InterfaceForm extends ModelForm<Interface, string> {
loadInstance(pk: string): Promise<Interface> {
return new InterfacesApi(DEFAULT_CONFIG).interfacesRetrieve({
interfaceUuid: pk,
});
}
getSuccessMessage(): string {
if (this.instance) {
return t`Successfully updated interface.`;
} else {
return t`Successfully created interface.`;
}
}
send = (data: Interface): Promise<Interface> => {
if (this.instance?.interfaceUuid) {
return new InterfacesApi(DEFAULT_CONFIG).interfacesUpdate({
interfaceUuid: this.instance.interfaceUuid,
interfaceRequest: data,
});
} else {
return new InterfacesApi(DEFAULT_CONFIG).interfacesCreate({
interfaceRequest: data,
});
}
};
renderForm(): TemplateResult {
return html`<form class="pf-c-form pf-m-horizontal">
<ak-form-element-horizontal label=${t`URL Name`} ?required=${true} name="urlName">
<input
type="text"
value="${first(this.instance?.urlName, "")}"
class="pf-c-form-control"
required
/>
<p class="pf-c-form__helper-text">
${t`Name used in the URL when accessing this interface.`}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${t`Type`} ?required=${true} name="type">
<ak-radio
.options=${[
{
label: t`Enduser interface`,
value: InterfaceTypeEnum.User,
default: true,
},
{
label: t`Flow interface`,
value: InterfaceTypeEnum.Flow,
},
{
label: t`Admin interface`,
value: InterfaceTypeEnum.Admin,
},
]}
.value=${this.instance?.type}
>
</ak-radio>
<p class="pf-c-form__helper-text">
${t`Configure how authentik will use this interface.`}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${t`Template`} ?required=${true} name="template"
><ak-codemirror
mode="html"
value="${ifDefined(this.instance?.template)}"
></ak-codemirror>
</ak-form-element-horizontal>
</form>`;
}
}

View file

@ -0,0 +1,101 @@
import "@goauthentik/admin/interfaces/InterfaceForm";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { uiConfig } from "@goauthentik/common/ui/config";
import "@goauthentik/elements/buttons/SpinnerButton";
import "@goauthentik/elements/forms/DeleteBulkForm";
import "@goauthentik/elements/forms/ModalForm";
import { PaginatedResponse } from "@goauthentik/elements/table/Table";
import { TableColumn } from "@goauthentik/elements/table/Table";
import { TablePage } from "@goauthentik/elements/table/TablePage";
import { t } from "@lingui/macro";
import { TemplateResult, html } from "lit";
import { customElement, property } from "lit/decorators.js";
import { Interface, InterfacesApi } from "@goauthentik/api";
@customElement("ak-interface-list")
export class InterfaceListPage extends TablePage<Interface> {
searchEnabled(): boolean {
return true;
}
pageTitle(): string {
return t`Interfaces`;
}
pageDescription(): string {
return t`Manage custom interfaces for authentik`;
}
pageIcon(): string {
return "fa fa-home";
}
checkbox = true;
@property()
order = "url_name";
async apiEndpoint(page: number): Promise<PaginatedResponse<Interface>> {
return new InterfacesApi(DEFAULT_CONFIG).interfacesList({
ordering: this.order,
page: page,
pageSize: (await uiConfig()).pagination.perPage,
search: this.search || "",
});
}
columns(): TableColumn[] {
return [new TableColumn(t`URL Name`, "url_name"), new TableColumn(t`Actions`)];
}
renderToolbarSelected(): TemplateResult {
const disabled = this.selectedElements.length < 1;
return html`<ak-forms-delete-bulk
objectLabel=${t`Interface(s)`}
.objects=${this.selectedElements}
.metadata=${(item: Interface) => {
return [{ key: t`Domain`, value: item.urlName }];
}}
.usedBy=${(item: Interface) => {
return new InterfacesApi(DEFAULT_CONFIG).interfacesUsedByList({
interfaceUuid: item.interfaceUuid,
});
}}
.delete=${(item: Interface) => {
return new InterfacesApi(DEFAULT_CONFIG).interfacesDestroy({
interfaceUuid: item.interfaceUuid,
});
}}
>
<button ?disabled=${disabled} slot="trigger" class="pf-c-button pf-m-danger">
${t`Delete`}
</button>
</ak-forms-delete-bulk>`;
}
row(item: Interface): TemplateResult[] {
return [
html`${item.urlName}`,
html`<ak-forms-modal>
<span slot="submit"> ${t`Update`} </span>
<span slot="header"> ${t`Update Interface`} </span>
<ak-interface-form slot="form" .instancePk=${item.interfaceUuid}>
</ak-interface-form>
<button slot="trigger" class="pf-c-button pf-m-plain">
<i class="fas fa-edit"></i>
</button>
</ak-forms-modal>`,
];
}
renderObjectCreate(): TemplateResult {
return html`
<ak-forms-modal>
<span slot="submit"> ${t`Create`} </span>
<span slot="header"> ${t`Create Interface`} </span>
<ak-interface-form slot="form"> </ak-interface-form>
<button slot="trigger" class="pf-c-button pf-m-primary">${t`Create`}</button>
</ak-forms-modal>
`;
}
}

View file

@ -23,6 +23,10 @@ import {
FlowsApi, FlowsApi,
FlowsInstancesListDesignationEnum, FlowsInstancesListDesignationEnum,
FlowsInstancesListRequest, FlowsInstancesListRequest,
Interface,
InterfacesApi,
InterfacesListRequest,
InterfacesListTypeEnum,
Tenant, Tenant,
} from "@goauthentik/api"; } from "@goauthentik/api";
@ -368,6 +372,107 @@ export class TenantForm extends ModelForm<Tenant, string> {
</ak-form-element-horizontal> </ak-form-element-horizontal>
</div> </div>
</ak-form-group> </ak-form-group>
<ak-form-group>
<span slot="header"> ${t`Interfaces`} </span>
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal label=${t`User Interface`} name="interfaceUser">
<ak-search-select
.fetchObjects=${async (query?: string): Promise<Interface[]> => {
const args: InterfacesListRequest = {
ordering: "url_name",
type: InterfacesListTypeEnum.User,
};
if (query !== undefined) {
args.search = query;
}
const flows = await new InterfacesApi(
DEFAULT_CONFIG,
).interfacesList(args);
return flows.results;
}}
.renderElement=${(iface: Interface): string => {
return iface.urlName;
}}
.renderDescription=${(iface: Interface): TemplateResult => {
return html`${iface.type}`;
}}
.value=${(iface: Interface | undefined): string | undefined => {
return iface?.interfaceUuid;
}}
.selected=${(iface: Interface): boolean => {
return this.instance?.interfaceUser === iface.interfaceUuid;
}}
?blankable=${true}
>
</ak-search-select>
<p class="pf-c-form__helper-text">${t`.`}</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${t`Flow Interface`} name="interfaceFlow">
<ak-search-select
.fetchObjects=${async (query?: string): Promise<Interface[]> => {
const args: InterfacesListRequest = {
ordering: "url_name",
type: InterfacesListTypeEnum.Flow,
};
if (query !== undefined) {
args.search = query;
}
const flows = await new InterfacesApi(
DEFAULT_CONFIG,
).interfacesList(args);
return flows.results;
}}
.renderElement=${(iface: Interface): string => {
return iface.urlName;
}}
.renderDescription=${(iface: Interface): TemplateResult => {
return html`${iface.type}`;
}}
.value=${(iface: Interface | undefined): string | undefined => {
return iface?.interfaceUuid;
}}
.selected=${(iface: Interface): boolean => {
return this.instance?.interfaceFlow === iface.interfaceUuid;
}}
?blankable=${true}
>
</ak-search-select>
<p class="pf-c-form__helper-text">${t`.`}</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${t`Admin Interface`} name="interfaceAdmin">
<ak-search-select
.fetchObjects=${async (query?: string): Promise<Interface[]> => {
const args: InterfacesListRequest = {
ordering: "url_name",
type: InterfacesListTypeEnum.Admin,
};
if (query !== undefined) {
args.search = query;
}
const flows = await new InterfacesApi(
DEFAULT_CONFIG,
).interfacesList(args);
return flows.results;
}}
.renderElement=${(iface: Interface): string => {
return iface.urlName;
}}
.renderDescription=${(iface: Interface): TemplateResult => {
return html`${iface.type}`;
}}
.value=${(iface: Interface | undefined): string | undefined => {
return iface?.interfaceUuid;
}}
.selected=${(iface: Interface): boolean => {
return this.instance?.interfaceAdmin === iface.interfaceUuid;
}}
?blankable=${true}
>
</ak-search-select>
<p class="pf-c-form__helper-text">${t`.`}</p>
</ak-form-element-horizontal>
</div>
</ak-form-group>
<ak-form-group> <ak-form-group>
<span slot="header"> ${t`Other global settings`} </span> <span slot="header"> ${t`Other global settings`} </span>
<div slot="body" class="pf-c-form"> <div slot="body" class="pf-c-form">