Compare commits
	
		
			10 Commits
		
	
	
		
			dependabot
			...
			interfaces
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| a1c1c3a27c | |||
| c0262f0802 | |||
| c6f8290ca1 | |||
| 905ae00e02 | |||
| 3ec477d58d | |||
| ff996f798f | |||
| 1889e82309 | |||
| 48a4080699 | |||
| 246a6c7384 | |||
| e39c460e3a | 
@ -18,6 +18,7 @@ from authentik.core.api.utils import PassiveSerializer
 | 
			
		||||
from authentik.lib.utils.reflection import get_env
 | 
			
		||||
from authentik.outposts.apps import MANAGED_OUTPOST
 | 
			
		||||
from authentik.outposts.models import Outpost
 | 
			
		||||
from authentik.tenants.utils import get_tenant
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class RuntimeDict(TypedDict):
 | 
			
		||||
@ -77,7 +78,7 @@ class SystemSerializer(PassiveSerializer):
 | 
			
		||||
 | 
			
		||||
    def get_tenant(self, request: Request) -> str:
 | 
			
		||||
        """Currently active tenant"""
 | 
			
		||||
        return str(request._request.tenant)
 | 
			
		||||
        return str(get_tenant(request))
 | 
			
		||||
 | 
			
		||||
    def get_server_time(self, request: Request) -> datetime:
 | 
			
		||||
        """Current server time"""
 | 
			
		||||
 | 
			
		||||
@ -33,6 +33,7 @@ from authentik.flows.api.flows import FlowViewSet
 | 
			
		||||
from authentik.flows.api.stages import StageViewSet
 | 
			
		||||
from authentik.flows.views.executor import FlowExecutorView
 | 
			
		||||
from authentik.flows.views.inspector import FlowInspectorView
 | 
			
		||||
from authentik.interfaces.api import InterfaceViewSet
 | 
			
		||||
from authentik.outposts.api.outposts import OutpostViewSet
 | 
			
		||||
from authentik.outposts.api.service_connections import (
 | 
			
		||||
    DockerServiceConnectionViewSet,
 | 
			
		||||
@ -123,6 +124,8 @@ router.register("core/user_consent", UserConsentViewSet)
 | 
			
		||||
router.register("core/tokens", TokenViewSet)
 | 
			
		||||
router.register("core/tenants", TenantViewSet)
 | 
			
		||||
 | 
			
		||||
router.register("interfaces", InterfaceViewSet)
 | 
			
		||||
 | 
			
		||||
router.register("outposts/instances", OutpostViewSet)
 | 
			
		||||
router.register("outposts/service_connections/all", ServiceConnectionViewSet)
 | 
			
		||||
router.register("outposts/service_connections/docker", DockerServiceConnectionViewSet)
 | 
			
		||||
 | 
			
		||||
@ -10,7 +10,6 @@ from django.db.models.functions import ExtractHour
 | 
			
		||||
from django.db.models.query import QuerySet
 | 
			
		||||
from django.db.transaction import atomic
 | 
			
		||||
from django.db.utils import IntegrityError
 | 
			
		||||
from django.urls import reverse_lazy
 | 
			
		||||
from django.utils.http import urlencode
 | 
			
		||||
from django.utils.text import slugify
 | 
			
		||||
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.planner import PLAN_CONTEXT_PENDING_USER, FlowPlanner
 | 
			
		||||
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.tasks import send_mails
 | 
			
		||||
from authentik.stages.email.utils import TemplateEmailMessage
 | 
			
		||||
from authentik.tenants.models import Tenant
 | 
			
		||||
from authentik.tenants.utils import get_tenant
 | 
			
		||||
 | 
			
		||||
LOGGER = get_logger()
 | 
			
		||||
 | 
			
		||||
@ -321,7 +322,7 @@ class UserViewSet(UsedByMixin, ModelViewSet):
 | 
			
		||||
    def _create_recovery_link(self) -> tuple[Optional[str], Optional[Token]]:
 | 
			
		||||
        """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"""
 | 
			
		||||
        tenant: Tenant = self.request._request.tenant
 | 
			
		||||
        tenant = get_tenant(self.request)
 | 
			
		||||
        # Check that there is a recovery flow, if not return an error
 | 
			
		||||
        flow = tenant.flow_recovery
 | 
			
		||||
        if not flow:
 | 
			
		||||
@ -350,8 +351,12 @@ class UserViewSet(UsedByMixin, ModelViewSet):
 | 
			
		||||
        )
 | 
			
		||||
        querystring = urlencode({QS_KEY_TOKEN: token.key})
 | 
			
		||||
        link = self.request.build_absolute_uri(
 | 
			
		||||
            reverse_lazy("authentik_core:if-flow", kwargs={"flow_slug": flow.slug})
 | 
			
		||||
            + f"?{querystring}"
 | 
			
		||||
            reverse_interface(
 | 
			
		||||
                self.request,
 | 
			
		||||
                InterfaceType.FLOW,
 | 
			
		||||
                flow_slug=flow.slug,
 | 
			
		||||
            ),
 | 
			
		||||
            +f"?{querystring}",
 | 
			
		||||
        )
 | 
			
		||||
        return link, token
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -33,6 +33,7 @@ from authentik.lib.models import (
 | 
			
		||||
)
 | 
			
		||||
from authentik.lib.utils.http import get_client_ip
 | 
			
		||||
from authentik.policies.models import PolicyBindingModel
 | 
			
		||||
from authentik.tenants.utils import get_tenant
 | 
			
		||||
 | 
			
		||||
LOGGER = get_logger()
 | 
			
		||||
USER_ATTRIBUTE_DEBUG = "goauthentik.io/user/debug"
 | 
			
		||||
@ -168,7 +169,7 @@ class User(SerializerModel, GuardianUserMixin, AbstractUser):
 | 
			
		||||
        including the users attributes"""
 | 
			
		||||
        final_attributes = {}
 | 
			
		||||
        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"):
 | 
			
		||||
            always_merger.merge(final_attributes, group.attributes)
 | 
			
		||||
        always_merger.merge(final_attributes, self.attributes)
 | 
			
		||||
@ -227,7 +228,7 @@ class User(SerializerModel, GuardianUserMixin, AbstractUser):
 | 
			
		||||
        except Exception as exc:
 | 
			
		||||
            LOGGER.warning("Failed to get default locale", exc=exc)
 | 
			
		||||
        if request:
 | 
			
		||||
            return request.tenant.locale
 | 
			
		||||
            return get_tenant(request).default_locale
 | 
			
		||||
        return ""
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
 | 
			
		||||
@ -25,7 +25,8 @@ from authentik.flows.planner import (
 | 
			
		||||
)
 | 
			
		||||
from authentik.flows.stage import StageView
 | 
			
		||||
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.policies.denied import AccessDeniedResponse
 | 
			
		||||
from authentik.policies.utils import delete_none_keys
 | 
			
		||||
@ -226,7 +227,7 @@ class SourceFlowManager:
 | 
			
		||||
        # Ensure redirect is carried through when user was trying to
 | 
			
		||||
        # authorize application
 | 
			
		||||
        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(
 | 
			
		||||
            {
 | 
			
		||||
@ -253,9 +254,9 @@ class SourceFlowManager:
 | 
			
		||||
            for stage in stages:
 | 
			
		||||
                plan.append_stage(stage)
 | 
			
		||||
        self.request.session[SESSION_KEY_PLAN] = plan
 | 
			
		||||
        return redirect_with_qs(
 | 
			
		||||
            "authentik_core:if-flow",
 | 
			
		||||
            self.request.GET,
 | 
			
		||||
        return redirect_to_default_interface(
 | 
			
		||||
            self.request,
 | 
			
		||||
            InterfaceType.FLOW,
 | 
			
		||||
            flow_slug=flow.slug,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
@ -299,8 +300,9 @@ class SourceFlowManager:
 | 
			
		||||
            _("Successfully linked %(source)s!" % {"source": self.source.name}),
 | 
			
		||||
        )
 | 
			
		||||
        return redirect(
 | 
			
		||||
            # Not ideal that we don't directly redirect to the configured user interface
 | 
			
		||||
            reverse(
 | 
			
		||||
                "authentik_core:if-user",
 | 
			
		||||
                "authentik_core:root-redirect",
 | 
			
		||||
            )
 | 
			
		||||
            + f"#/settings;page-{self.source.slug}"
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
@ -59,4 +59,6 @@ class TestImpersonation(TestCase):
 | 
			
		||||
        self.client.force_login(self.other_user)
 | 
			
		||||
 | 
			
		||||
        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"})
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
@ -3,23 +3,30 @@ from channels.auth import AuthMiddleware
 | 
			
		||||
from channels.sessions import CookieMiddleware
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from django.contrib.auth.decorators import login_required
 | 
			
		||||
from django.http import HttpRequest, HttpResponse
 | 
			
		||||
from django.urls import path
 | 
			
		||||
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.debug import AccessDeniedView
 | 
			
		||||
from authentik.core.views.interface import FlowInterfaceView, InterfaceView
 | 
			
		||||
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.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 = [
 | 
			
		||||
    path(
 | 
			
		||||
        "",
 | 
			
		||||
        login_required(
 | 
			
		||||
            RedirectView.as_view(pattern_name="authentik_core:if-user", query_string=True)
 | 
			
		||||
        ),
 | 
			
		||||
        login_required(RedirectToInterface.as_view(type=InterfaceType.USER)),
 | 
			
		||||
        name="root-redirect",
 | 
			
		||||
    ),
 | 
			
		||||
    path(
 | 
			
		||||
@ -40,31 +47,16 @@ urlpatterns = [
 | 
			
		||||
        name="impersonate-end",
 | 
			
		||||
    ),
 | 
			
		||||
    # Interfaces
 | 
			
		||||
    path(
 | 
			
		||||
        "if/admin/",
 | 
			
		||||
        ensure_csrf_cookie(InterfaceView.as_view(template_name="if/admin.html")),
 | 
			
		||||
        name="if-admin",
 | 
			
		||||
    ),
 | 
			
		||||
    path(
 | 
			
		||||
        "if/user/",
 | 
			
		||||
        ensure_csrf_cookie(InterfaceView.as_view(template_name="if/user.html")),
 | 
			
		||||
        name="if-user",
 | 
			
		||||
    ),
 | 
			
		||||
    path(
 | 
			
		||||
        "if/flow/<slug:flow_slug>/",
 | 
			
		||||
        ensure_csrf_cookie(FlowInterfaceView.as_view()),
 | 
			
		||||
        name="if-flow",
 | 
			
		||||
    ),
 | 
			
		||||
    path(
 | 
			
		||||
        "if/session-end/<slug:application_slug>/",
 | 
			
		||||
        ensure_csrf_cookie(EndSessionView.as_view()),
 | 
			
		||||
        name="if-session-end",
 | 
			
		||||
    ),
 | 
			
		||||
    # Fallback for WS
 | 
			
		||||
    path("ws/outpost/<uuid:pk>/", InterfaceView.as_view(template_name="if/admin.html")),
 | 
			
		||||
    path("ws/outpost/<uuid:pk>/", placeholder_view),
 | 
			
		||||
    path(
 | 
			
		||||
        "ws/client/",
 | 
			
		||||
        InterfaceView.as_view(template_name="if/admin.html"),
 | 
			
		||||
        placeholder_view,
 | 
			
		||||
    ),
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -20,11 +20,13 @@ from authentik.flows.views.executor import (
 | 
			
		||||
    SESSION_KEY_PLAN,
 | 
			
		||||
    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 (
 | 
			
		||||
    PLAN_CONTEXT_CONSENT_HEADER,
 | 
			
		||||
    PLAN_CONTEXT_CONSENT_PERMISSIONS,
 | 
			
		||||
)
 | 
			
		||||
from authentik.tenants.utils import get_tenant
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class RedirectToAppLaunch(View):
 | 
			
		||||
@ -59,7 +61,7 @@ class RedirectToAppLaunch(View):
 | 
			
		||||
            raise Http404
 | 
			
		||||
        plan.insert_stage(in_memory_stage(RedirectToAppStage))
 | 
			
		||||
        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):
 | 
			
		||||
 | 
			
		||||
@ -35,7 +35,7 @@ class ImpersonateInitView(View):
 | 
			
		||||
 | 
			
		||||
        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):
 | 
			
		||||
@ -48,7 +48,7 @@ class ImpersonateEndView(View):
 | 
			
		||||
            or SESSION_KEY_IMPERSONATE_ORIGINAL_USER not in request.session
 | 
			
		||||
        ):
 | 
			
		||||
            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]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -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)
 | 
			
		||||
@ -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.policies.models import PolicyBindingModel
 | 
			
		||||
from authentik.stages.email.utils import TemplateEmailMessage
 | 
			
		||||
from authentik.tenants.models import Tenant
 | 
			
		||||
from authentik.tenants.utils import DEFAULT_TENANT
 | 
			
		||||
from authentik.tenants.utils import get_fallback_tenant, get_tenant
 | 
			
		||||
 | 
			
		||||
LOGGER = get_logger()
 | 
			
		||||
if TYPE_CHECKING:
 | 
			
		||||
@ -57,7 +56,7 @@ def default_event_duration():
 | 
			
		||||
 | 
			
		||||
def default_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):
 | 
			
		||||
@ -227,7 +226,7 @@ class Event(SerializerModel, ExpiringModel):
 | 
			
		||||
                wrapped = self.context["http_request"]["args"][QS_QUERY]
 | 
			
		||||
                self.context["http_request"]["args"] = QueryDict(wrapped)
 | 
			
		||||
        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
 | 
			
		||||
            # hence we set self.created to now and then use it
 | 
			
		||||
            self.created = now()
 | 
			
		||||
 | 
			
		||||
@ -25,6 +25,8 @@ from authentik.flows.exceptions import FlowNonApplicableException
 | 
			
		||||
from authentik.flows.models import Flow
 | 
			
		||||
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.interfaces.models import InterfaceType
 | 
			
		||||
from authentik.interfaces.views import reverse_interface
 | 
			
		||||
from authentik.lib.utils.file import (
 | 
			
		||||
    FilePathSerializer,
 | 
			
		||||
    FileUploadSerializer,
 | 
			
		||||
@ -294,7 +296,11 @@ class FlowViewSet(UsedByMixin, ModelViewSet):
 | 
			
		||||
        return Response(
 | 
			
		||||
            {
 | 
			
		||||
                "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,
 | 
			
		||||
                    ),
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
@ -7,6 +7,8 @@ from authentik.core.tests.utils import create_test_flow
 | 
			
		||||
from authentik.flows.models import Flow, FlowDesignation
 | 
			
		||||
from authentik.flows.planner import FlowPlan
 | 
			
		||||
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.providers.oauth2.models import OAuth2Provider
 | 
			
		||||
 | 
			
		||||
@ -21,7 +23,10 @@ class TestHelperView(TestCase):
 | 
			
		||||
        response = self.client.get(
 | 
			
		||||
            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.url, expected_url)
 | 
			
		||||
 | 
			
		||||
@ -72,6 +77,9 @@ class TestHelperView(TestCase):
 | 
			
		||||
        response = self.client.get(
 | 
			
		||||
            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.url, expected_url)
 | 
			
		||||
 | 
			
		||||
@ -53,12 +53,14 @@ from authentik.flows.planner import (
 | 
			
		||||
    FlowPlanner,
 | 
			
		||||
)
 | 
			
		||||
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.utils.errors import exception_to_string
 | 
			
		||||
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.policies.engine import PolicyEngine
 | 
			
		||||
from authentik.tenants.models import Tenant
 | 
			
		||||
from authentik.tenants.utils import get_tenant
 | 
			
		||||
 | 
			
		||||
LOGGER = get_logger()
 | 
			
		||||
# Argument used to redirect user after login
 | 
			
		||||
@ -479,7 +481,7 @@ class ToDefaultFlow(View):
 | 
			
		||||
 | 
			
		||||
    def get_flow(self) -> Flow:
 | 
			
		||||
        """Get a flow for the selected designation"""
 | 
			
		||||
        tenant: Tenant = self.request.tenant
 | 
			
		||||
        tenant = get_tenant(self.request)
 | 
			
		||||
        flow = None
 | 
			
		||||
        # First, attempt to get default flow from tenant
 | 
			
		||||
        if self.designation == FlowDesignation.AUTHENTICATION:
 | 
			
		||||
@ -512,7 +514,7 @@ class ToDefaultFlow(View):
 | 
			
		||||
                    flow_slug=flow.slug,
 | 
			
		||||
                )
 | 
			
		||||
                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:
 | 
			
		||||
@ -583,8 +585,8 @@ class ConfigureFlowInitView(LoginRequiredMixin, View):
 | 
			
		||||
            LOGGER.warning("Flow not applicable to user")
 | 
			
		||||
            raise Http404
 | 
			
		||||
        request.session[SESSION_KEY_PLAN] = plan
 | 
			
		||||
        return redirect_with_qs(
 | 
			
		||||
            "authentik_core:if-flow",
 | 
			
		||||
            self.request.GET,
 | 
			
		||||
        return redirect_to_default_interface(
 | 
			
		||||
            self.request,
 | 
			
		||||
            InterfaceType.FLOW,
 | 
			
		||||
            flow_slug=stage.configure_flow.slug,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										0
									
								
								authentik/interfaces/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								authentik/interfaces/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										28
									
								
								authentik/interfaces/api.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								authentik/interfaces/api.py
									
									
									
									
									
										Normal 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"]
 | 
			
		||||
							
								
								
									
										12
									
								
								authentik/interfaces/apps.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								authentik/interfaces/apps.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,12 @@
 | 
			
		||||
"""authentik interfaces app config"""
 | 
			
		||||
from authentik.blueprints.apps import ManagedAppConfig
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AuthentikInterfacesConfig(ManagedAppConfig):
 | 
			
		||||
    """authentik interfaces app config"""
 | 
			
		||||
 | 
			
		||||
    name = "authentik.interfaces"
 | 
			
		||||
    label = "authentik_interfaces"
 | 
			
		||||
    verbose_name = "authentik Interfaces"
 | 
			
		||||
    mountpoint = "if/"
 | 
			
		||||
    default = True
 | 
			
		||||
							
								
								
									
										36
									
								
								authentik/interfaces/migrations/0001_initial.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								authentik/interfaces/migrations/0001_initial.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,36 @@
 | 
			
		||||
# Generated by Django 4.1.7 on 2023-02-16 11:01
 | 
			
		||||
 | 
			
		||||
import uuid
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
    initial = True
 | 
			
		||||
 | 
			
		||||
    dependencies = []
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.CreateModel(
 | 
			
		||||
            name="Interface",
 | 
			
		||||
            fields=[
 | 
			
		||||
                (
 | 
			
		||||
                    "interface_uuid",
 | 
			
		||||
                    models.UUIDField(
 | 
			
		||||
                        default=uuid.uuid4, editable=False, primary_key=True, serialize=False
 | 
			
		||||
                    ),
 | 
			
		||||
                ),
 | 
			
		||||
                ("url_name", models.SlugField(unique=True)),
 | 
			
		||||
                (
 | 
			
		||||
                    "type",
 | 
			
		||||
                    models.TextField(
 | 
			
		||||
                        choices=[("user", "User"), ("admin", "Admin"), ("flow", "Flow")]
 | 
			
		||||
                    ),
 | 
			
		||||
                ),
 | 
			
		||||
                ("template", models.TextField()),
 | 
			
		||||
            ],
 | 
			
		||||
            options={
 | 
			
		||||
                "abstract": False,
 | 
			
		||||
            },
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
							
								
								
									
										0
									
								
								authentik/interfaces/migrations/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								authentik/interfaces/migrations/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										33
									
								
								authentik/interfaces/models.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								authentik/interfaces/models.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,33 @@
 | 
			
		||||
"""Interface models"""
 | 
			
		||||
from typing import Type
 | 
			
		||||
from uuid import uuid4
 | 
			
		||||
 | 
			
		||||
from django.db import models
 | 
			
		||||
from rest_framework.serializers import BaseSerializer
 | 
			
		||||
 | 
			
		||||
from authentik.lib.models import SerializerModel
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class InterfaceType(models.TextChoices):
 | 
			
		||||
    """Interface types"""
 | 
			
		||||
 | 
			
		||||
    USER = "user"
 | 
			
		||||
    ADMIN = "admin"
 | 
			
		||||
    FLOW = "flow"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Interface(SerializerModel):
 | 
			
		||||
    """Interface"""
 | 
			
		||||
 | 
			
		||||
    interface_uuid = models.UUIDField(primary_key=True, editable=False, default=uuid4)
 | 
			
		||||
 | 
			
		||||
    url_name = models.SlugField(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
 | 
			
		||||
							
								
								
									
										12
									
								
								authentik/interfaces/tests.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								authentik/interfaces/tests.py
									
									
									
									
									
										Normal 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)
 | 
			
		||||
							
								
								
									
										14
									
								
								authentik/interfaces/urls.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								authentik/interfaces/urls.py
									
									
									
									
									
										Normal 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"),
 | 
			
		||||
]
 | 
			
		||||
							
								
								
									
										113
									
								
								authentik/interfaces/views.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										113
									
								
								authentik/interfaces/views.py
									
									
									
									
									
										Normal 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)
 | 
			
		||||
@ -12,6 +12,7 @@ from authentik.lib.utils.http import get_http_session
 | 
			
		||||
from authentik.policies.models import Policy
 | 
			
		||||
from authentik.policies.types import PolicyRequest, PolicyResult
 | 
			
		||||
from authentik.stages.prompt.stage import PLAN_CONTEXT_PROMPT
 | 
			
		||||
from authentik.tenants.utils import get_tenant
 | 
			
		||||
 | 
			
		||||
LOGGER = get_logger()
 | 
			
		||||
RE_LOWER = re.compile("[a-z]")
 | 
			
		||||
@ -143,7 +144,8 @@ class PasswordPolicy(Policy):
 | 
			
		||||
            user_inputs.append(request.user.name)
 | 
			
		||||
            user_inputs.append(request.user.email)
 | 
			
		||||
        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
 | 
			
		||||
        # long passwords we can be reasonably sure that they'll surpass the score anyways
 | 
			
		||||
        # See https://github.com/dropbox/zxcvbn#runtime-latency
 | 
			
		||||
 | 
			
		||||
@ -39,8 +39,9 @@ class TesOAuth2DeviceInit(OAuthTestCase):
 | 
			
		||||
        self.assertEqual(
 | 
			
		||||
            res.url,
 | 
			
		||||
            reverse(
 | 
			
		||||
                "authentik_core:if-flow",
 | 
			
		||||
                "authentik_interfaces:if",
 | 
			
		||||
                kwargs={
 | 
			
		||||
                    "if_name": "flow",
 | 
			
		||||
                    "flow_slug": self.device_flow.slug,
 | 
			
		||||
                },
 | 
			
		||||
            ),
 | 
			
		||||
@ -68,8 +69,9 @@ class TesOAuth2DeviceInit(OAuthTestCase):
 | 
			
		||||
        self.assertEqual(
 | 
			
		||||
            res.url,
 | 
			
		||||
            reverse(
 | 
			
		||||
                "authentik_core:if-flow",
 | 
			
		||||
                "authentik_interfaces:if",
 | 
			
		||||
                kwargs={
 | 
			
		||||
                    "if_name": "flow",
 | 
			
		||||
                    "flow_slug": self.provider.authorization_flow.slug,
 | 
			
		||||
                },
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
@ -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.stage import StageView
 | 
			
		||||
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.urls import redirect_with_qs
 | 
			
		||||
from authentik.lib.views import bad_request_message
 | 
			
		||||
from authentik.policies.types import PolicyRequest
 | 
			
		||||
from authentik.policies.views import PolicyAccessView, RequestValidationError
 | 
			
		||||
@ -404,9 +405,9 @@ class AuthorizationFlowInitView(PolicyAccessView):
 | 
			
		||||
        plan.append_stage(in_memory_stage(OAuthFulfillmentStage))
 | 
			
		||||
 | 
			
		||||
        self.request.session[SESSION_KEY_PLAN] = plan
 | 
			
		||||
        return redirect_with_qs(
 | 
			
		||||
            "authentik_core:if-flow",
 | 
			
		||||
            self.request.GET,
 | 
			
		||||
        return redirect_to_default_interface(
 | 
			
		||||
            self.request,
 | 
			
		||||
            InterfaceType.FLOW,
 | 
			
		||||
            flow_slug=self.provider.authorization_flow.slug,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -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.stage import ChallengeStageView
 | 
			
		||||
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.views.device_finish import (
 | 
			
		||||
    PLAN_CONTEXT_DEVICE,
 | 
			
		||||
@ -26,7 +27,7 @@ from authentik.stages.consent.stage import (
 | 
			
		||||
    PLAN_CONTEXT_CONSENT_HEADER,
 | 
			
		||||
    PLAN_CONTEXT_CONSENT_PERMISSIONS,
 | 
			
		||||
)
 | 
			
		||||
from authentik.tenants.models import Tenant
 | 
			
		||||
from authentik.tenants.utils import get_tenant
 | 
			
		||||
 | 
			
		||||
LOGGER = get_logger()
 | 
			
		||||
QS_KEY_CODE = "code"  # nosec
 | 
			
		||||
@ -77,9 +78,9 @@ def validate_code(code: int, request: HttpRequest) -> Optional[HttpResponse]:
 | 
			
		||||
        return None
 | 
			
		||||
    plan.insert_stage(in_memory_stage(OAuthDeviceCodeFinishStage))
 | 
			
		||||
    request.session[SESSION_KEY_PLAN] = plan
 | 
			
		||||
    return redirect_with_qs(
 | 
			
		||||
        "authentik_core:if-flow",
 | 
			
		||||
        request.GET,
 | 
			
		||||
    return redirect_to_default_interface(
 | 
			
		||||
        request,
 | 
			
		||||
        InterfaceType.FLOW,
 | 
			
		||||
        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"""
 | 
			
		||||
 | 
			
		||||
    def dispatch(self, request: HttpRequest) -> HttpResponse:
 | 
			
		||||
        tenant: Tenant = request.tenant
 | 
			
		||||
        tenant = get_tenant(request)
 | 
			
		||||
        device_flow = tenant.flow_device_code
 | 
			
		||||
        if not device_flow:
 | 
			
		||||
            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))
 | 
			
		||||
 | 
			
		||||
        self.request.session[SESSION_KEY_PLAN] = plan
 | 
			
		||||
        return redirect_with_qs(
 | 
			
		||||
            "authentik_core:if-flow",
 | 
			
		||||
            self.request.GET,
 | 
			
		||||
        return redirect_to_default_interface(
 | 
			
		||||
            self.request,
 | 
			
		||||
            InterfaceType.FLOW,
 | 
			
		||||
            flow_slug=device_flow.slug,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -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.models import RefreshToken
 | 
			
		||||
from authentik.providers.oauth2.utils import protected_resource_view
 | 
			
		||||
from authentik.tenants.utils import get_tenant
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@method_decorator(csrf_exempt, name="dispatch")
 | 
			
		||||
@ -76,6 +77,7 @@ class GitHubUserTeamsView(View):
 | 
			
		||||
    def get(self, request: HttpRequest, token: RefreshToken) -> HttpResponse:
 | 
			
		||||
        """Emulate GitHub's /user/teams API Endpoint"""
 | 
			
		||||
        user = token.user
 | 
			
		||||
        tenant = get_tenant(request)
 | 
			
		||||
 | 
			
		||||
        orgs_response = []
 | 
			
		||||
        for org in user.ak_groups.all():
 | 
			
		||||
@ -97,7 +99,7 @@ class GitHubUserTeamsView(View):
 | 
			
		||||
                "created_at": "",
 | 
			
		||||
                "updated_at": "",
 | 
			
		||||
                "organization": {
 | 
			
		||||
                    "login": slugify(request.tenant.branding_title),
 | 
			
		||||
                    "login": slugify(tenant.branding_title),
 | 
			
		||||
                    "id": 1,
 | 
			
		||||
                    "node_id": "",
 | 
			
		||||
                    "url": "",
 | 
			
		||||
@ -109,7 +111,7 @@ class GitHubUserTeamsView(View):
 | 
			
		||||
                    "public_members_url": "",
 | 
			
		||||
                    "avatar_url": "",
 | 
			
		||||
                    "description": "",
 | 
			
		||||
                    "name": request.tenant.branding_title,
 | 
			
		||||
                    "name": tenant.branding_title,
 | 
			
		||||
                    "company": "",
 | 
			
		||||
                    "blog": "",
 | 
			
		||||
                    "location": "",
 | 
			
		||||
 | 
			
		||||
@ -15,7 +15,8 @@ from authentik.flows.exceptions import FlowNonApplicableException
 | 
			
		||||
from authentik.flows.models import in_memory_stage
 | 
			
		||||
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.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.policies.views import PolicyAccessView
 | 
			
		||||
from authentik.providers.saml.exceptions import CannotHandleAssertion
 | 
			
		||||
@ -76,9 +77,9 @@ class SAMLSSOView(PolicyAccessView):
 | 
			
		||||
            raise Http404
 | 
			
		||||
        plan.append_stage(in_memory_stage(SAMLFlowFinalView))
 | 
			
		||||
        request.session[SESSION_KEY_PLAN] = plan
 | 
			
		||||
        return redirect_with_qs(
 | 
			
		||||
            "authentik_core:if-flow",
 | 
			
		||||
            request.GET,
 | 
			
		||||
        return redirect_to_default_interface(
 | 
			
		||||
            request,
 | 
			
		||||
            InterfaceType.FLOW,
 | 
			
		||||
            flow_slug=self.provider.authorization_flow.slug,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -22,4 +22,4 @@ class UseTokenView(View):
 | 
			
		||||
        login(request, token.user, backend=BACKEND_INBUILT)
 | 
			
		||||
        token.delete()
 | 
			
		||||
        messages.warning(request, _("Used recovery-link to authenticate."))
 | 
			
		||||
        return redirect("authentik_core:if-user")
 | 
			
		||||
        return redirect("authentik_core:root-redirect")
 | 
			
		||||
 | 
			
		||||
@ -65,6 +65,7 @@ INSTALLED_APPS = [
 | 
			
		||||
    "authentik.admin",
 | 
			
		||||
    "authentik.api",
 | 
			
		||||
    "authentik.crypto",
 | 
			
		||||
    "authentik.interfaces",
 | 
			
		||||
    "authentik.events",
 | 
			
		||||
    "authentik.flows",
 | 
			
		||||
    "authentik.lib",
 | 
			
		||||
 | 
			
		||||
@ -32,7 +32,8 @@ from authentik.flows.planner import (
 | 
			
		||||
)
 | 
			
		||||
from authentik.flows.stage import ChallengeStageView
 | 
			
		||||
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.providers.saml.utils.encoding import nice64
 | 
			
		||||
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
 | 
			
		||||
        # authorize application
 | 
			
		||||
        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(
 | 
			
		||||
            {
 | 
			
		||||
@ -91,9 +92,9 @@ class InitiateView(View):
 | 
			
		||||
        for stage in stages_to_append:
 | 
			
		||||
            plan.append_stage(stage)
 | 
			
		||||
        self.request.session[SESSION_KEY_PLAN] = plan
 | 
			
		||||
        return redirect_with_qs(
 | 
			
		||||
            "authentik_core:if-flow",
 | 
			
		||||
            self.request.GET,
 | 
			
		||||
        return redirect_to_default_interface(
 | 
			
		||||
            self.request,
 | 
			
		||||
            InterfaceType.FLOW,
 | 
			
		||||
            flow_slug=source.pre_authentication_flow.slug,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -17,6 +17,7 @@ from authentik.flows.challenge import (
 | 
			
		||||
from authentik.flows.stage import ChallengeStageView
 | 
			
		||||
from authentik.stages.authenticator_totp.models import AuthenticatorTOTPStage
 | 
			
		||||
from authentik.stages.authenticator_totp.settings import OTP_TOTP_ISSUER
 | 
			
		||||
from authentik.tenants.utils import get_tenant
 | 
			
		||||
 | 
			
		||||
SESSION_TOTP_DEVICE = "totp_device"
 | 
			
		||||
 | 
			
		||||
@ -57,7 +58,7 @@ class AuthenticatorTOTPStageView(ChallengeStageView):
 | 
			
		||||
            data={
 | 
			
		||||
                "type": ChallengeTypes.NATIVE.value,
 | 
			
		||||
                "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)
 | 
			
		||||
                ),
 | 
			
		||||
            }
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
@ -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.stage import SESSION_KEY_WEBAUTHN_CHALLENGE
 | 
			
		||||
from authentik.stages.authenticator_webauthn.utils import get_origin, get_rp_id
 | 
			
		||||
from authentik.tenants.utils import get_tenant
 | 
			
		||||
 | 
			
		||||
LOGGER = get_logger()
 | 
			
		||||
 | 
			
		||||
@ -187,7 +188,7 @@ def validate_challenge_duo(device_pk: int, stage_view: StageView, user: User) ->
 | 
			
		||||
            type=__(
 | 
			
		||||
                "%(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,
 | 
			
		||||
 | 
			
		||||
@ -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.models import AuthenticatorValidateStage, DeviceClasses
 | 
			
		||||
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):
 | 
			
		||||
@ -36,7 +36,7 @@ class AuthenticatorValidateStageDuoTests(FlowTestCase):
 | 
			
		||||
        middleware = SessionMiddleware(dummy_get_response)
 | 
			
		||||
        middleware.process_request(request)
 | 
			
		||||
        request.session.save()
 | 
			
		||||
        setattr(request, "tenant", get_tenant_for_request(request))
 | 
			
		||||
        setattr(request, "tenant", lookup_tenant_for_request(request))
 | 
			
		||||
 | 
			
		||||
        stage = AuthenticatorDuoStage.objects.create(
 | 
			
		||||
            name=generate_id(),
 | 
			
		||||
 | 
			
		||||
@ -29,6 +29,7 @@ from authentik.flows.challenge import (
 | 
			
		||||
from authentik.flows.stage import ChallengeStageView
 | 
			
		||||
from authentik.stages.authenticator_webauthn.models import AuthenticateWebAuthnStage, WebAuthnDevice
 | 
			
		||||
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"
 | 
			
		||||
 | 
			
		||||
@ -92,7 +93,7 @@ class AuthenticatorWebAuthnStageView(ChallengeStageView):
 | 
			
		||||
 | 
			
		||||
        registration_options: PublicKeyCredentialCreationOptions = generate_registration_options(
 | 
			
		||||
            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_name=user.username,
 | 
			
		||||
            user_display_name=user.name,
 | 
			
		||||
 | 
			
		||||
@ -3,7 +3,6 @@ from datetime import timedelta
 | 
			
		||||
 | 
			
		||||
from django.contrib import messages
 | 
			
		||||
from django.http import HttpRequest, HttpResponse
 | 
			
		||||
from django.urls import reverse
 | 
			
		||||
from django.utils.http import urlencode
 | 
			
		||||
from django.utils.text import slugify
 | 
			
		||||
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.stage import ChallengeStageView
 | 
			
		||||
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.tasks import send_mails
 | 
			
		||||
from authentik.stages.email.utils import TemplateEmailMessage
 | 
			
		||||
@ -47,9 +48,10 @@ class EmailStageView(ChallengeStageView):
 | 
			
		||||
 | 
			
		||||
    def get_full_url(self, **kwargs) -> str:
 | 
			
		||||
        """Get full URL to be used in template"""
 | 
			
		||||
        base_url = reverse(
 | 
			
		||||
            "authentik_core:if-flow",
 | 
			
		||||
            kwargs={"flow_slug": self.executor.flow.slug},
 | 
			
		||||
        base_url = reverse_interface(
 | 
			
		||||
            self.request,
 | 
			
		||||
            InterfaceType.FLOW,
 | 
			
		||||
            flow_slug=self.executor.flow.slug,
 | 
			
		||||
        )
 | 
			
		||||
        relative_url = f"{base_url}?{urlencode(kwargs)}"
 | 
			
		||||
        return self.request.build_absolute_uri(relative_url)
 | 
			
		||||
 | 
			
		||||
@ -7,6 +7,7 @@ from django.core.mail.backends.locmem import EmailBackend
 | 
			
		||||
from django.urls import reverse
 | 
			
		||||
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.events.models import Event, EventAction
 | 
			
		||||
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)
 | 
			
		||||
 | 
			
		||||
    @apply_blueprint("system/interfaces.yaml")
 | 
			
		||||
    def test_pending_user(self):
 | 
			
		||||
        """Test with pending user"""
 | 
			
		||||
        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["from_email"], "system@authentik.local")
 | 
			
		||||
 | 
			
		||||
    @apply_blueprint("system/interfaces.yaml")
 | 
			
		||||
    def test_send_error(self):
 | 
			
		||||
        """Test error during sending (sending will be retried)"""
 | 
			
		||||
        plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
 | 
			
		||||
 | 
			
		||||
@ -7,6 +7,7 @@ from django.core.mail.backends.smtp import EmailBackend as SMTPEmailBackend
 | 
			
		||||
from django.urls import reverse
 | 
			
		||||
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.flows.markers import StageMarker
 | 
			
		||||
from authentik.flows.models import FlowDesignation, FlowStageBinding, FlowToken
 | 
			
		||||
@ -74,6 +75,7 @@ class TestEmailStage(FlowTestCase):
 | 
			
		||||
        response = self.client.get(url)
 | 
			
		||||
        self.assertEqual(response.status_code, 200)
 | 
			
		||||
 | 
			
		||||
    @apply_blueprint("system/interfaces.yaml")
 | 
			
		||||
    @patch(
 | 
			
		||||
        "authentik.stages.email.models.EmailStage.backend_class",
 | 
			
		||||
        PropertyMock(return_value=EmailBackend),
 | 
			
		||||
@ -123,6 +125,7 @@ class TestEmailStage(FlowTestCase):
 | 
			
		||||
        with self.settings(EMAIL_HOST=host):
 | 
			
		||||
            self.assertEqual(EmailStage(use_global_settings=True).backend.host, host)
 | 
			
		||||
 | 
			
		||||
    @apply_blueprint("system/interfaces.yaml")
 | 
			
		||||
    def test_token(self):
 | 
			
		||||
        """Test with token"""
 | 
			
		||||
        # Make sure token exists
 | 
			
		||||
 | 
			
		||||
@ -26,8 +26,9 @@ from authentik.flows.models import FlowDesignation
 | 
			
		||||
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER
 | 
			
		||||
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.interfaces.models import InterfaceType
 | 
			
		||||
from authentik.interfaces.views import reverse_interface
 | 
			
		||||
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.plex.models import PlexAuthenticationChallenge
 | 
			
		||||
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)
 | 
			
		||||
        # Check for related enrollment and recovery flow, add URL to view
 | 
			
		||||
        if current_stage.enrollment_flow:
 | 
			
		||||
            challenge.initial_data["enroll_url"] = reverse_with_qs(
 | 
			
		||||
                "authentik_core:if-flow",
 | 
			
		||||
            challenge.initial_data["enroll_url"] = reverse_interface(
 | 
			
		||||
                self.request,
 | 
			
		||||
                InterfaceType.FLOW,
 | 
			
		||||
                query=get_qs,
 | 
			
		||||
                kwargs={"flow_slug": current_stage.enrollment_flow.slug},
 | 
			
		||||
                flow_slug=current_stage.enrollment_flow.slug,
 | 
			
		||||
            )
 | 
			
		||||
        if current_stage.recovery_flow:
 | 
			
		||||
            challenge.initial_data["recovery_url"] = reverse_with_qs(
 | 
			
		||||
                "authentik_core:if-flow",
 | 
			
		||||
            challenge.initial_data["recovery_url"] = reverse_interface(
 | 
			
		||||
                self.request,
 | 
			
		||||
                InterfaceType.FLOW,
 | 
			
		||||
                query=get_qs,
 | 
			
		||||
                kwargs={"flow_slug": current_stage.recovery_flow.slug},
 | 
			
		||||
                flow_slug=current_stage.recovery_flow.slug,
 | 
			
		||||
            )
 | 
			
		||||
        if current_stage.passwordless_flow:
 | 
			
		||||
            challenge.initial_data["passwordless_url"] = reverse_with_qs(
 | 
			
		||||
                "authentik_core:if-flow",
 | 
			
		||||
            challenge.initial_data["passwordless_url"] = reverse_interface(
 | 
			
		||||
                self.request,
 | 
			
		||||
                InterfaceType.FLOW,
 | 
			
		||||
                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.
 | 
			
		||||
 | 
			
		||||
@ -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.models import FlowDesignation, FlowStageBinding
 | 
			
		||||
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.stages.identification.models import IdentificationStage, UserFields
 | 
			
		||||
from authentik.stages.password import BACKEND_INBUILT
 | 
			
		||||
@ -166,9 +168,9 @@ class TestIdentificationStage(FlowTestCase):
 | 
			
		||||
            component="ak-stage-identification",
 | 
			
		||||
            user_fields=["email"],
 | 
			
		||||
            password_fields=False,
 | 
			
		||||
            enroll_url=reverse(
 | 
			
		||||
                "authentik_core:if-flow",
 | 
			
		||||
                kwargs={"flow_slug": flow.slug},
 | 
			
		||||
            enroll_url=reverse_interface(
 | 
			
		||||
                InterfaceType.FLOW,
 | 
			
		||||
                flow_slug=flow.slug,
 | 
			
		||||
            ),
 | 
			
		||||
            show_source_labels=False,
 | 
			
		||||
            primary_action="Log in",
 | 
			
		||||
@ -204,9 +206,9 @@ class TestIdentificationStage(FlowTestCase):
 | 
			
		||||
            component="ak-stage-identification",
 | 
			
		||||
            user_fields=["email"],
 | 
			
		||||
            password_fields=False,
 | 
			
		||||
            recovery_url=reverse(
 | 
			
		||||
                "authentik_core:if-flow",
 | 
			
		||||
                kwargs={"flow_slug": flow.slug},
 | 
			
		||||
            recovery_url=reverse_interface(
 | 
			
		||||
                InterfaceType.FLOW,
 | 
			
		||||
                flow_slug=flow.slug,
 | 
			
		||||
            ),
 | 
			
		||||
            show_source_labels=False,
 | 
			
		||||
            primary_action="Log in",
 | 
			
		||||
 | 
			
		||||
@ -5,7 +5,6 @@ from django.contrib.auth import _clean_credentials
 | 
			
		||||
from django.contrib.auth.backends import BaseBackend
 | 
			
		||||
from django.core.exceptions import PermissionDenied
 | 
			
		||||
from django.http import HttpRequest, HttpResponse
 | 
			
		||||
from django.urls import reverse
 | 
			
		||||
from django.utils.translation import gettext as _
 | 
			
		||||
from rest_framework.exceptions import ErrorDetail, ValidationError
 | 
			
		||||
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.planner import PLAN_CONTEXT_PENDING_USER
 | 
			
		||||
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.stages.password.models import PasswordStage
 | 
			
		||||
 | 
			
		||||
@ -95,11 +96,12 @@ class PasswordStageView(ChallengeStageView):
 | 
			
		||||
                "type": ChallengeTypes.NATIVE.value,
 | 
			
		||||
            }
 | 
			
		||||
        )
 | 
			
		||||
        recovery_flow = Flow.objects.filter(designation=FlowDesignation.RECOVERY)
 | 
			
		||||
        if recovery_flow.exists():
 | 
			
		||||
            recover_url = reverse(
 | 
			
		||||
                "authentik_core:if-flow",
 | 
			
		||||
                kwargs={"flow_slug": recovery_flow.first().slug},
 | 
			
		||||
        recovery_flow = Flow.objects.filter(designation=FlowDesignation.RECOVERY).first()
 | 
			
		||||
        if recovery_flow:
 | 
			
		||||
            recover_url = reverse_interface(
 | 
			
		||||
                self.request,
 | 
			
		||||
                InterfaceType.FLOW,
 | 
			
		||||
                flow_slug=recovery_flow.slug,
 | 
			
		||||
            )
 | 
			
		||||
            challenge.initial_data["recovery_url"] = self.request.build_absolute_uri(recover_url)
 | 
			
		||||
        return challenge
 | 
			
		||||
 | 
			
		||||
@ -18,6 +18,7 @@ from authentik.core.api.used_by import UsedByMixin
 | 
			
		||||
from authentik.core.api.utils import PassiveSerializer
 | 
			
		||||
from authentik.lib.config import CONFIG
 | 
			
		||||
from authentik.tenants.models import Tenant
 | 
			
		||||
from authentik.tenants.utils import get_tenant
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class FooterLinkSerializer(PassiveSerializer):
 | 
			
		||||
@ -54,6 +55,9 @@ class TenantSerializer(ModelSerializer):
 | 
			
		||||
            "flow_unenrollment",
 | 
			
		||||
            "flow_user_settings",
 | 
			
		||||
            "flow_device_code",
 | 
			
		||||
            "interface_admin",
 | 
			
		||||
            "interface_user",
 | 
			
		||||
            "interface_flow",
 | 
			
		||||
            "event_retention",
 | 
			
		||||
            "web_certificate",
 | 
			
		||||
            "attributes",
 | 
			
		||||
@ -120,6 +124,9 @@ class TenantViewSet(UsedByMixin, ModelViewSet):
 | 
			
		||||
        "flow_unenrollment",
 | 
			
		||||
        "flow_user_settings",
 | 
			
		||||
        "flow_device_code",
 | 
			
		||||
        "interface_admin",
 | 
			
		||||
        "interface_user",
 | 
			
		||||
        "interface_flow",
 | 
			
		||||
        "event_retention",
 | 
			
		||||
        "web_certificate",
 | 
			
		||||
    ]
 | 
			
		||||
@ -133,5 +140,4 @@ class TenantViewSet(UsedByMixin, ModelViewSet):
 | 
			
		||||
    @action(methods=["GET"], detail=False, permission_classes=[AllowAny])
 | 
			
		||||
    def current(self, request: Request) -> Response:
 | 
			
		||||
        """Get current tenant"""
 | 
			
		||||
        tenant: Tenant = request._request.tenant
 | 
			
		||||
        return Response(CurrentTenantSerializer(tenant).data)
 | 
			
		||||
        return Response(CurrentTenantSerializer(get_tenant(request)).data)
 | 
			
		||||
 | 
			
		||||
@ -6,7 +6,7 @@ from django.http.response import HttpResponse
 | 
			
		||||
from django.utils.translation import activate
 | 
			
		||||
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:
 | 
			
		||||
@ -19,7 +19,7 @@ class TenantMiddleware:
 | 
			
		||||
 | 
			
		||||
    def __call__(self, request: HttpRequest) -> HttpResponse:
 | 
			
		||||
        if not hasattr(request, "tenant"):
 | 
			
		||||
            tenant = get_tenant_for_request(request)
 | 
			
		||||
            tenant = lookup_tenant_for_request(request)
 | 
			
		||||
            setattr(request, "tenant", tenant)
 | 
			
		||||
            set_tag("authentik.tenant_uuid", tenant.tenant_uuid.hex)
 | 
			
		||||
            set_tag("authentik.tenant_domain", tenant.domain)
 | 
			
		||||
 | 
			
		||||
@ -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),
 | 
			
		||||
    ]
 | 
			
		||||
@ -7,7 +7,6 @@ from rest_framework.serializers import Serializer
 | 
			
		||||
from structlog.stdlib import get_logger
 | 
			
		||||
 | 
			
		||||
from authentik.crypto.models import CertificateKeyPair
 | 
			
		||||
from authentik.flows.models import Flow
 | 
			
		||||
from authentik.lib.models import SerializerModel
 | 
			
		||||
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")
 | 
			
		||||
 | 
			
		||||
    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, 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, 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, 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, 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, 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(
 | 
			
		||||
 | 
			
		||||
@ -75,7 +75,7 @@ class TestTenants(APITestCase):
 | 
			
		||||
        )
 | 
			
		||||
        factory = RequestFactory()
 | 
			
		||||
        request = factory.get("/")
 | 
			
		||||
        request.tenant = tenant
 | 
			
		||||
        setattr(request, "tenant", tenant)
 | 
			
		||||
        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(
 | 
			
		||||
 | 
			
		||||
@ -4,17 +4,41 @@ from typing import Any
 | 
			
		||||
from django.db.models import F, Q
 | 
			
		||||
from django.db.models import Value as V
 | 
			
		||||
from django.http.request import HttpRequest
 | 
			
		||||
from rest_framework.request import Request
 | 
			
		||||
from sentry_sdk.hub import Hub
 | 
			
		||||
 | 
			
		||||
from authentik import get_full_version
 | 
			
		||||
from authentik.interfaces.models import Interface, InterfaceType
 | 
			
		||||
from authentik.lib.config import CONFIG
 | 
			
		||||
from authentik.tenants.models import Tenant
 | 
			
		||||
 | 
			
		||||
_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"""
 | 
			
		||||
    db_tenants = (
 | 
			
		||||
        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())
 | 
			
		||||
    if len(tenants) < 1:
 | 
			
		||||
        return DEFAULT_TENANT
 | 
			
		||||
        return get_fallback_tenant()
 | 
			
		||||
    return tenants[0]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def context_processor(request: HttpRequest) -> dict[str, Any]:
 | 
			
		||||
    """Context Processor that injects tenant object into every template"""
 | 
			
		||||
    tenant = getattr(request, "tenant", DEFAULT_TENANT)
 | 
			
		||||
    tenant = getattr(request, "tenant", get_fallback_tenant())
 | 
			
		||||
    trace = ""
 | 
			
		||||
    span = Hub.current.scope.span
 | 
			
		||||
    if span:
 | 
			
		||||
 | 
			
		||||
@ -2,6 +2,11 @@ metadata:
 | 
			
		||||
  name: Default - Tenant
 | 
			
		||||
version: 1
 | 
			
		||||
entries:
 | 
			
		||||
- model: authentik_blueprints.metaapplyblueprint
 | 
			
		||||
  attrs:
 | 
			
		||||
    identifiers:
 | 
			
		||||
      name: System - Interfaces
 | 
			
		||||
    required: false
 | 
			
		||||
- model: authentik_blueprints.metaapplyblueprint
 | 
			
		||||
  attrs:
 | 
			
		||||
    identifiers:
 | 
			
		||||
@ -21,6 +26,9 @@ entries:
 | 
			
		||||
    flow_authentication: !Find [authentik_flows.flow, [slug, default-authentication-flow]]
 | 
			
		||||
    flow_invalidation: !Find [authentik_flows.flow, [slug, default-invalidation-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:
 | 
			
		||||
    domain: authentik-default
 | 
			
		||||
    default: True
 | 
			
		||||
 | 
			
		||||
@ -61,6 +61,7 @@
 | 
			
		||||
                            "authentik_events.notificationwebhookmapping",
 | 
			
		||||
                            "authentik_flows.flow",
 | 
			
		||||
                            "authentik_flows.flowstagebinding",
 | 
			
		||||
                            "authentik_interfaces.interface",
 | 
			
		||||
                            "authentik_outposts.dockerserviceconnection",
 | 
			
		||||
                            "authentik_outposts.kubernetesserviceconnection",
 | 
			
		||||
                            "authentik_outposts.outpost",
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										139
									
								
								blueprints/system/interfaces.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										139
									
								
								blueprints/system/interfaces.yaml
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,139 @@
 | 
			
		||||
version: 1
 | 
			
		||||
metadata:
 | 
			
		||||
  labels:
 | 
			
		||||
    blueprints.goauthentik.io/system: "true"
 | 
			
		||||
  name: System - Interfaces
 | 
			
		||||
entries:
 | 
			
		||||
  - model: authentik_interfaces.interface
 | 
			
		||||
    identifiers:
 | 
			
		||||
      url_name: user
 | 
			
		||||
      type: user
 | 
			
		||||
    attrs:
 | 
			
		||||
      template: |
 | 
			
		||||
        {% extends "base/skeleton.html" %}
 | 
			
		||||
 | 
			
		||||
        {% load static %}
 | 
			
		||||
        {% load i18n %}
 | 
			
		||||
 | 
			
		||||
        {% block head %}
 | 
			
		||||
        <script src="{% static 'dist/user/UserInterface.js' %}?version={{ version }}" type="module"></script>
 | 
			
		||||
        <meta name="theme-color" content="#151515" media="(prefers-color-scheme: light)">
 | 
			
		||||
        <meta name="theme-color" content="#151515" media="(prefers-color-scheme: dark)">
 | 
			
		||||
        <link rel="icon" href="{{ tenant.branding_favicon }}">
 | 
			
		||||
        <link rel="shortcut icon" href="{{ tenant.branding_favicon }}">
 | 
			
		||||
        {% include "base/header_js.html" %}
 | 
			
		||||
        {% endblock %}
 | 
			
		||||
 | 
			
		||||
        {% block body %}
 | 
			
		||||
        <ak-message-container></ak-message-container>
 | 
			
		||||
        <ak-interface-user>
 | 
			
		||||
            <section class="ak-static-page pf-c-page__main-section pf-m-no-padding-mobile pf-m-xl">
 | 
			
		||||
                <div class="pf-c-empty-state" style="height: 100vh;">
 | 
			
		||||
                    <div class="pf-c-empty-state__content">
 | 
			
		||||
                        <span class="pf-c-spinner pf-m-xl pf-c-empty-state__icon" role="progressbar" aria-valuetext="{% trans 'Loading...' %}">
 | 
			
		||||
                            <span class="pf-c-spinner__clipper"></span>
 | 
			
		||||
                            <span class="pf-c-spinner__lead-ball"></span>
 | 
			
		||||
                            <span class="pf-c-spinner__tail-ball"></span>
 | 
			
		||||
                        </span>
 | 
			
		||||
                        <h1 class="pf-c-title pf-m-lg">
 | 
			
		||||
                            {% trans "Loading..." %}
 | 
			
		||||
                        </h1>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
            </section>
 | 
			
		||||
        </ak-interface-user>
 | 
			
		||||
        {% endblock %}
 | 
			
		||||
  - model: authentik_interfaces.interface
 | 
			
		||||
    identifiers:
 | 
			
		||||
      url_name: admin
 | 
			
		||||
      type: admin
 | 
			
		||||
    attrs:
 | 
			
		||||
      template: |
 | 
			
		||||
        {% extends "base/skeleton.html" %}
 | 
			
		||||
 | 
			
		||||
        {% load static %}
 | 
			
		||||
        {% load i18n %}
 | 
			
		||||
 | 
			
		||||
        {% block head %}
 | 
			
		||||
        <script src="{% static 'dist/admin/AdminInterface.js' %}?version={{ version }}" type="module"></script>
 | 
			
		||||
        <meta name="theme-color" content="#18191a" media="(prefers-color-scheme: dark)">
 | 
			
		||||
        <meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)">
 | 
			
		||||
        <link rel="icon" href="{{ tenant.branding_favicon }}">
 | 
			
		||||
        <link rel="shortcut icon" href="{{ tenant.branding_favicon }}">
 | 
			
		||||
        {% include "base/header_js.html" %}
 | 
			
		||||
        {% endblock %}
 | 
			
		||||
 | 
			
		||||
        {% block body %}
 | 
			
		||||
        <ak-message-container></ak-message-container>
 | 
			
		||||
        <ak-interface-admin>
 | 
			
		||||
            <section class="ak-static-page pf-c-page__main-section pf-m-no-padding-mobile pf-m-xl">
 | 
			
		||||
                <div class="pf-c-empty-state" style="height: 100vh;">
 | 
			
		||||
                    <div class="pf-c-empty-state__content">
 | 
			
		||||
                        <span class="pf-c-spinner pf-m-xl pf-c-empty-state__icon" role="progressbar" aria-valuetext="{% trans 'Loading...' %}">
 | 
			
		||||
                            <span class="pf-c-spinner__clipper"></span>
 | 
			
		||||
                            <span class="pf-c-spinner__lead-ball"></span>
 | 
			
		||||
                            <span class="pf-c-spinner__tail-ball"></span>
 | 
			
		||||
                        </span>
 | 
			
		||||
                        <h1 class="pf-c-title pf-m-lg">
 | 
			
		||||
                            {% trans "Loading..." %}
 | 
			
		||||
                        </h1>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
            </section>
 | 
			
		||||
        </ak-interface-admin>
 | 
			
		||||
        {% endblock %}
 | 
			
		||||
  - model: authentik_interfaces.interface
 | 
			
		||||
    identifiers:
 | 
			
		||||
      url_name: flow
 | 
			
		||||
      type: flow
 | 
			
		||||
    attrs:
 | 
			
		||||
      template: |
 | 
			
		||||
        {% extends "base/skeleton.html" %}
 | 
			
		||||
 | 
			
		||||
        {% load static %}
 | 
			
		||||
        {% load i18n %}
 | 
			
		||||
 | 
			
		||||
        {% block head_before %}
 | 
			
		||||
        {{ block.super }}
 | 
			
		||||
        <link rel="prefetch" href="{{ flow.background_url }}" />
 | 
			
		||||
        <link rel="icon" href="{{ tenant.branding_favicon }}">
 | 
			
		||||
        <link rel="shortcut icon" href="{{ tenant.branding_favicon }}">
 | 
			
		||||
        {% if flow.compatibility_mode and not inspector %}
 | 
			
		||||
        <script>ShadyDOM = { force: !navigator.webdriver };</script>
 | 
			
		||||
        {% endif %}
 | 
			
		||||
        {% include "base/header_js.html" %}
 | 
			
		||||
        <script>
 | 
			
		||||
        window.authentik.flow = {
 | 
			
		||||
            "layout": "{{ flow.layout }}",
 | 
			
		||||
        };
 | 
			
		||||
        </script>
 | 
			
		||||
        {% endblock %}
 | 
			
		||||
 | 
			
		||||
        {% block head %}
 | 
			
		||||
        <script src="{% static 'dist/flow/FlowInterface.js' %}?version={{ version }}" type="module"></script>
 | 
			
		||||
        <style>
 | 
			
		||||
        :root {
 | 
			
		||||
            --ak-flow-background: url("{{ flow.background_url }}");
 | 
			
		||||
        }
 | 
			
		||||
        </style>
 | 
			
		||||
        {% endblock %}
 | 
			
		||||
 | 
			
		||||
        {% block body %}
 | 
			
		||||
        <ak-message-container></ak-message-container>
 | 
			
		||||
        <ak-flow-executor>
 | 
			
		||||
            <section class="ak-static-page pf-c-page__main-section pf-m-no-padding-mobile pf-m-xl">
 | 
			
		||||
                <div class="pf-c-empty-state" style="height: 100vh;">
 | 
			
		||||
                    <div class="pf-c-empty-state__content">
 | 
			
		||||
                        <span class="pf-c-spinner pf-m-xl pf-c-empty-state__icon" role="progressbar" aria-valuetext="{% trans 'Loading...' %}">
 | 
			
		||||
                            <span class="pf-c-spinner__clipper"></span>
 | 
			
		||||
                            <span class="pf-c-spinner__lead-ball"></span>
 | 
			
		||||
                            <span class="pf-c-spinner__tail-ball"></span>
 | 
			
		||||
                        </span>
 | 
			
		||||
                        <h1 class="pf-c-title pf-m-lg">
 | 
			
		||||
                            {% trans "Loading..." %}
 | 
			
		||||
                        </h1>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
            </section>
 | 
			
		||||
        </ak-flow-executor>
 | 
			
		||||
        {% endblock %}
 | 
			
		||||
							
								
								
									
										446
									
								
								schema.yml
									
									
									
									
									
								
							
							
						
						
									
										446
									
								
								schema.yml
									
									
									
									
									
								
							@ -3676,6 +3676,24 @@ paths:
 | 
			
		||||
        description: flow_user_settings
 | 
			
		||||
        schema:
 | 
			
		||||
          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
 | 
			
		||||
        required: false
 | 
			
		||||
        in: query
 | 
			
		||||
@ -7731,6 +7749,295 @@ paths:
 | 
			
		||||
              schema:
 | 
			
		||||
                $ref: '#/components/schemas/GenericError'
 | 
			
		||||
          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/:
 | 
			
		||||
    get:
 | 
			
		||||
      operationId: managed_blueprints_list
 | 
			
		||||
@ -26307,6 +26614,7 @@ components:
 | 
			
		||||
      - authentik.admin
 | 
			
		||||
      - authentik.api
 | 
			
		||||
      - authentik.crypto
 | 
			
		||||
      - authentik.interfaces
 | 
			
		||||
      - authentik.events
 | 
			
		||||
      - authentik.flows
 | 
			
		||||
      - authentik.lib
 | 
			
		||||
@ -26357,6 +26665,7 @@ components:
 | 
			
		||||
        * `authentik.admin` - authentik Admin
 | 
			
		||||
        * `authentik.api` - authentik API
 | 
			
		||||
        * `authentik.crypto` - authentik Crypto
 | 
			
		||||
        * `authentik.interfaces` - authentik Interfaces
 | 
			
		||||
        * `authentik.events` - authentik Events
 | 
			
		||||
        * `authentik.flows` - authentik Flows
 | 
			
		||||
        * `authentik.lib` - authentik lib
 | 
			
		||||
@ -29074,6 +29383,7 @@ components:
 | 
			
		||||
            * `authentik.admin` - authentik Admin
 | 
			
		||||
            * `authentik.api` - authentik API
 | 
			
		||||
            * `authentik.crypto` - authentik Crypto
 | 
			
		||||
            * `authentik.interfaces` - authentik Interfaces
 | 
			
		||||
            * `authentik.events` - authentik Events
 | 
			
		||||
            * `authentik.flows` - authentik Flows
 | 
			
		||||
            * `authentik.lib` - authentik lib
 | 
			
		||||
@ -29184,6 +29494,7 @@ components:
 | 
			
		||||
            * `authentik.admin` - authentik Admin
 | 
			
		||||
            * `authentik.api` - authentik API
 | 
			
		||||
            * `authentik.crypto` - authentik Crypto
 | 
			
		||||
            * `authentik.interfaces` - authentik Interfaces
 | 
			
		||||
            * `authentik.events` - authentik Events
 | 
			
		||||
            * `authentik.flows` - authentik Flows
 | 
			
		||||
            * `authentik.lib` - authentik lib
 | 
			
		||||
@ -30297,6 +30608,55 @@ components:
 | 
			
		||||
        * `api` - Intent Api
 | 
			
		||||
        * `recovery` - Intent Recovery
 | 
			
		||||
        * `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:
 | 
			
		||||
      enum:
 | 
			
		||||
      - retry
 | 
			
		||||
@ -33051,6 +33411,41 @@ components:
 | 
			
		||||
      required:
 | 
			
		||||
      - pagination
 | 
			
		||||
      - 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:
 | 
			
		||||
      type: object
 | 
			
		||||
      properties:
 | 
			
		||||
@ -35870,6 +36265,7 @@ components:
 | 
			
		||||
            * `authentik.admin` - authentik Admin
 | 
			
		||||
            * `authentik.api` - authentik API
 | 
			
		||||
            * `authentik.crypto` - authentik Crypto
 | 
			
		||||
            * `authentik.interfaces` - authentik Interfaces
 | 
			
		||||
            * `authentik.events` - authentik Events
 | 
			
		||||
            * `authentik.flows` - authentik Flows
 | 
			
		||||
            * `authentik.lib` - authentik lib
 | 
			
		||||
@ -36121,6 +36517,20 @@ components:
 | 
			
		||||
          description: Specify which sources should be shown.
 | 
			
		||||
        show_source_labels:
 | 
			
		||||
          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:
 | 
			
		||||
      type: object
 | 
			
		||||
      description: Invitation Serializer
 | 
			
		||||
@ -37405,6 +37815,18 @@ components:
 | 
			
		||||
          type: string
 | 
			
		||||
          format: uuid
 | 
			
		||||
          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:
 | 
			
		||||
          type: string
 | 
			
		||||
          minLength: 1
 | 
			
		||||
@ -40576,6 +40998,18 @@ components:
 | 
			
		||||
          type: string
 | 
			
		||||
          format: uuid
 | 
			
		||||
          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:
 | 
			
		||||
          type: string
 | 
			
		||||
          description: 'Events will be deleted after this duration.(Format: weeks=3;days=2;hours=3,seconds=2).'
 | 
			
		||||
@ -40634,6 +41068,18 @@ components:
 | 
			
		||||
          type: string
 | 
			
		||||
          format: uuid
 | 
			
		||||
          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:
 | 
			
		||||
          type: string
 | 
			
		||||
          minLength: 1
 | 
			
		||||
 | 
			
		||||
@ -33,7 +33,7 @@ class TestFlowsAuthenticator(SeleniumTestCase):
 | 
			
		||||
 | 
			
		||||
        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()
 | 
			
		||||
 | 
			
		||||
        # Get expected token
 | 
			
		||||
@ -57,7 +57,7 @@ class TestFlowsAuthenticator(SeleniumTestCase):
 | 
			
		||||
        """test TOTP Setup stage"""
 | 
			
		||||
        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.wait_for_url(self.if_user_url("/library"))
 | 
			
		||||
@ -103,7 +103,7 @@ class TestFlowsAuthenticator(SeleniumTestCase):
 | 
			
		||||
        """test Static OTP Setup stage"""
 | 
			
		||||
        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.wait_for_url(self.if_user_url("/library"))
 | 
			
		||||
 | 
			
		||||
@ -15,7 +15,8 @@ class TestFlowsLogin(SeleniumTestCase):
 | 
			
		||||
        """test default login flow"""
 | 
			
		||||
        self.driver.get(
 | 
			
		||||
            self.url(
 | 
			
		||||
                "authentik_core:if-flow",
 | 
			
		||||
                "authentik_interfaces:if",
 | 
			
		||||
                if_name="flow",
 | 
			
		||||
                flow_slug="default-authentication-flow",
 | 
			
		||||
            )
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
@ -35,7 +35,8 @@ class TestFlowsStageSetup(SeleniumTestCase):
 | 
			
		||||
 | 
			
		||||
        self.driver.get(
 | 
			
		||||
            self.url(
 | 
			
		||||
                "authentik_core:if-flow",
 | 
			
		||||
                "authentik_interfaces:if",
 | 
			
		||||
                if_name="flow",
 | 
			
		||||
                flow_slug="default-authentication-flow",
 | 
			
		||||
            )
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
@ -299,6 +299,9 @@ export class AdminInterface extends Interface {
 | 
			
		||||
                <ak-sidebar-item path="/core/tenants">
 | 
			
		||||
                    <span slot="label">${t`Tenants`}</span>
 | 
			
		||||
                </ak-sidebar-item>
 | 
			
		||||
                <ak-sidebar-item path="/interfaces">
 | 
			
		||||
                    <span slot="label">${t`Interfaces`}</span>
 | 
			
		||||
                </ak-sidebar-item>
 | 
			
		||||
                <ak-sidebar-item path="/crypto/certificates">
 | 
			
		||||
                    <span slot="label">${t`Certificates`}</span>
 | 
			
		||||
                </ak-sidebar-item>
 | 
			
		||||
 | 
			
		||||
@ -132,6 +132,10 @@ export const ROUTES: Route[] = [
 | 
			
		||||
        await import("@goauthentik/admin/blueprints/BlueprintListPage");
 | 
			
		||||
        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 () => {
 | 
			
		||||
        await import("@goauthentik/admin/DebugPage");
 | 
			
		||||
        return html`<ak-admin-debug-page></ak-admin-debug-page>`;
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										90
									
								
								web/src/admin/interfaces/InterfaceForm.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										90
									
								
								web/src/admin/interfaces/InterfaceForm.ts
									
									
									
									
									
										Normal 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>`;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										101
									
								
								web/src/admin/interfaces/InterfaceListPage.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										101
									
								
								web/src/admin/interfaces/InterfaceListPage.ts
									
									
									
									
									
										Normal 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>
 | 
			
		||||
        `;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -23,6 +23,10 @@ import {
 | 
			
		||||
    FlowsApi,
 | 
			
		||||
    FlowsInstancesListDesignationEnum,
 | 
			
		||||
    FlowsInstancesListRequest,
 | 
			
		||||
    Interface,
 | 
			
		||||
    InterfacesApi,
 | 
			
		||||
    InterfacesListRequest,
 | 
			
		||||
    InterfacesListTypeEnum,
 | 
			
		||||
    Tenant,
 | 
			
		||||
} from "@goauthentik/api";
 | 
			
		||||
 | 
			
		||||
@ -368,6 +372,107 @@ export class TenantForm extends ModelForm<Tenant, string> {
 | 
			
		||||
                    </ak-form-element-horizontal>
 | 
			
		||||
                </div>
 | 
			
		||||
            </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>
 | 
			
		||||
                <span slot="header"> ${t`Other global settings`} </span>
 | 
			
		||||
                <div slot="body" class="pf-c-form">
 | 
			
		||||
 | 
			
		||||
		Reference in New Issue
	
	Block a user