Compare commits
	
		
			10 Commits
		
	
	
		
			version/20
			...
			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.lib.utils.reflection import get_env | ||||||
| from authentik.outposts.apps import MANAGED_OUTPOST | from authentik.outposts.apps import MANAGED_OUTPOST | ||||||
| from authentik.outposts.models import Outpost | from authentik.outposts.models import Outpost | ||||||
|  | from authentik.tenants.utils import get_tenant | ||||||
|  |  | ||||||
|  |  | ||||||
| class RuntimeDict(TypedDict): | class RuntimeDict(TypedDict): | ||||||
| @ -77,7 +78,7 @@ class SystemSerializer(PassiveSerializer): | |||||||
|  |  | ||||||
|     def get_tenant(self, request: Request) -> str: |     def get_tenant(self, request: Request) -> str: | ||||||
|         """Currently active tenant""" |         """Currently active tenant""" | ||||||
|         return str(request._request.tenant) |         return str(get_tenant(request)) | ||||||
|  |  | ||||||
|     def get_server_time(self, request: Request) -> datetime: |     def get_server_time(self, request: Request) -> datetime: | ||||||
|         """Current server time""" |         """Current server time""" | ||||||
|  | |||||||
| @ -33,6 +33,7 @@ from authentik.flows.api.flows import FlowViewSet | |||||||
| from authentik.flows.api.stages import StageViewSet | from authentik.flows.api.stages import StageViewSet | ||||||
| from authentik.flows.views.executor import FlowExecutorView | from authentik.flows.views.executor import FlowExecutorView | ||||||
| from authentik.flows.views.inspector import FlowInspectorView | from authentik.flows.views.inspector import FlowInspectorView | ||||||
|  | from authentik.interfaces.api import InterfaceViewSet | ||||||
| from authentik.outposts.api.outposts import OutpostViewSet | from authentik.outposts.api.outposts import OutpostViewSet | ||||||
| from authentik.outposts.api.service_connections import ( | from authentik.outposts.api.service_connections import ( | ||||||
|     DockerServiceConnectionViewSet, |     DockerServiceConnectionViewSet, | ||||||
| @ -123,6 +124,8 @@ router.register("core/user_consent", UserConsentViewSet) | |||||||
| router.register("core/tokens", TokenViewSet) | router.register("core/tokens", TokenViewSet) | ||||||
| router.register("core/tenants", TenantViewSet) | router.register("core/tenants", TenantViewSet) | ||||||
|  |  | ||||||
|  | router.register("interfaces", InterfaceViewSet) | ||||||
|  |  | ||||||
| router.register("outposts/instances", OutpostViewSet) | router.register("outposts/instances", OutpostViewSet) | ||||||
| router.register("outposts/service_connections/all", ServiceConnectionViewSet) | router.register("outposts/service_connections/all", ServiceConnectionViewSet) | ||||||
| router.register("outposts/service_connections/docker", DockerServiceConnectionViewSet) | router.register("outposts/service_connections/docker", DockerServiceConnectionViewSet) | ||||||
|  | |||||||
| @ -10,7 +10,6 @@ from django.db.models.functions import ExtractHour | |||||||
| from django.db.models.query import QuerySet | from django.db.models.query import QuerySet | ||||||
| from django.db.transaction import atomic | from django.db.transaction import atomic | ||||||
| from django.db.utils import IntegrityError | from django.db.utils import IntegrityError | ||||||
| from django.urls import reverse_lazy |  | ||||||
| from django.utils.http import urlencode | from django.utils.http import urlencode | ||||||
| from django.utils.text import slugify | from django.utils.text import slugify | ||||||
| from django.utils.timezone import now | from django.utils.timezone import now | ||||||
| @ -72,10 +71,12 @@ from authentik.flows.exceptions import FlowNonApplicableException | |||||||
| from authentik.flows.models import FlowToken | from authentik.flows.models import FlowToken | ||||||
| from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlanner | from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlanner | ||||||
| from authentik.flows.views.executor import QS_KEY_TOKEN | from authentik.flows.views.executor import QS_KEY_TOKEN | ||||||
|  | from authentik.interfaces.models import InterfaceType | ||||||
|  | from authentik.interfaces.views import reverse_interface | ||||||
| from authentik.stages.email.models import EmailStage | from authentik.stages.email.models import EmailStage | ||||||
| from authentik.stages.email.tasks import send_mails | from authentik.stages.email.tasks import send_mails | ||||||
| from authentik.stages.email.utils import TemplateEmailMessage | from authentik.stages.email.utils import TemplateEmailMessage | ||||||
| from authentik.tenants.models import Tenant | from authentik.tenants.utils import get_tenant | ||||||
|  |  | ||||||
| LOGGER = get_logger() | LOGGER = get_logger() | ||||||
|  |  | ||||||
| @ -321,7 +322,7 @@ class UserViewSet(UsedByMixin, ModelViewSet): | |||||||
|     def _create_recovery_link(self) -> tuple[Optional[str], Optional[Token]]: |     def _create_recovery_link(self) -> tuple[Optional[str], Optional[Token]]: | ||||||
|         """Create a recovery link (when the current tenant has a recovery flow set), |         """Create a recovery link (when the current tenant has a recovery flow set), | ||||||
|         that can either be shown to an admin or sent to the user directly""" |         that can either be shown to an admin or sent to the user directly""" | ||||||
|         tenant: Tenant = self.request._request.tenant |         tenant = get_tenant(self.request) | ||||||
|         # Check that there is a recovery flow, if not return an error |         # Check that there is a recovery flow, if not return an error | ||||||
|         flow = tenant.flow_recovery |         flow = tenant.flow_recovery | ||||||
|         if not flow: |         if not flow: | ||||||
| @ -350,8 +351,12 @@ class UserViewSet(UsedByMixin, ModelViewSet): | |||||||
|         ) |         ) | ||||||
|         querystring = urlencode({QS_KEY_TOKEN: token.key}) |         querystring = urlencode({QS_KEY_TOKEN: token.key}) | ||||||
|         link = self.request.build_absolute_uri( |         link = self.request.build_absolute_uri( | ||||||
|             reverse_lazy("authentik_core:if-flow", kwargs={"flow_slug": flow.slug}) |             reverse_interface( | ||||||
|             + f"?{querystring}" |                 self.request, | ||||||
|  |                 InterfaceType.FLOW, | ||||||
|  |                 flow_slug=flow.slug, | ||||||
|  |             ), | ||||||
|  |             +f"?{querystring}", | ||||||
|         ) |         ) | ||||||
|         return link, token |         return link, token | ||||||
|  |  | ||||||
|  | |||||||
| @ -33,6 +33,7 @@ from authentik.lib.models import ( | |||||||
| ) | ) | ||||||
| from authentik.lib.utils.http import get_client_ip | from authentik.lib.utils.http import get_client_ip | ||||||
| from authentik.policies.models import PolicyBindingModel | from authentik.policies.models import PolicyBindingModel | ||||||
|  | from authentik.tenants.utils import get_tenant | ||||||
|  |  | ||||||
| LOGGER = get_logger() | LOGGER = get_logger() | ||||||
| USER_ATTRIBUTE_DEBUG = "goauthentik.io/user/debug" | USER_ATTRIBUTE_DEBUG = "goauthentik.io/user/debug" | ||||||
| @ -168,7 +169,7 @@ class User(SerializerModel, GuardianUserMixin, AbstractUser): | |||||||
|         including the users attributes""" |         including the users attributes""" | ||||||
|         final_attributes = {} |         final_attributes = {} | ||||||
|         if request and hasattr(request, "tenant"): |         if request and hasattr(request, "tenant"): | ||||||
|             always_merger.merge(final_attributes, request.tenant.attributes) |             always_merger.merge(final_attributes, get_tenant(request).attributes) | ||||||
|         for group in self.ak_groups.all().order_by("name"): |         for group in self.ak_groups.all().order_by("name"): | ||||||
|             always_merger.merge(final_attributes, group.attributes) |             always_merger.merge(final_attributes, group.attributes) | ||||||
|         always_merger.merge(final_attributes, self.attributes) |         always_merger.merge(final_attributes, self.attributes) | ||||||
| @ -227,7 +228,7 @@ class User(SerializerModel, GuardianUserMixin, AbstractUser): | |||||||
|         except Exception as exc: |         except Exception as exc: | ||||||
|             LOGGER.warning("Failed to get default locale", exc=exc) |             LOGGER.warning("Failed to get default locale", exc=exc) | ||||||
|         if request: |         if request: | ||||||
|             return request.tenant.locale |             return get_tenant(request).default_locale | ||||||
|         return "" |         return "" | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|  | |||||||
| @ -25,7 +25,8 @@ from authentik.flows.planner import ( | |||||||
| ) | ) | ||||||
| from authentik.flows.stage import StageView | from authentik.flows.stage import StageView | ||||||
| from authentik.flows.views.executor import NEXT_ARG_NAME, SESSION_KEY_GET, SESSION_KEY_PLAN | from authentik.flows.views.executor import NEXT_ARG_NAME, SESSION_KEY_GET, SESSION_KEY_PLAN | ||||||
| from authentik.lib.utils.urls import redirect_with_qs | from authentik.interfaces.models import InterfaceType | ||||||
|  | from authentik.interfaces.views import redirect_to_default_interface | ||||||
| from authentik.lib.views import bad_request_message | from authentik.lib.views import bad_request_message | ||||||
| from authentik.policies.denied import AccessDeniedResponse | from authentik.policies.denied import AccessDeniedResponse | ||||||
| from authentik.policies.utils import delete_none_keys | from authentik.policies.utils import delete_none_keys | ||||||
| @ -226,7 +227,7 @@ class SourceFlowManager: | |||||||
|         # Ensure redirect is carried through when user was trying to |         # Ensure redirect is carried through when user was trying to | ||||||
|         # authorize application |         # authorize application | ||||||
|         final_redirect = self.request.session.get(SESSION_KEY_GET, {}).get( |         final_redirect = self.request.session.get(SESSION_KEY_GET, {}).get( | ||||||
|             NEXT_ARG_NAME, "authentik_core:if-user" |             NEXT_ARG_NAME, "authentik_core:root-redirect" | ||||||
|         ) |         ) | ||||||
|         kwargs.update( |         kwargs.update( | ||||||
|             { |             { | ||||||
| @ -253,9 +254,9 @@ class SourceFlowManager: | |||||||
|             for stage in stages: |             for stage in stages: | ||||||
|                 plan.append_stage(stage) |                 plan.append_stage(stage) | ||||||
|         self.request.session[SESSION_KEY_PLAN] = plan |         self.request.session[SESSION_KEY_PLAN] = plan | ||||||
|         return redirect_with_qs( |         return redirect_to_default_interface( | ||||||
|             "authentik_core:if-flow", |             self.request, | ||||||
|             self.request.GET, |             InterfaceType.FLOW, | ||||||
|             flow_slug=flow.slug, |             flow_slug=flow.slug, | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
| @ -299,8 +300,9 @@ class SourceFlowManager: | |||||||
|             _("Successfully linked %(source)s!" % {"source": self.source.name}), |             _("Successfully linked %(source)s!" % {"source": self.source.name}), | ||||||
|         ) |         ) | ||||||
|         return redirect( |         return redirect( | ||||||
|  |             # Not ideal that we don't directly redirect to the configured user interface | ||||||
|             reverse( |             reverse( | ||||||
|                 "authentik_core:if-user", |                 "authentik_core:root-redirect", | ||||||
|             ) |             ) | ||||||
|             + f"#/settings;page-{self.source.slug}" |             + f"#/settings;page-{self.source.slug}" | ||||||
|         ) |         ) | ||||||
|  | |||||||
| @ -59,4 +59,6 @@ class TestImpersonation(TestCase): | |||||||
|         self.client.force_login(self.other_user) |         self.client.force_login(self.other_user) | ||||||
|  |  | ||||||
|         response = self.client.get(reverse("authentik_core:impersonate-end")) |         response = self.client.get(reverse("authentik_core:impersonate-end")) | ||||||
|         self.assertRedirects(response, reverse("authentik_core:if-user")) |         self.assertRedirects( | ||||||
|  |             response, reverse("authentik_interfaces:if", kwargs={"if_name": "user"}) | ||||||
|  |         ) | ||||||
|  | |||||||
| @ -3,23 +3,30 @@ from channels.auth import AuthMiddleware | |||||||
| from channels.sessions import CookieMiddleware | from channels.sessions import CookieMiddleware | ||||||
| from django.conf import settings | from django.conf import settings | ||||||
| from django.contrib.auth.decorators import login_required | from django.contrib.auth.decorators import login_required | ||||||
|  | from django.http import HttpRequest, HttpResponse | ||||||
| from django.urls import path | from django.urls import path | ||||||
| from django.views.decorators.csrf import ensure_csrf_cookie | from django.views.decorators.csrf import ensure_csrf_cookie | ||||||
| from django.views.generic import RedirectView |  | ||||||
|  |  | ||||||
| from authentik.core.views import apps, impersonate | from authentik.core.views import apps, impersonate | ||||||
| from authentik.core.views.debug import AccessDeniedView | from authentik.core.views.debug import AccessDeniedView | ||||||
| from authentik.core.views.interface import FlowInterfaceView, InterfaceView |  | ||||||
| from authentik.core.views.session import EndSessionView | from authentik.core.views.session import EndSessionView | ||||||
|  | from authentik.interfaces.models import InterfaceType | ||||||
|  | from authentik.interfaces.views import RedirectToInterface | ||||||
| from authentik.root.asgi_middleware import SessionMiddleware | from authentik.root.asgi_middleware import SessionMiddleware | ||||||
| from authentik.root.messages.consumer import MessageConsumer | from authentik.root.messages.consumer import MessageConsumer | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def placeholder_view(request: HttpRequest, *args, **kwargs) -> HttpResponse: | ||||||
|  |     """Empty view used as placeholder | ||||||
|  |  | ||||||
|  |     (Mounted to websocket endpoints and used by e2e tests)""" | ||||||
|  |     return HttpResponse(status_code=200) | ||||||
|  |  | ||||||
|  |  | ||||||
| urlpatterns = [ | urlpatterns = [ | ||||||
|     path( |     path( | ||||||
|         "", |         "", | ||||||
|         login_required( |         login_required(RedirectToInterface.as_view(type=InterfaceType.USER)), | ||||||
|             RedirectView.as_view(pattern_name="authentik_core:if-user", query_string=True) |  | ||||||
|         ), |  | ||||||
|         name="root-redirect", |         name="root-redirect", | ||||||
|     ), |     ), | ||||||
|     path( |     path( | ||||||
| @ -40,31 +47,16 @@ urlpatterns = [ | |||||||
|         name="impersonate-end", |         name="impersonate-end", | ||||||
|     ), |     ), | ||||||
|     # Interfaces |     # Interfaces | ||||||
|     path( |  | ||||||
|         "if/admin/", |  | ||||||
|         ensure_csrf_cookie(InterfaceView.as_view(template_name="if/admin.html")), |  | ||||||
|         name="if-admin", |  | ||||||
|     ), |  | ||||||
|     path( |  | ||||||
|         "if/user/", |  | ||||||
|         ensure_csrf_cookie(InterfaceView.as_view(template_name="if/user.html")), |  | ||||||
|         name="if-user", |  | ||||||
|     ), |  | ||||||
|     path( |  | ||||||
|         "if/flow/<slug:flow_slug>/", |  | ||||||
|         ensure_csrf_cookie(FlowInterfaceView.as_view()), |  | ||||||
|         name="if-flow", |  | ||||||
|     ), |  | ||||||
|     path( |     path( | ||||||
|         "if/session-end/<slug:application_slug>/", |         "if/session-end/<slug:application_slug>/", | ||||||
|         ensure_csrf_cookie(EndSessionView.as_view()), |         ensure_csrf_cookie(EndSessionView.as_view()), | ||||||
|         name="if-session-end", |         name="if-session-end", | ||||||
|     ), |     ), | ||||||
|     # Fallback for WS |     # Fallback for WS | ||||||
|     path("ws/outpost/<uuid:pk>/", InterfaceView.as_view(template_name="if/admin.html")), |     path("ws/outpost/<uuid:pk>/", placeholder_view), | ||||||
|     path( |     path( | ||||||
|         "ws/client/", |         "ws/client/", | ||||||
|         InterfaceView.as_view(template_name="if/admin.html"), |         placeholder_view, | ||||||
|     ), |     ), | ||||||
| ] | ] | ||||||
|  |  | ||||||
|  | |||||||
| @ -20,11 +20,13 @@ from authentik.flows.views.executor import ( | |||||||
|     SESSION_KEY_PLAN, |     SESSION_KEY_PLAN, | ||||||
|     ToDefaultFlow, |     ToDefaultFlow, | ||||||
| ) | ) | ||||||
| from authentik.lib.utils.urls import redirect_with_qs | from authentik.interfaces.models import InterfaceType | ||||||
|  | from authentik.interfaces.views import redirect_to_default_interface | ||||||
| from authentik.stages.consent.stage import ( | from authentik.stages.consent.stage import ( | ||||||
|     PLAN_CONTEXT_CONSENT_HEADER, |     PLAN_CONTEXT_CONSENT_HEADER, | ||||||
|     PLAN_CONTEXT_CONSENT_PERMISSIONS, |     PLAN_CONTEXT_CONSENT_PERMISSIONS, | ||||||
| ) | ) | ||||||
|  | from authentik.tenants.utils import get_tenant | ||||||
|  |  | ||||||
|  |  | ||||||
| class RedirectToAppLaunch(View): | class RedirectToAppLaunch(View): | ||||||
| @ -59,7 +61,7 @@ class RedirectToAppLaunch(View): | |||||||
|             raise Http404 |             raise Http404 | ||||||
|         plan.insert_stage(in_memory_stage(RedirectToAppStage)) |         plan.insert_stage(in_memory_stage(RedirectToAppStage)) | ||||||
|         request.session[SESSION_KEY_PLAN] = plan |         request.session[SESSION_KEY_PLAN] = plan | ||||||
|         return redirect_with_qs("authentik_core:if-flow", request.GET, flow_slug=flow.slug) |         return redirect_to_default_interface(request, InterfaceType.FLOW, flow_slug=flow.slug) | ||||||
|  |  | ||||||
|  |  | ||||||
| class RedirectToAppStage(ChallengeStageView): | class RedirectToAppStage(ChallengeStageView): | ||||||
|  | |||||||
| @ -35,7 +35,7 @@ class ImpersonateInitView(View): | |||||||
|  |  | ||||||
|         Event.new(EventAction.IMPERSONATION_STARTED).from_http(request, user_to_be) |         Event.new(EventAction.IMPERSONATION_STARTED).from_http(request, user_to_be) | ||||||
|  |  | ||||||
|         return redirect("authentik_core:if-user") |         return redirect("authentik_core:root-redirect") | ||||||
|  |  | ||||||
|  |  | ||||||
| class ImpersonateEndView(View): | class ImpersonateEndView(View): | ||||||
| @ -48,7 +48,7 @@ class ImpersonateEndView(View): | |||||||
|             or SESSION_KEY_IMPERSONATE_ORIGINAL_USER not in request.session |             or SESSION_KEY_IMPERSONATE_ORIGINAL_USER not in request.session | ||||||
|         ): |         ): | ||||||
|             LOGGER.debug("Can't end impersonation", user=request.user) |             LOGGER.debug("Can't end impersonation", user=request.user) | ||||||
|             return redirect("authentik_core:if-user") |             return redirect("authentik_core:root-redirect") | ||||||
|  |  | ||||||
|         original_user = request.session[SESSION_KEY_IMPERSONATE_ORIGINAL_USER] |         original_user = request.session[SESSION_KEY_IMPERSONATE_ORIGINAL_USER] | ||||||
|  |  | ||||||
|  | |||||||
| @ -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.lib.utils.time import timedelta_from_string | ||||||
| from authentik.policies.models import PolicyBindingModel | from authentik.policies.models import PolicyBindingModel | ||||||
| from authentik.stages.email.utils import TemplateEmailMessage | from authentik.stages.email.utils import TemplateEmailMessage | ||||||
| from authentik.tenants.models import Tenant | from authentik.tenants.utils import get_fallback_tenant, get_tenant | ||||||
| from authentik.tenants.utils import DEFAULT_TENANT |  | ||||||
|  |  | ||||||
| LOGGER = get_logger() | LOGGER = get_logger() | ||||||
| if TYPE_CHECKING: | if TYPE_CHECKING: | ||||||
| @ -57,7 +56,7 @@ def default_event_duration(): | |||||||
|  |  | ||||||
| def default_tenant(): | def default_tenant(): | ||||||
|     """Get a default value for tenant""" |     """Get a default value for tenant""" | ||||||
|     return sanitize_dict(model_to_dict(DEFAULT_TENANT)) |     return sanitize_dict(model_to_dict(get_fallback_tenant())) | ||||||
|  |  | ||||||
|  |  | ||||||
| class NotificationTransportError(SentryIgnoredException): | class NotificationTransportError(SentryIgnoredException): | ||||||
| @ -227,7 +226,7 @@ class Event(SerializerModel, ExpiringModel): | |||||||
|                 wrapped = self.context["http_request"]["args"][QS_QUERY] |                 wrapped = self.context["http_request"]["args"][QS_QUERY] | ||||||
|                 self.context["http_request"]["args"] = QueryDict(wrapped) |                 self.context["http_request"]["args"] = QueryDict(wrapped) | ||||||
|         if hasattr(request, "tenant"): |         if hasattr(request, "tenant"): | ||||||
|             tenant: Tenant = request.tenant |             tenant = get_tenant(request) | ||||||
|             # Because self.created only gets set on save, we can't use it's value here |             # Because self.created only gets set on save, we can't use it's value here | ||||||
|             # hence we set self.created to now and then use it |             # hence we set self.created to now and then use it | ||||||
|             self.created = now() |             self.created = now() | ||||||
|  | |||||||
| @ -25,6 +25,8 @@ from authentik.flows.exceptions import FlowNonApplicableException | |||||||
| from authentik.flows.models import Flow | from authentik.flows.models import Flow | ||||||
| from authentik.flows.planner import CACHE_PREFIX, PLAN_CONTEXT_PENDING_USER, FlowPlanner, cache_key | from authentik.flows.planner import CACHE_PREFIX, PLAN_CONTEXT_PENDING_USER, FlowPlanner, cache_key | ||||||
| from authentik.flows.views.executor import SESSION_KEY_HISTORY, SESSION_KEY_PLAN | from authentik.flows.views.executor import SESSION_KEY_HISTORY, SESSION_KEY_PLAN | ||||||
|  | from authentik.interfaces.models import InterfaceType | ||||||
|  | from authentik.interfaces.views import reverse_interface | ||||||
| from authentik.lib.utils.file import ( | from authentik.lib.utils.file import ( | ||||||
|     FilePathSerializer, |     FilePathSerializer, | ||||||
|     FileUploadSerializer, |     FileUploadSerializer, | ||||||
| @ -294,7 +296,11 @@ class FlowViewSet(UsedByMixin, ModelViewSet): | |||||||
|         return Response( |         return Response( | ||||||
|             { |             { | ||||||
|                 "link": request._request.build_absolute_uri( |                 "link": request._request.build_absolute_uri( | ||||||
|                     reverse("authentik_core:if-flow", kwargs={"flow_slug": flow.slug}) |                     reverse_interface( | ||||||
|  |                         request, | ||||||
|  |                         InterfaceType.FLOW, | ||||||
|  |                         flow_slug=flow.slug, | ||||||
|  |                     ), | ||||||
|                 ) |                 ) | ||||||
|             } |             } | ||||||
|         ) |         ) | ||||||
|  | |||||||
| @ -7,6 +7,8 @@ from authentik.core.tests.utils import create_test_flow | |||||||
| from authentik.flows.models import Flow, FlowDesignation | from authentik.flows.models import Flow, FlowDesignation | ||||||
| from authentik.flows.planner import FlowPlan | from authentik.flows.planner import FlowPlan | ||||||
| from authentik.flows.views.executor import SESSION_KEY_APPLICATION_PRE, SESSION_KEY_PLAN | from authentik.flows.views.executor import SESSION_KEY_APPLICATION_PRE, SESSION_KEY_PLAN | ||||||
|  | from authentik.interfaces.models import InterfaceType | ||||||
|  | from authentik.interfaces.tests import reverse_interface | ||||||
| from authentik.lib.generators import generate_id | from authentik.lib.generators import generate_id | ||||||
| from authentik.providers.oauth2.models import OAuth2Provider | from authentik.providers.oauth2.models import OAuth2Provider | ||||||
|  |  | ||||||
| @ -21,7 +23,10 @@ class TestHelperView(TestCase): | |||||||
|         response = self.client.get( |         response = self.client.get( | ||||||
|             reverse("authentik_flows:default-invalidation"), |             reverse("authentik_flows:default-invalidation"), | ||||||
|         ) |         ) | ||||||
|         expected_url = reverse("authentik_core:if-flow", kwargs={"flow_slug": flow.slug}) |         expected_url = reverse_interface( | ||||||
|  |             InterfaceType.FLOW, | ||||||
|  |             flow_slug=flow.slug, | ||||||
|  |         ) | ||||||
|         self.assertEqual(response.status_code, 302) |         self.assertEqual(response.status_code, 302) | ||||||
|         self.assertEqual(response.url, expected_url) |         self.assertEqual(response.url, expected_url) | ||||||
|  |  | ||||||
| @ -72,6 +77,9 @@ class TestHelperView(TestCase): | |||||||
|         response = self.client.get( |         response = self.client.get( | ||||||
|             reverse("authentik_flows:default-invalidation"), |             reverse("authentik_flows:default-invalidation"), | ||||||
|         ) |         ) | ||||||
|         expected_url = reverse("authentik_core:if-flow", kwargs={"flow_slug": flow.slug}) |         expected_url = reverse_interface( | ||||||
|  |             InterfaceType.FLOW, | ||||||
|  |             flow_slug=flow.slug, | ||||||
|  |         ) | ||||||
|         self.assertEqual(response.status_code, 302) |         self.assertEqual(response.status_code, 302) | ||||||
|         self.assertEqual(response.url, expected_url) |         self.assertEqual(response.url, expected_url) | ||||||
|  | |||||||
| @ -53,12 +53,14 @@ from authentik.flows.planner import ( | |||||||
|     FlowPlanner, |     FlowPlanner, | ||||||
| ) | ) | ||||||
| from authentik.flows.stage import AccessDeniedChallengeView, StageView | from authentik.flows.stage import AccessDeniedChallengeView, StageView | ||||||
|  | from authentik.interfaces.models import InterfaceType | ||||||
|  | from authentik.interfaces.views import redirect_to_default_interface | ||||||
| from authentik.lib.sentry import SentryIgnoredException | from authentik.lib.sentry import SentryIgnoredException | ||||||
| from authentik.lib.utils.errors import exception_to_string | from authentik.lib.utils.errors import exception_to_string | ||||||
| from authentik.lib.utils.reflection import all_subclasses, class_to_path | from authentik.lib.utils.reflection import all_subclasses, class_to_path | ||||||
| from authentik.lib.utils.urls import is_url_absolute, redirect_with_qs | from authentik.lib.utils.urls import is_url_absolute, redirect_with_qs | ||||||
| from authentik.policies.engine import PolicyEngine | from authentik.policies.engine import PolicyEngine | ||||||
| from authentik.tenants.models import Tenant | from authentik.tenants.utils import get_tenant | ||||||
|  |  | ||||||
| LOGGER = get_logger() | LOGGER = get_logger() | ||||||
| # Argument used to redirect user after login | # Argument used to redirect user after login | ||||||
| @ -479,7 +481,7 @@ class ToDefaultFlow(View): | |||||||
|  |  | ||||||
|     def get_flow(self) -> Flow: |     def get_flow(self) -> Flow: | ||||||
|         """Get a flow for the selected designation""" |         """Get a flow for the selected designation""" | ||||||
|         tenant: Tenant = self.request.tenant |         tenant = get_tenant(self.request) | ||||||
|         flow = None |         flow = None | ||||||
|         # First, attempt to get default flow from tenant |         # First, attempt to get default flow from tenant | ||||||
|         if self.designation == FlowDesignation.AUTHENTICATION: |         if self.designation == FlowDesignation.AUTHENTICATION: | ||||||
| @ -512,7 +514,7 @@ class ToDefaultFlow(View): | |||||||
|                     flow_slug=flow.slug, |                     flow_slug=flow.slug, | ||||||
|                 ) |                 ) | ||||||
|                 del self.request.session[SESSION_KEY_PLAN] |                 del self.request.session[SESSION_KEY_PLAN] | ||||||
|         return redirect_with_qs("authentik_core:if-flow", request.GET, flow_slug=flow.slug) |         return redirect_to_default_interface(request, InterfaceType.FLOW, flow_slug=flow.slug) | ||||||
|  |  | ||||||
|  |  | ||||||
| def to_stage_response(request: HttpRequest, source: HttpResponse) -> HttpResponse: | def to_stage_response(request: HttpRequest, source: HttpResponse) -> HttpResponse: | ||||||
| @ -583,8 +585,8 @@ class ConfigureFlowInitView(LoginRequiredMixin, View): | |||||||
|             LOGGER.warning("Flow not applicable to user") |             LOGGER.warning("Flow not applicable to user") | ||||||
|             raise Http404 |             raise Http404 | ||||||
|         request.session[SESSION_KEY_PLAN] = plan |         request.session[SESSION_KEY_PLAN] = plan | ||||||
|         return redirect_with_qs( |         return redirect_to_default_interface( | ||||||
|             "authentik_core:if-flow", |             self.request, | ||||||
|             self.request.GET, |             InterfaceType.FLOW, | ||||||
|             flow_slug=stage.configure_flow.slug, |             flow_slug=stage.configure_flow.slug, | ||||||
|         ) |         ) | ||||||
|  | |||||||
							
								
								
									
										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.models import Policy | ||||||
| from authentik.policies.types import PolicyRequest, PolicyResult | from authentik.policies.types import PolicyRequest, PolicyResult | ||||||
| from authentik.stages.prompt.stage import PLAN_CONTEXT_PROMPT | from authentik.stages.prompt.stage import PLAN_CONTEXT_PROMPT | ||||||
|  | from authentik.tenants.utils import get_tenant | ||||||
|  |  | ||||||
| LOGGER = get_logger() | LOGGER = get_logger() | ||||||
| RE_LOWER = re.compile("[a-z]") | RE_LOWER = re.compile("[a-z]") | ||||||
| @ -143,7 +144,8 @@ class PasswordPolicy(Policy): | |||||||
|             user_inputs.append(request.user.name) |             user_inputs.append(request.user.name) | ||||||
|             user_inputs.append(request.user.email) |             user_inputs.append(request.user.email) | ||||||
|         if request.http_request: |         if request.http_request: | ||||||
|             user_inputs.append(request.http_request.tenant.branding_title) |             tenant = get_tenant(request.http_request) | ||||||
|  |             user_inputs.append(tenant.branding_title) | ||||||
|         # Only calculate result for the first 100 characters, as with over 100 char |         # Only calculate result for the first 100 characters, as with over 100 char | ||||||
|         # long passwords we can be reasonably sure that they'll surpass the score anyways |         # long passwords we can be reasonably sure that they'll surpass the score anyways | ||||||
|         # See https://github.com/dropbox/zxcvbn#runtime-latency |         # See https://github.com/dropbox/zxcvbn#runtime-latency | ||||||
|  | |||||||
| @ -39,8 +39,9 @@ class TesOAuth2DeviceInit(OAuthTestCase): | |||||||
|         self.assertEqual( |         self.assertEqual( | ||||||
|             res.url, |             res.url, | ||||||
|             reverse( |             reverse( | ||||||
|                 "authentik_core:if-flow", |                 "authentik_interfaces:if", | ||||||
|                 kwargs={ |                 kwargs={ | ||||||
|  |                     "if_name": "flow", | ||||||
|                     "flow_slug": self.device_flow.slug, |                     "flow_slug": self.device_flow.slug, | ||||||
|                 }, |                 }, | ||||||
|             ), |             ), | ||||||
| @ -68,8 +69,9 @@ class TesOAuth2DeviceInit(OAuthTestCase): | |||||||
|         self.assertEqual( |         self.assertEqual( | ||||||
|             res.url, |             res.url, | ||||||
|             reverse( |             reverse( | ||||||
|                 "authentik_core:if-flow", |                 "authentik_interfaces:if", | ||||||
|                 kwargs={ |                 kwargs={ | ||||||
|  |                     "if_name": "flow", | ||||||
|                     "flow_slug": self.provider.authorization_flow.slug, |                     "flow_slug": self.provider.authorization_flow.slug, | ||||||
|                 }, |                 }, | ||||||
|             ) |             ) | ||||||
|  | |||||||
| @ -29,8 +29,9 @@ from authentik.flows.models import in_memory_stage | |||||||
| from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, PLAN_CONTEXT_SSO, FlowPlanner | from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, PLAN_CONTEXT_SSO, FlowPlanner | ||||||
| from authentik.flows.stage import StageView | from authentik.flows.stage import StageView | ||||||
| from authentik.flows.views.executor import SESSION_KEY_PLAN | from authentik.flows.views.executor import SESSION_KEY_PLAN | ||||||
|  | from authentik.interfaces.models import InterfaceType | ||||||
|  | from authentik.interfaces.views import redirect_to_default_interface | ||||||
| from authentik.lib.utils.time import timedelta_from_string | from authentik.lib.utils.time import timedelta_from_string | ||||||
| from authentik.lib.utils.urls import redirect_with_qs |  | ||||||
| from authentik.lib.views import bad_request_message | from authentik.lib.views import bad_request_message | ||||||
| from authentik.policies.types import PolicyRequest | from authentik.policies.types import PolicyRequest | ||||||
| from authentik.policies.views import PolicyAccessView, RequestValidationError | from authentik.policies.views import PolicyAccessView, RequestValidationError | ||||||
| @ -404,9 +405,9 @@ class AuthorizationFlowInitView(PolicyAccessView): | |||||||
|         plan.append_stage(in_memory_stage(OAuthFulfillmentStage)) |         plan.append_stage(in_memory_stage(OAuthFulfillmentStage)) | ||||||
|  |  | ||||||
|         self.request.session[SESSION_KEY_PLAN] = plan |         self.request.session[SESSION_KEY_PLAN] = plan | ||||||
|         return redirect_with_qs( |         return redirect_to_default_interface( | ||||||
|             "authentik_core:if-flow", |             self.request, | ||||||
|             self.request.GET, |             InterfaceType.FLOW, | ||||||
|             flow_slug=self.provider.authorization_flow.slug, |             flow_slug=self.provider.authorization_flow.slug, | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|  | |||||||
| @ -15,7 +15,8 @@ from authentik.flows.models import in_memory_stage | |||||||
| from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, PLAN_CONTEXT_SSO, FlowPlanner | from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, PLAN_CONTEXT_SSO, FlowPlanner | ||||||
| from authentik.flows.stage import ChallengeStageView | from authentik.flows.stage import ChallengeStageView | ||||||
| from authentik.flows.views.executor import SESSION_KEY_PLAN | from authentik.flows.views.executor import SESSION_KEY_PLAN | ||||||
| from authentik.lib.utils.urls import redirect_with_qs | from authentik.interfaces.models import InterfaceType | ||||||
|  | from authentik.interfaces.views import redirect_to_default_interface | ||||||
| from authentik.providers.oauth2.models import DeviceToken, OAuth2Provider | from authentik.providers.oauth2.models import DeviceToken, OAuth2Provider | ||||||
| from authentik.providers.oauth2.views.device_finish import ( | from authentik.providers.oauth2.views.device_finish import ( | ||||||
|     PLAN_CONTEXT_DEVICE, |     PLAN_CONTEXT_DEVICE, | ||||||
| @ -26,7 +27,7 @@ from authentik.stages.consent.stage import ( | |||||||
|     PLAN_CONTEXT_CONSENT_HEADER, |     PLAN_CONTEXT_CONSENT_HEADER, | ||||||
|     PLAN_CONTEXT_CONSENT_PERMISSIONS, |     PLAN_CONTEXT_CONSENT_PERMISSIONS, | ||||||
| ) | ) | ||||||
| from authentik.tenants.models import Tenant | from authentik.tenants.utils import get_tenant | ||||||
|  |  | ||||||
| LOGGER = get_logger() | LOGGER = get_logger() | ||||||
| QS_KEY_CODE = "code"  # nosec | QS_KEY_CODE = "code"  # nosec | ||||||
| @ -77,9 +78,9 @@ def validate_code(code: int, request: HttpRequest) -> Optional[HttpResponse]: | |||||||
|         return None |         return None | ||||||
|     plan.insert_stage(in_memory_stage(OAuthDeviceCodeFinishStage)) |     plan.insert_stage(in_memory_stage(OAuthDeviceCodeFinishStage)) | ||||||
|     request.session[SESSION_KEY_PLAN] = plan |     request.session[SESSION_KEY_PLAN] = plan | ||||||
|     return redirect_with_qs( |     return redirect_to_default_interface( | ||||||
|         "authentik_core:if-flow", |         request, | ||||||
|         request.GET, |         InterfaceType.FLOW, | ||||||
|         flow_slug=token.provider.authorization_flow.slug, |         flow_slug=token.provider.authorization_flow.slug, | ||||||
|     ) |     ) | ||||||
|  |  | ||||||
| @ -88,7 +89,7 @@ class DeviceEntryView(View): | |||||||
|     """View used to initiate the device-code flow, url entered by endusers""" |     """View used to initiate the device-code flow, url entered by endusers""" | ||||||
|  |  | ||||||
|     def dispatch(self, request: HttpRequest) -> HttpResponse: |     def dispatch(self, request: HttpRequest) -> HttpResponse: | ||||||
|         tenant: Tenant = request.tenant |         tenant = get_tenant(request) | ||||||
|         device_flow = tenant.flow_device_code |         device_flow = tenant.flow_device_code | ||||||
|         if not device_flow: |         if not device_flow: | ||||||
|             LOGGER.info("Tenant has no device code flow configured", tenant=tenant) |             LOGGER.info("Tenant has no device code flow configured", tenant=tenant) | ||||||
| @ -110,9 +111,9 @@ class DeviceEntryView(View): | |||||||
|         plan.append_stage(in_memory_stage(OAuthDeviceCodeStage)) |         plan.append_stage(in_memory_stage(OAuthDeviceCodeStage)) | ||||||
|  |  | ||||||
|         self.request.session[SESSION_KEY_PLAN] = plan |         self.request.session[SESSION_KEY_PLAN] = plan | ||||||
|         return redirect_with_qs( |         return redirect_to_default_interface( | ||||||
|             "authentik_core:if-flow", |             self.request, | ||||||
|             self.request.GET, |             InterfaceType.FLOW, | ||||||
|             flow_slug=device_flow.slug, |             flow_slug=device_flow.slug, | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|  | |||||||
| @ -9,6 +9,7 @@ from django.views.decorators.csrf import csrf_exempt | |||||||
| from authentik.providers.oauth2.constants import SCOPE_GITHUB_ORG_READ, SCOPE_GITHUB_USER_EMAIL | from authentik.providers.oauth2.constants import SCOPE_GITHUB_ORG_READ, SCOPE_GITHUB_USER_EMAIL | ||||||
| from authentik.providers.oauth2.models import RefreshToken | from authentik.providers.oauth2.models import RefreshToken | ||||||
| from authentik.providers.oauth2.utils import protected_resource_view | from authentik.providers.oauth2.utils import protected_resource_view | ||||||
|  | from authentik.tenants.utils import get_tenant | ||||||
|  |  | ||||||
|  |  | ||||||
| @method_decorator(csrf_exempt, name="dispatch") | @method_decorator(csrf_exempt, name="dispatch") | ||||||
| @ -76,6 +77,7 @@ class GitHubUserTeamsView(View): | |||||||
|     def get(self, request: HttpRequest, token: RefreshToken) -> HttpResponse: |     def get(self, request: HttpRequest, token: RefreshToken) -> HttpResponse: | ||||||
|         """Emulate GitHub's /user/teams API Endpoint""" |         """Emulate GitHub's /user/teams API Endpoint""" | ||||||
|         user = token.user |         user = token.user | ||||||
|  |         tenant = get_tenant(request) | ||||||
|  |  | ||||||
|         orgs_response = [] |         orgs_response = [] | ||||||
|         for org in user.ak_groups.all(): |         for org in user.ak_groups.all(): | ||||||
| @ -97,7 +99,7 @@ class GitHubUserTeamsView(View): | |||||||
|                 "created_at": "", |                 "created_at": "", | ||||||
|                 "updated_at": "", |                 "updated_at": "", | ||||||
|                 "organization": { |                 "organization": { | ||||||
|                     "login": slugify(request.tenant.branding_title), |                     "login": slugify(tenant.branding_title), | ||||||
|                     "id": 1, |                     "id": 1, | ||||||
|                     "node_id": "", |                     "node_id": "", | ||||||
|                     "url": "", |                     "url": "", | ||||||
| @ -109,7 +111,7 @@ class GitHubUserTeamsView(View): | |||||||
|                     "public_members_url": "", |                     "public_members_url": "", | ||||||
|                     "avatar_url": "", |                     "avatar_url": "", | ||||||
|                     "description": "", |                     "description": "", | ||||||
|                     "name": request.tenant.branding_title, |                     "name": tenant.branding_title, | ||||||
|                     "company": "", |                     "company": "", | ||||||
|                     "blog": "", |                     "blog": "", | ||||||
|                     "location": "", |                     "location": "", | ||||||
|  | |||||||
| @ -15,7 +15,8 @@ from authentik.flows.exceptions import FlowNonApplicableException | |||||||
| from authentik.flows.models import in_memory_stage | from authentik.flows.models import in_memory_stage | ||||||
| from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, PLAN_CONTEXT_SSO, FlowPlanner | from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, PLAN_CONTEXT_SSO, FlowPlanner | ||||||
| from authentik.flows.views.executor import SESSION_KEY_PLAN, SESSION_KEY_POST | from authentik.flows.views.executor import SESSION_KEY_PLAN, SESSION_KEY_POST | ||||||
| from authentik.lib.utils.urls import redirect_with_qs | from authentik.interfaces.models import InterfaceType | ||||||
|  | from authentik.interfaces.views import redirect_to_default_interface | ||||||
| from authentik.lib.views import bad_request_message | from authentik.lib.views import bad_request_message | ||||||
| from authentik.policies.views import PolicyAccessView | from authentik.policies.views import PolicyAccessView | ||||||
| from authentik.providers.saml.exceptions import CannotHandleAssertion | from authentik.providers.saml.exceptions import CannotHandleAssertion | ||||||
| @ -76,9 +77,9 @@ class SAMLSSOView(PolicyAccessView): | |||||||
|             raise Http404 |             raise Http404 | ||||||
|         plan.append_stage(in_memory_stage(SAMLFlowFinalView)) |         plan.append_stage(in_memory_stage(SAMLFlowFinalView)) | ||||||
|         request.session[SESSION_KEY_PLAN] = plan |         request.session[SESSION_KEY_PLAN] = plan | ||||||
|         return redirect_with_qs( |         return redirect_to_default_interface( | ||||||
|             "authentik_core:if-flow", |             request, | ||||||
|             request.GET, |             InterfaceType.FLOW, | ||||||
|             flow_slug=self.provider.authorization_flow.slug, |             flow_slug=self.provider.authorization_flow.slug, | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|  | |||||||
| @ -22,4 +22,4 @@ class UseTokenView(View): | |||||||
|         login(request, token.user, backend=BACKEND_INBUILT) |         login(request, token.user, backend=BACKEND_INBUILT) | ||||||
|         token.delete() |         token.delete() | ||||||
|         messages.warning(request, _("Used recovery-link to authenticate.")) |         messages.warning(request, _("Used recovery-link to authenticate.")) | ||||||
|         return redirect("authentik_core:if-user") |         return redirect("authentik_core:root-redirect") | ||||||
|  | |||||||
| @ -65,6 +65,7 @@ INSTALLED_APPS = [ | |||||||
|     "authentik.admin", |     "authentik.admin", | ||||||
|     "authentik.api", |     "authentik.api", | ||||||
|     "authentik.crypto", |     "authentik.crypto", | ||||||
|  |     "authentik.interfaces", | ||||||
|     "authentik.events", |     "authentik.events", | ||||||
|     "authentik.flows", |     "authentik.flows", | ||||||
|     "authentik.lib", |     "authentik.lib", | ||||||
|  | |||||||
| @ -32,7 +32,8 @@ from authentik.flows.planner import ( | |||||||
| ) | ) | ||||||
| from authentik.flows.stage import ChallengeStageView | from authentik.flows.stage import ChallengeStageView | ||||||
| from authentik.flows.views.executor import NEXT_ARG_NAME, SESSION_KEY_GET, SESSION_KEY_PLAN | from authentik.flows.views.executor import NEXT_ARG_NAME, SESSION_KEY_GET, SESSION_KEY_PLAN | ||||||
| from authentik.lib.utils.urls import redirect_with_qs | from authentik.interfaces.models import InterfaceType | ||||||
|  | from authentik.interfaces.views import redirect_to_default_interface | ||||||
| from authentik.lib.views import bad_request_message | from authentik.lib.views import bad_request_message | ||||||
| from authentik.providers.saml.utils.encoding import nice64 | from authentik.providers.saml.utils.encoding import nice64 | ||||||
| from authentik.sources.saml.exceptions import MissingSAMLResponse, UnsupportedNameIDFormat | from authentik.sources.saml.exceptions import MissingSAMLResponse, UnsupportedNameIDFormat | ||||||
| @ -72,7 +73,7 @@ class InitiateView(View): | |||||||
|         # Ensure redirect is carried through when user was trying to |         # Ensure redirect is carried through when user was trying to | ||||||
|         # authorize application |         # authorize application | ||||||
|         final_redirect = self.request.session.get(SESSION_KEY_GET, {}).get( |         final_redirect = self.request.session.get(SESSION_KEY_GET, {}).get( | ||||||
|             NEXT_ARG_NAME, "authentik_core:if-user" |             NEXT_ARG_NAME, "authentik_core:root-redirect" | ||||||
|         ) |         ) | ||||||
|         kwargs.update( |         kwargs.update( | ||||||
|             { |             { | ||||||
| @ -91,9 +92,9 @@ class InitiateView(View): | |||||||
|         for stage in stages_to_append: |         for stage in stages_to_append: | ||||||
|             plan.append_stage(stage) |             plan.append_stage(stage) | ||||||
|         self.request.session[SESSION_KEY_PLAN] = plan |         self.request.session[SESSION_KEY_PLAN] = plan | ||||||
|         return redirect_with_qs( |         return redirect_to_default_interface( | ||||||
|             "authentik_core:if-flow", |             self.request, | ||||||
|             self.request.GET, |             InterfaceType.FLOW, | ||||||
|             flow_slug=source.pre_authentication_flow.slug, |             flow_slug=source.pre_authentication_flow.slug, | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|  | |||||||
| @ -17,6 +17,7 @@ from authentik.flows.challenge import ( | |||||||
| from authentik.flows.stage import ChallengeStageView | from authentik.flows.stage import ChallengeStageView | ||||||
| from authentik.stages.authenticator_totp.models import AuthenticatorTOTPStage | from authentik.stages.authenticator_totp.models import AuthenticatorTOTPStage | ||||||
| from authentik.stages.authenticator_totp.settings import OTP_TOTP_ISSUER | from authentik.stages.authenticator_totp.settings import OTP_TOTP_ISSUER | ||||||
|  | from authentik.tenants.utils import get_tenant | ||||||
|  |  | ||||||
| SESSION_TOTP_DEVICE = "totp_device" | SESSION_TOTP_DEVICE = "totp_device" | ||||||
|  |  | ||||||
| @ -57,7 +58,7 @@ class AuthenticatorTOTPStageView(ChallengeStageView): | |||||||
|             data={ |             data={ | ||||||
|                 "type": ChallengeTypes.NATIVE.value, |                 "type": ChallengeTypes.NATIVE.value, | ||||||
|                 "config_url": device.config_url.replace( |                 "config_url": device.config_url.replace( | ||||||
|                     OTP_TOTP_ISSUER, quote(self.request.tenant.branding_title) |                     OTP_TOTP_ISSUER, quote(get_tenant(self.request).branding_title) | ||||||
|                 ), |                 ), | ||||||
|             } |             } | ||||||
|         ) |         ) | ||||||
|  | |||||||
| @ -33,6 +33,7 @@ from authentik.stages.authenticator_validate.models import AuthenticatorValidate | |||||||
| from authentik.stages.authenticator_webauthn.models import UserVerification, WebAuthnDevice | from authentik.stages.authenticator_webauthn.models import UserVerification, WebAuthnDevice | ||||||
| from authentik.stages.authenticator_webauthn.stage import SESSION_KEY_WEBAUTHN_CHALLENGE | from authentik.stages.authenticator_webauthn.stage import SESSION_KEY_WEBAUTHN_CHALLENGE | ||||||
| from authentik.stages.authenticator_webauthn.utils import get_origin, get_rp_id | from authentik.stages.authenticator_webauthn.utils import get_origin, get_rp_id | ||||||
|  | from authentik.tenants.utils import get_tenant | ||||||
|  |  | ||||||
| LOGGER = get_logger() | LOGGER = get_logger() | ||||||
|  |  | ||||||
| @ -187,7 +188,7 @@ def validate_challenge_duo(device_pk: int, stage_view: StageView, user: User) -> | |||||||
|             type=__( |             type=__( | ||||||
|                 "%(brand_name)s Login request" |                 "%(brand_name)s Login request" | ||||||
|                 % { |                 % { | ||||||
|                     "brand_name": stage_view.request.tenant.branding_title, |                     "brand_name": get_tenant(stage_view.request).branding_title, | ||||||
|                 } |                 } | ||||||
|             ), |             ), | ||||||
|             display_username=user.username, |             display_username=user.username, | ||||||
|  | |||||||
| @ -19,7 +19,7 @@ from authentik.stages.authenticator_duo.models import AuthenticatorDuoStage, Duo | |||||||
| from authentik.stages.authenticator_validate.challenge import validate_challenge_duo | from authentik.stages.authenticator_validate.challenge import validate_challenge_duo | ||||||
| from authentik.stages.authenticator_validate.models import AuthenticatorValidateStage, DeviceClasses | from authentik.stages.authenticator_validate.models import AuthenticatorValidateStage, DeviceClasses | ||||||
| from authentik.stages.user_login.models import UserLoginStage | from authentik.stages.user_login.models import UserLoginStage | ||||||
| from authentik.tenants.utils import get_tenant_for_request | from authentik.tenants.utils import lookup_tenant_for_request | ||||||
|  |  | ||||||
|  |  | ||||||
| class AuthenticatorValidateStageDuoTests(FlowTestCase): | class AuthenticatorValidateStageDuoTests(FlowTestCase): | ||||||
| @ -36,7 +36,7 @@ class AuthenticatorValidateStageDuoTests(FlowTestCase): | |||||||
|         middleware = SessionMiddleware(dummy_get_response) |         middleware = SessionMiddleware(dummy_get_response) | ||||||
|         middleware.process_request(request) |         middleware.process_request(request) | ||||||
|         request.session.save() |         request.session.save() | ||||||
|         setattr(request, "tenant", get_tenant_for_request(request)) |         setattr(request, "tenant", lookup_tenant_for_request(request)) | ||||||
|  |  | ||||||
|         stage = AuthenticatorDuoStage.objects.create( |         stage = AuthenticatorDuoStage.objects.create( | ||||||
|             name=generate_id(), |             name=generate_id(), | ||||||
|  | |||||||
| @ -29,6 +29,7 @@ from authentik.flows.challenge import ( | |||||||
| from authentik.flows.stage import ChallengeStageView | from authentik.flows.stage import ChallengeStageView | ||||||
| from authentik.stages.authenticator_webauthn.models import AuthenticateWebAuthnStage, WebAuthnDevice | from authentik.stages.authenticator_webauthn.models import AuthenticateWebAuthnStage, WebAuthnDevice | ||||||
| from authentik.stages.authenticator_webauthn.utils import get_origin, get_rp_id | from authentik.stages.authenticator_webauthn.utils import get_origin, get_rp_id | ||||||
|  | from authentik.tenants.utils import get_tenant | ||||||
|  |  | ||||||
| SESSION_KEY_WEBAUTHN_CHALLENGE = "authentik/stages/authenticator_webauthn/challenge" | SESSION_KEY_WEBAUTHN_CHALLENGE = "authentik/stages/authenticator_webauthn/challenge" | ||||||
|  |  | ||||||
| @ -92,7 +93,7 @@ class AuthenticatorWebAuthnStageView(ChallengeStageView): | |||||||
|  |  | ||||||
|         registration_options: PublicKeyCredentialCreationOptions = generate_registration_options( |         registration_options: PublicKeyCredentialCreationOptions = generate_registration_options( | ||||||
|             rp_id=get_rp_id(self.request), |             rp_id=get_rp_id(self.request), | ||||||
|             rp_name=self.request.tenant.branding_title, |             rp_name=get_tenant(self.request).branding_title, | ||||||
|             user_id=user.uid, |             user_id=user.uid, | ||||||
|             user_name=user.username, |             user_name=user.username, | ||||||
|             user_display_name=user.name, |             user_display_name=user.name, | ||||||
|  | |||||||
| @ -3,7 +3,6 @@ from datetime import timedelta | |||||||
|  |  | ||||||
| from django.contrib import messages | from django.contrib import messages | ||||||
| from django.http import HttpRequest, HttpResponse | from django.http import HttpRequest, HttpResponse | ||||||
| from django.urls import reverse |  | ||||||
| from django.utils.http import urlencode | from django.utils.http import urlencode | ||||||
| from django.utils.text import slugify | from django.utils.text import slugify | ||||||
| from django.utils.timezone import now | from django.utils.timezone import now | ||||||
| @ -16,6 +15,8 @@ from authentik.flows.models import FlowToken | |||||||
| from authentik.flows.planner import PLAN_CONTEXT_IS_RESTORED, PLAN_CONTEXT_PENDING_USER | from authentik.flows.planner import PLAN_CONTEXT_IS_RESTORED, PLAN_CONTEXT_PENDING_USER | ||||||
| from authentik.flows.stage import ChallengeStageView | from authentik.flows.stage import ChallengeStageView | ||||||
| from authentik.flows.views.executor import QS_KEY_TOKEN | from authentik.flows.views.executor import QS_KEY_TOKEN | ||||||
|  | from authentik.interfaces.models import InterfaceType | ||||||
|  | from authentik.interfaces.views import reverse_interface | ||||||
| from authentik.stages.email.models import EmailStage | from authentik.stages.email.models import EmailStage | ||||||
| from authentik.stages.email.tasks import send_mails | from authentik.stages.email.tasks import send_mails | ||||||
| from authentik.stages.email.utils import TemplateEmailMessage | from authentik.stages.email.utils import TemplateEmailMessage | ||||||
| @ -47,9 +48,10 @@ class EmailStageView(ChallengeStageView): | |||||||
|  |  | ||||||
|     def get_full_url(self, **kwargs) -> str: |     def get_full_url(self, **kwargs) -> str: | ||||||
|         """Get full URL to be used in template""" |         """Get full URL to be used in template""" | ||||||
|         base_url = reverse( |         base_url = reverse_interface( | ||||||
|             "authentik_core:if-flow", |             self.request, | ||||||
|             kwargs={"flow_slug": self.executor.flow.slug}, |             InterfaceType.FLOW, | ||||||
|  |             flow_slug=self.executor.flow.slug, | ||||||
|         ) |         ) | ||||||
|         relative_url = f"{base_url}?{urlencode(kwargs)}" |         relative_url = f"{base_url}?{urlencode(kwargs)}" | ||||||
|         return self.request.build_absolute_uri(relative_url) |         return self.request.build_absolute_uri(relative_url) | ||||||
|  | |||||||
| @ -7,6 +7,7 @@ from django.core.mail.backends.locmem import EmailBackend | |||||||
| from django.urls import reverse | from django.urls import reverse | ||||||
| from rest_framework.test import APITestCase | from rest_framework.test import APITestCase | ||||||
|  |  | ||||||
|  | from authentik.blueprints.tests import apply_blueprint | ||||||
| from authentik.core.tests.utils import create_test_admin_user, create_test_flow | from authentik.core.tests.utils import create_test_admin_user, create_test_flow | ||||||
| from authentik.events.models import Event, EventAction | from authentik.events.models import Event, EventAction | ||||||
| from authentik.flows.markers import StageMarker | from authentik.flows.markers import StageMarker | ||||||
| @ -29,6 +30,7 @@ class TestEmailStageSending(APITestCase): | |||||||
|         ) |         ) | ||||||
|         self.binding = FlowStageBinding.objects.create(target=self.flow, stage=self.stage, order=2) |         self.binding = FlowStageBinding.objects.create(target=self.flow, stage=self.stage, order=2) | ||||||
|  |  | ||||||
|  |     @apply_blueprint("system/interfaces.yaml") | ||||||
|     def test_pending_user(self): |     def test_pending_user(self): | ||||||
|         """Test with pending user""" |         """Test with pending user""" | ||||||
|         plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()]) |         plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()]) | ||||||
| @ -54,6 +56,7 @@ class TestEmailStageSending(APITestCase): | |||||||
|             self.assertEqual(event.context["to_email"], [self.user.email]) |             self.assertEqual(event.context["to_email"], [self.user.email]) | ||||||
|             self.assertEqual(event.context["from_email"], "system@authentik.local") |             self.assertEqual(event.context["from_email"], "system@authentik.local") | ||||||
|  |  | ||||||
|  |     @apply_blueprint("system/interfaces.yaml") | ||||||
|     def test_send_error(self): |     def test_send_error(self): | ||||||
|         """Test error during sending (sending will be retried)""" |         """Test error during sending (sending will be retried)""" | ||||||
|         plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()]) |         plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()]) | ||||||
|  | |||||||
| @ -7,6 +7,7 @@ from django.core.mail.backends.smtp import EmailBackend as SMTPEmailBackend | |||||||
| from django.urls import reverse | from django.urls import reverse | ||||||
| from django.utils.http import urlencode | from django.utils.http import urlencode | ||||||
|  |  | ||||||
|  | from authentik.blueprints.tests import apply_blueprint | ||||||
| from authentik.core.tests.utils import create_test_admin_user, create_test_flow | from authentik.core.tests.utils import create_test_admin_user, create_test_flow | ||||||
| from authentik.flows.markers import StageMarker | from authentik.flows.markers import StageMarker | ||||||
| from authentik.flows.models import FlowDesignation, FlowStageBinding, FlowToken | from authentik.flows.models import FlowDesignation, FlowStageBinding, FlowToken | ||||||
| @ -74,6 +75,7 @@ class TestEmailStage(FlowTestCase): | |||||||
|         response = self.client.get(url) |         response = self.client.get(url) | ||||||
|         self.assertEqual(response.status_code, 200) |         self.assertEqual(response.status_code, 200) | ||||||
|  |  | ||||||
|  |     @apply_blueprint("system/interfaces.yaml") | ||||||
|     @patch( |     @patch( | ||||||
|         "authentik.stages.email.models.EmailStage.backend_class", |         "authentik.stages.email.models.EmailStage.backend_class", | ||||||
|         PropertyMock(return_value=EmailBackend), |         PropertyMock(return_value=EmailBackend), | ||||||
| @ -123,6 +125,7 @@ class TestEmailStage(FlowTestCase): | |||||||
|         with self.settings(EMAIL_HOST=host): |         with self.settings(EMAIL_HOST=host): | ||||||
|             self.assertEqual(EmailStage(use_global_settings=True).backend.host, host) |             self.assertEqual(EmailStage(use_global_settings=True).backend.host, host) | ||||||
|  |  | ||||||
|  |     @apply_blueprint("system/interfaces.yaml") | ||||||
|     def test_token(self): |     def test_token(self): | ||||||
|         """Test with token""" |         """Test with token""" | ||||||
|         # Make sure token exists |         # Make sure token exists | ||||||
|  | |||||||
| @ -26,8 +26,9 @@ from authentik.flows.models import FlowDesignation | |||||||
| from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER | from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER | ||||||
| from authentik.flows.stage import PLAN_CONTEXT_PENDING_USER_IDENTIFIER, ChallengeStageView | from authentik.flows.stage import PLAN_CONTEXT_PENDING_USER_IDENTIFIER, ChallengeStageView | ||||||
| from authentik.flows.views.executor import SESSION_KEY_APPLICATION_PRE, SESSION_KEY_GET | from authentik.flows.views.executor import SESSION_KEY_APPLICATION_PRE, SESSION_KEY_GET | ||||||
|  | from authentik.interfaces.models import InterfaceType | ||||||
|  | from authentik.interfaces.views import reverse_interface | ||||||
| from authentik.lib.utils.http import get_client_ip | from authentik.lib.utils.http import get_client_ip | ||||||
| from authentik.lib.utils.urls import reverse_with_qs |  | ||||||
| from authentik.sources.oauth.types.apple import AppleLoginChallenge | from authentik.sources.oauth.types.apple import AppleLoginChallenge | ||||||
| from authentik.sources.plex.models import PlexAuthenticationChallenge | from authentik.sources.plex.models import PlexAuthenticationChallenge | ||||||
| from authentik.stages.identification.models import IdentificationStage | from authentik.stages.identification.models import IdentificationStage | ||||||
| @ -205,22 +206,25 @@ class IdentificationStageView(ChallengeStageView): | |||||||
|         get_qs = self.request.session.get(SESSION_KEY_GET, self.request.GET) |         get_qs = self.request.session.get(SESSION_KEY_GET, self.request.GET) | ||||||
|         # Check for related enrollment and recovery flow, add URL to view |         # Check for related enrollment and recovery flow, add URL to view | ||||||
|         if current_stage.enrollment_flow: |         if current_stage.enrollment_flow: | ||||||
|             challenge.initial_data["enroll_url"] = reverse_with_qs( |             challenge.initial_data["enroll_url"] = reverse_interface( | ||||||
|                 "authentik_core:if-flow", |                 self.request, | ||||||
|  |                 InterfaceType.FLOW, | ||||||
|                 query=get_qs, |                 query=get_qs, | ||||||
|                 kwargs={"flow_slug": current_stage.enrollment_flow.slug}, |                 flow_slug=current_stage.enrollment_flow.slug, | ||||||
|             ) |             ) | ||||||
|         if current_stage.recovery_flow: |         if current_stage.recovery_flow: | ||||||
|             challenge.initial_data["recovery_url"] = reverse_with_qs( |             challenge.initial_data["recovery_url"] = reverse_interface( | ||||||
|                 "authentik_core:if-flow", |                 self.request, | ||||||
|  |                 InterfaceType.FLOW, | ||||||
|                 query=get_qs, |                 query=get_qs, | ||||||
|                 kwargs={"flow_slug": current_stage.recovery_flow.slug}, |                 flow_slug=current_stage.recovery_flow.slug, | ||||||
|             ) |             ) | ||||||
|         if current_stage.passwordless_flow: |         if current_stage.passwordless_flow: | ||||||
|             challenge.initial_data["passwordless_url"] = reverse_with_qs( |             challenge.initial_data["passwordless_url"] = reverse_interface( | ||||||
|                 "authentik_core:if-flow", |                 self.request, | ||||||
|  |                 InterfaceType.FLOW, | ||||||
|                 query=get_qs, |                 query=get_qs, | ||||||
|                 kwargs={"flow_slug": current_stage.passwordless_flow.slug}, |                 flow_slug=current_stage.passwordless_flow.slug, | ||||||
|             ) |             ) | ||||||
|  |  | ||||||
|         # Check all enabled source, add them if they have a UI Login button. |         # Check all enabled source, add them if they have a UI Login button. | ||||||
|  | |||||||
| @ -5,6 +5,8 @@ from authentik.core.tests.utils import create_test_admin_user, create_test_flow | |||||||
| from authentik.flows.challenge import ChallengeTypes | from authentik.flows.challenge import ChallengeTypes | ||||||
| from authentik.flows.models import FlowDesignation, FlowStageBinding | from authentik.flows.models import FlowDesignation, FlowStageBinding | ||||||
| from authentik.flows.tests import FlowTestCase | from authentik.flows.tests import FlowTestCase | ||||||
|  | from authentik.interfaces.models import InterfaceType | ||||||
|  | from authentik.interfaces.tests import reverse_interface | ||||||
| from authentik.sources.oauth.models import OAuthSource | from authentik.sources.oauth.models import OAuthSource | ||||||
| from authentik.stages.identification.models import IdentificationStage, UserFields | from authentik.stages.identification.models import IdentificationStage, UserFields | ||||||
| from authentik.stages.password import BACKEND_INBUILT | from authentik.stages.password import BACKEND_INBUILT | ||||||
| @ -166,9 +168,9 @@ class TestIdentificationStage(FlowTestCase): | |||||||
|             component="ak-stage-identification", |             component="ak-stage-identification", | ||||||
|             user_fields=["email"], |             user_fields=["email"], | ||||||
|             password_fields=False, |             password_fields=False, | ||||||
|             enroll_url=reverse( |             enroll_url=reverse_interface( | ||||||
|                 "authentik_core:if-flow", |                 InterfaceType.FLOW, | ||||||
|                 kwargs={"flow_slug": flow.slug}, |                 flow_slug=flow.slug, | ||||||
|             ), |             ), | ||||||
|             show_source_labels=False, |             show_source_labels=False, | ||||||
|             primary_action="Log in", |             primary_action="Log in", | ||||||
| @ -204,9 +206,9 @@ class TestIdentificationStage(FlowTestCase): | |||||||
|             component="ak-stage-identification", |             component="ak-stage-identification", | ||||||
|             user_fields=["email"], |             user_fields=["email"], | ||||||
|             password_fields=False, |             password_fields=False, | ||||||
|             recovery_url=reverse( |             recovery_url=reverse_interface( | ||||||
|                 "authentik_core:if-flow", |                 InterfaceType.FLOW, | ||||||
|                 kwargs={"flow_slug": flow.slug}, |                 flow_slug=flow.slug, | ||||||
|             ), |             ), | ||||||
|             show_source_labels=False, |             show_source_labels=False, | ||||||
|             primary_action="Log in", |             primary_action="Log in", | ||||||
|  | |||||||
| @ -5,7 +5,6 @@ from django.contrib.auth import _clean_credentials | |||||||
| from django.contrib.auth.backends import BaseBackend | from django.contrib.auth.backends import BaseBackend | ||||||
| from django.core.exceptions import PermissionDenied | from django.core.exceptions import PermissionDenied | ||||||
| from django.http import HttpRequest, HttpResponse | from django.http import HttpRequest, HttpResponse | ||||||
| from django.urls import reverse |  | ||||||
| from django.utils.translation import gettext as _ | from django.utils.translation import gettext as _ | ||||||
| from rest_framework.exceptions import ErrorDetail, ValidationError | from rest_framework.exceptions import ErrorDetail, ValidationError | ||||||
| from rest_framework.fields import CharField | from rest_framework.fields import CharField | ||||||
| @ -23,6 +22,8 @@ from authentik.flows.challenge import ( | |||||||
| from authentik.flows.models import Flow, FlowDesignation, Stage | from authentik.flows.models import Flow, FlowDesignation, Stage | ||||||
| from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER | from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER | ||||||
| from authentik.flows.stage import ChallengeStageView | from authentik.flows.stage import ChallengeStageView | ||||||
|  | from authentik.interfaces.models import InterfaceType | ||||||
|  | from authentik.interfaces.views import reverse_interface | ||||||
| from authentik.lib.utils.reflection import path_to_class | from authentik.lib.utils.reflection import path_to_class | ||||||
| from authentik.stages.password.models import PasswordStage | from authentik.stages.password.models import PasswordStage | ||||||
|  |  | ||||||
| @ -95,11 +96,12 @@ class PasswordStageView(ChallengeStageView): | |||||||
|                 "type": ChallengeTypes.NATIVE.value, |                 "type": ChallengeTypes.NATIVE.value, | ||||||
|             } |             } | ||||||
|         ) |         ) | ||||||
|         recovery_flow = Flow.objects.filter(designation=FlowDesignation.RECOVERY) |         recovery_flow = Flow.objects.filter(designation=FlowDesignation.RECOVERY).first() | ||||||
|         if recovery_flow.exists(): |         if recovery_flow: | ||||||
|             recover_url = reverse( |             recover_url = reverse_interface( | ||||||
|                 "authentik_core:if-flow", |                 self.request, | ||||||
|                 kwargs={"flow_slug": recovery_flow.first().slug}, |                 InterfaceType.FLOW, | ||||||
|  |                 flow_slug=recovery_flow.slug, | ||||||
|             ) |             ) | ||||||
|             challenge.initial_data["recovery_url"] = self.request.build_absolute_uri(recover_url) |             challenge.initial_data["recovery_url"] = self.request.build_absolute_uri(recover_url) | ||||||
|         return challenge |         return challenge | ||||||
|  | |||||||
| @ -18,6 +18,7 @@ from authentik.core.api.used_by import UsedByMixin | |||||||
| from authentik.core.api.utils import PassiveSerializer | from authentik.core.api.utils import PassiveSerializer | ||||||
| from authentik.lib.config import CONFIG | from authentik.lib.config import CONFIG | ||||||
| from authentik.tenants.models import Tenant | from authentik.tenants.models import Tenant | ||||||
|  | from authentik.tenants.utils import get_tenant | ||||||
|  |  | ||||||
|  |  | ||||||
| class FooterLinkSerializer(PassiveSerializer): | class FooterLinkSerializer(PassiveSerializer): | ||||||
| @ -54,6 +55,9 @@ class TenantSerializer(ModelSerializer): | |||||||
|             "flow_unenrollment", |             "flow_unenrollment", | ||||||
|             "flow_user_settings", |             "flow_user_settings", | ||||||
|             "flow_device_code", |             "flow_device_code", | ||||||
|  |             "interface_admin", | ||||||
|  |             "interface_user", | ||||||
|  |             "interface_flow", | ||||||
|             "event_retention", |             "event_retention", | ||||||
|             "web_certificate", |             "web_certificate", | ||||||
|             "attributes", |             "attributes", | ||||||
| @ -120,6 +124,9 @@ class TenantViewSet(UsedByMixin, ModelViewSet): | |||||||
|         "flow_unenrollment", |         "flow_unenrollment", | ||||||
|         "flow_user_settings", |         "flow_user_settings", | ||||||
|         "flow_device_code", |         "flow_device_code", | ||||||
|  |         "interface_admin", | ||||||
|  |         "interface_user", | ||||||
|  |         "interface_flow", | ||||||
|         "event_retention", |         "event_retention", | ||||||
|         "web_certificate", |         "web_certificate", | ||||||
|     ] |     ] | ||||||
| @ -133,5 +140,4 @@ class TenantViewSet(UsedByMixin, ModelViewSet): | |||||||
|     @action(methods=["GET"], detail=False, permission_classes=[AllowAny]) |     @action(methods=["GET"], detail=False, permission_classes=[AllowAny]) | ||||||
|     def current(self, request: Request) -> Response: |     def current(self, request: Request) -> Response: | ||||||
|         """Get current tenant""" |         """Get current tenant""" | ||||||
|         tenant: Tenant = request._request.tenant |         return Response(CurrentTenantSerializer(get_tenant(request)).data) | ||||||
|         return Response(CurrentTenantSerializer(tenant).data) |  | ||||||
|  | |||||||
| @ -6,7 +6,7 @@ from django.http.response import HttpResponse | |||||||
| from django.utils.translation import activate | from django.utils.translation import activate | ||||||
| from sentry_sdk.api import set_tag | from sentry_sdk.api import set_tag | ||||||
|  |  | ||||||
| from authentik.tenants.utils import get_tenant_for_request | from authentik.tenants.utils import lookup_tenant_for_request | ||||||
|  |  | ||||||
|  |  | ||||||
| class TenantMiddleware: | class TenantMiddleware: | ||||||
| @ -19,7 +19,7 @@ class TenantMiddleware: | |||||||
|  |  | ||||||
|     def __call__(self, request: HttpRequest) -> HttpResponse: |     def __call__(self, request: HttpRequest) -> HttpResponse: | ||||||
|         if not hasattr(request, "tenant"): |         if not hasattr(request, "tenant"): | ||||||
|             tenant = get_tenant_for_request(request) |             tenant = lookup_tenant_for_request(request) | ||||||
|             setattr(request, "tenant", tenant) |             setattr(request, "tenant", tenant) | ||||||
|             set_tag("authentik.tenant_uuid", tenant.tenant_uuid.hex) |             set_tag("authentik.tenant_uuid", tenant.tenant_uuid.hex) | ||||||
|             set_tag("authentik.tenant_domain", tenant.domain) |             set_tag("authentik.tenant_domain", tenant.domain) | ||||||
|  | |||||||
| @ -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 structlog.stdlib import get_logger | ||||||
|  |  | ||||||
| from authentik.crypto.models import CertificateKeyPair | from authentik.crypto.models import CertificateKeyPair | ||||||
| from authentik.flows.models import Flow |  | ||||||
| from authentik.lib.models import SerializerModel | from authentik.lib.models import SerializerModel | ||||||
| from authentik.lib.utils.time import timedelta_string_validator | from authentik.lib.utils.time import timedelta_string_validator | ||||||
|  |  | ||||||
| @ -33,22 +32,59 @@ class Tenant(SerializerModel): | |||||||
|     branding_favicon = models.TextField(default="/static/dist/assets/icons/icon.png") |     branding_favicon = models.TextField(default="/static/dist/assets/icons/icon.png") | ||||||
|  |  | ||||||
|     flow_authentication = models.ForeignKey( |     flow_authentication = models.ForeignKey( | ||||||
|         Flow, null=True, on_delete=models.SET_NULL, related_name="tenant_authentication" |         "authentik_flows.Flow", | ||||||
|  |         null=True, | ||||||
|  |         on_delete=models.SET_NULL, | ||||||
|  |         related_name="tenant_authentication", | ||||||
|     ) |     ) | ||||||
|     flow_invalidation = models.ForeignKey( |     flow_invalidation = models.ForeignKey( | ||||||
|         Flow, null=True, on_delete=models.SET_NULL, related_name="tenant_invalidation" |         "authentik_flows.Flow", | ||||||
|  |         null=True, | ||||||
|  |         on_delete=models.SET_NULL, | ||||||
|  |         related_name="tenant_invalidation", | ||||||
|     ) |     ) | ||||||
|     flow_recovery = models.ForeignKey( |     flow_recovery = models.ForeignKey( | ||||||
|         Flow, null=True, on_delete=models.SET_NULL, related_name="tenant_recovery" |         "authentik_flows.Flow", null=True, on_delete=models.SET_NULL, related_name="tenant_recovery" | ||||||
|     ) |     ) | ||||||
|     flow_unenrollment = models.ForeignKey( |     flow_unenrollment = models.ForeignKey( | ||||||
|         Flow, null=True, on_delete=models.SET_NULL, related_name="tenant_unenrollment" |         "authentik_flows.Flow", | ||||||
|  |         null=True, | ||||||
|  |         on_delete=models.SET_NULL, | ||||||
|  |         related_name="tenant_unenrollment", | ||||||
|     ) |     ) | ||||||
|     flow_user_settings = models.ForeignKey( |     flow_user_settings = models.ForeignKey( | ||||||
|         Flow, null=True, on_delete=models.SET_NULL, related_name="tenant_user_settings" |         "authentik_flows.Flow", | ||||||
|  |         null=True, | ||||||
|  |         on_delete=models.SET_NULL, | ||||||
|  |         related_name="tenant_user_settings", | ||||||
|     ) |     ) | ||||||
|     flow_device_code = models.ForeignKey( |     flow_device_code = models.ForeignKey( | ||||||
|         Flow, null=True, on_delete=models.SET_NULL, related_name="tenant_device_code" |         "authentik_flows.Flow", | ||||||
|  |         null=True, | ||||||
|  |         on_delete=models.SET_NULL, | ||||||
|  |         related_name="tenant_device_code", | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     interface_flow = models.ForeignKey( | ||||||
|  |         "authentik_interfaces.Interface", | ||||||
|  |         default=None, | ||||||
|  |         on_delete=models.SET_NULL, | ||||||
|  |         null=True, | ||||||
|  |         related_name="tenant_flow", | ||||||
|  |     ) | ||||||
|  |     interface_user = models.ForeignKey( | ||||||
|  |         "authentik_interfaces.Interface", | ||||||
|  |         default=None, | ||||||
|  |         on_delete=models.SET_NULL, | ||||||
|  |         null=True, | ||||||
|  |         related_name="tenant_user", | ||||||
|  |     ) | ||||||
|  |     interface_admin = models.ForeignKey( | ||||||
|  |         "authentik_interfaces.Interface", | ||||||
|  |         default=None, | ||||||
|  |         on_delete=models.SET_NULL, | ||||||
|  |         null=True, | ||||||
|  |         related_name="tenant_admin", | ||||||
|     ) |     ) | ||||||
|  |  | ||||||
|     event_retention = models.TextField( |     event_retention = models.TextField( | ||||||
|  | |||||||
| @ -75,7 +75,7 @@ class TestTenants(APITestCase): | |||||||
|         ) |         ) | ||||||
|         factory = RequestFactory() |         factory = RequestFactory() | ||||||
|         request = factory.get("/") |         request = factory.get("/") | ||||||
|         request.tenant = tenant |         setattr(request, "tenant", tenant) | ||||||
|         event = Event.new(action=EventAction.SYSTEM_EXCEPTION, message="test").from_http(request) |         event = Event.new(action=EventAction.SYSTEM_EXCEPTION, message="test").from_http(request) | ||||||
|         self.assertEqual(event.expires.day, (event.created + timedelta_from_string("weeks=3")).day) |         self.assertEqual(event.expires.day, (event.created + timedelta_from_string("weeks=3")).day) | ||||||
|         self.assertEqual( |         self.assertEqual( | ||||||
|  | |||||||
| @ -4,17 +4,41 @@ from typing import Any | |||||||
| from django.db.models import F, Q | from django.db.models import F, Q | ||||||
| from django.db.models import Value as V | from django.db.models import Value as V | ||||||
| from django.http.request import HttpRequest | from django.http.request import HttpRequest | ||||||
|  | from rest_framework.request import Request | ||||||
| from sentry_sdk.hub import Hub | from sentry_sdk.hub import Hub | ||||||
|  |  | ||||||
| from authentik import get_full_version | from authentik import get_full_version | ||||||
|  | from authentik.interfaces.models import Interface, InterfaceType | ||||||
| from authentik.lib.config import CONFIG | from authentik.lib.config import CONFIG | ||||||
| from authentik.tenants.models import Tenant | from authentik.tenants.models import Tenant | ||||||
|  |  | ||||||
| _q_default = Q(default=True) | _q_default = Q(default=True) | ||||||
| DEFAULT_TENANT = Tenant(domain="fallback") |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def get_tenant_for_request(request: HttpRequest) -> Tenant: | def get_fallback_tenant(): | ||||||
|  |     """Get fallback tenant""" | ||||||
|  |  | ||||||
|  |     fallback_interface = Interface( | ||||||
|  |         url_name="fallback", | ||||||
|  |         type=InterfaceType.FLOW, | ||||||
|  |         template="Fallback interface", | ||||||
|  |     ) | ||||||
|  |     return Tenant( | ||||||
|  |         domain="fallback", | ||||||
|  |         interface_flow=fallback_interface, | ||||||
|  |         interface_user=fallback_interface, | ||||||
|  |         interface_admin=fallback_interface, | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def get_tenant(request: HttpRequest | Request) -> "Tenant": | ||||||
|  |     """Get the request's tenant, falls back to a fallback tenant object""" | ||||||
|  |     if isinstance(request, Request): | ||||||
|  |         request = request._request | ||||||
|  |     return getattr(request, "tenant", get_fallback_tenant()) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def lookup_tenant_for_request(request: HttpRequest) -> "Tenant": | ||||||
|     """Get tenant object for current request""" |     """Get tenant object for current request""" | ||||||
|     db_tenants = ( |     db_tenants = ( | ||||||
|         Tenant.objects.annotate(host_domain=V(request.get_host())) |         Tenant.objects.annotate(host_domain=V(request.get_host())) | ||||||
| @ -23,13 +47,13 @@ def get_tenant_for_request(request: HttpRequest) -> Tenant: | |||||||
|     ) |     ) | ||||||
|     tenants = list(db_tenants.all()) |     tenants = list(db_tenants.all()) | ||||||
|     if len(tenants) < 1: |     if len(tenants) < 1: | ||||||
|         return DEFAULT_TENANT |         return get_fallback_tenant() | ||||||
|     return tenants[0] |     return tenants[0] | ||||||
|  |  | ||||||
|  |  | ||||||
| def context_processor(request: HttpRequest) -> dict[str, Any]: | def context_processor(request: HttpRequest) -> dict[str, Any]: | ||||||
|     """Context Processor that injects tenant object into every template""" |     """Context Processor that injects tenant object into every template""" | ||||||
|     tenant = getattr(request, "tenant", DEFAULT_TENANT) |     tenant = getattr(request, "tenant", get_fallback_tenant()) | ||||||
|     trace = "" |     trace = "" | ||||||
|     span = Hub.current.scope.span |     span = Hub.current.scope.span | ||||||
|     if span: |     if span: | ||||||
|  | |||||||
| @ -2,6 +2,11 @@ metadata: | |||||||
|   name: Default - Tenant |   name: Default - Tenant | ||||||
| version: 1 | version: 1 | ||||||
| entries: | entries: | ||||||
|  | - model: authentik_blueprints.metaapplyblueprint | ||||||
|  |   attrs: | ||||||
|  |     identifiers: | ||||||
|  |       name: System - Interfaces | ||||||
|  |     required: false | ||||||
| - model: authentik_blueprints.metaapplyblueprint | - model: authentik_blueprints.metaapplyblueprint | ||||||
|   attrs: |   attrs: | ||||||
|     identifiers: |     identifiers: | ||||||
| @ -21,6 +26,9 @@ entries: | |||||||
|     flow_authentication: !Find [authentik_flows.flow, [slug, default-authentication-flow]] |     flow_authentication: !Find [authentik_flows.flow, [slug, default-authentication-flow]] | ||||||
|     flow_invalidation: !Find [authentik_flows.flow, [slug, default-invalidation-flow]] |     flow_invalidation: !Find [authentik_flows.flow, [slug, default-invalidation-flow]] | ||||||
|     flow_user_settings: !Find [authentik_flows.flow, [slug, default-user-settings-flow]] |     flow_user_settings: !Find [authentik_flows.flow, [slug, default-user-settings-flow]] | ||||||
|  |     interface_admin: !Find [authentik_interfaces.Interface, [type, admin]] | ||||||
|  |     interface_user: !Find [authentik_interfaces.Interface, [type, user]] | ||||||
|  |     interface_flow: !Find [authentik_interfaces.Interface, [type, flow]] | ||||||
|   identifiers: |   identifiers: | ||||||
|     domain: authentik-default |     domain: authentik-default | ||||||
|     default: True |     default: True | ||||||
|  | |||||||
| @ -61,6 +61,7 @@ | |||||||
|                             "authentik_events.notificationwebhookmapping", |                             "authentik_events.notificationwebhookmapping", | ||||||
|                             "authentik_flows.flow", |                             "authentik_flows.flow", | ||||||
|                             "authentik_flows.flowstagebinding", |                             "authentik_flows.flowstagebinding", | ||||||
|  |                             "authentik_interfaces.interface", | ||||||
|                             "authentik_outposts.dockerserviceconnection", |                             "authentik_outposts.dockerserviceconnection", | ||||||
|                             "authentik_outposts.kubernetesserviceconnection", |                             "authentik_outposts.kubernetesserviceconnection", | ||||||
|                             "authentik_outposts.outpost", |                             "authentik_outposts.outpost", | ||||||
|  | |||||||
							
								
								
									
										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 |         description: flow_user_settings | ||||||
|         schema: |         schema: | ||||||
|           type: string |           type: string | ||||||
|  |       - name: interface_admin | ||||||
|  |         required: false | ||||||
|  |         in: query | ||||||
|  |         description: interface_admin | ||||||
|  |         schema: | ||||||
|  |           type: string | ||||||
|  |       - name: interface_flow | ||||||
|  |         required: false | ||||||
|  |         in: query | ||||||
|  |         description: interface_flow | ||||||
|  |         schema: | ||||||
|  |           type: string | ||||||
|  |       - name: interface_user | ||||||
|  |         required: false | ||||||
|  |         in: query | ||||||
|  |         description: interface_user | ||||||
|  |         schema: | ||||||
|  |           type: string | ||||||
|       - name: ordering |       - name: ordering | ||||||
|         required: false |         required: false | ||||||
|         in: query |         in: query | ||||||
| @ -7731,6 +7749,295 @@ paths: | |||||||
|               schema: |               schema: | ||||||
|                 $ref: '#/components/schemas/GenericError' |                 $ref: '#/components/schemas/GenericError' | ||||||
|           description: '' |           description: '' | ||||||
|  |   /interfaces/: | ||||||
|  |     get: | ||||||
|  |       operationId: interfaces_list | ||||||
|  |       description: Interface serializer | ||||||
|  |       parameters: | ||||||
|  |       - name: ordering | ||||||
|  |         required: false | ||||||
|  |         in: query | ||||||
|  |         description: Which field to use when ordering the results. | ||||||
|  |         schema: | ||||||
|  |           type: string | ||||||
|  |       - name: page | ||||||
|  |         required: false | ||||||
|  |         in: query | ||||||
|  |         description: A page number within the paginated result set. | ||||||
|  |         schema: | ||||||
|  |           type: integer | ||||||
|  |       - name: page_size | ||||||
|  |         required: false | ||||||
|  |         in: query | ||||||
|  |         description: Number of results to return per page. | ||||||
|  |         schema: | ||||||
|  |           type: integer | ||||||
|  |       - name: search | ||||||
|  |         required: false | ||||||
|  |         in: query | ||||||
|  |         description: A search term. | ||||||
|  |         schema: | ||||||
|  |           type: string | ||||||
|  |       - in: query | ||||||
|  |         name: template | ||||||
|  |         schema: | ||||||
|  |           type: string | ||||||
|  |       - in: query | ||||||
|  |         name: type | ||||||
|  |         schema: | ||||||
|  |           type: string | ||||||
|  |           enum: | ||||||
|  |           - admin | ||||||
|  |           - flow | ||||||
|  |           - user | ||||||
|  |         description: |- | ||||||
|  |           * `user` - User | ||||||
|  |           * `admin` - Admin | ||||||
|  |           * `flow` - Flow | ||||||
|  | 
 | ||||||
|  |           * `user` - User | ||||||
|  |           * `admin` - Admin | ||||||
|  |           * `flow` - Flow | ||||||
|  |       - in: query | ||||||
|  |         name: url_name | ||||||
|  |         schema: | ||||||
|  |           type: string | ||||||
|  |       tags: | ||||||
|  |       - interfaces | ||||||
|  |       security: | ||||||
|  |       - authentik: [] | ||||||
|  |       responses: | ||||||
|  |         '200': | ||||||
|  |           content: | ||||||
|  |             application/json: | ||||||
|  |               schema: | ||||||
|  |                 $ref: '#/components/schemas/PaginatedInterfaceList' | ||||||
|  |           description: '' | ||||||
|  |         '400': | ||||||
|  |           content: | ||||||
|  |             application/json: | ||||||
|  |               schema: | ||||||
|  |                 $ref: '#/components/schemas/ValidationError' | ||||||
|  |           description: '' | ||||||
|  |         '403': | ||||||
|  |           content: | ||||||
|  |             application/json: | ||||||
|  |               schema: | ||||||
|  |                 $ref: '#/components/schemas/GenericError' | ||||||
|  |           description: '' | ||||||
|  |     post: | ||||||
|  |       operationId: interfaces_create | ||||||
|  |       description: Interface serializer | ||||||
|  |       tags: | ||||||
|  |       - interfaces | ||||||
|  |       requestBody: | ||||||
|  |         content: | ||||||
|  |           application/json: | ||||||
|  |             schema: | ||||||
|  |               $ref: '#/components/schemas/InterfaceRequest' | ||||||
|  |         required: true | ||||||
|  |       security: | ||||||
|  |       - authentik: [] | ||||||
|  |       responses: | ||||||
|  |         '201': | ||||||
|  |           content: | ||||||
|  |             application/json: | ||||||
|  |               schema: | ||||||
|  |                 $ref: '#/components/schemas/Interface' | ||||||
|  |           description: '' | ||||||
|  |         '400': | ||||||
|  |           content: | ||||||
|  |             application/json: | ||||||
|  |               schema: | ||||||
|  |                 $ref: '#/components/schemas/ValidationError' | ||||||
|  |           description: '' | ||||||
|  |         '403': | ||||||
|  |           content: | ||||||
|  |             application/json: | ||||||
|  |               schema: | ||||||
|  |                 $ref: '#/components/schemas/GenericError' | ||||||
|  |           description: '' | ||||||
|  |   /interfaces/{interface_uuid}/: | ||||||
|  |     get: | ||||||
|  |       operationId: interfaces_retrieve | ||||||
|  |       description: Interface serializer | ||||||
|  |       parameters: | ||||||
|  |       - in: path | ||||||
|  |         name: interface_uuid | ||||||
|  |         schema: | ||||||
|  |           type: string | ||||||
|  |           format: uuid | ||||||
|  |         description: A UUID string identifying this interface. | ||||||
|  |         required: true | ||||||
|  |       tags: | ||||||
|  |       - interfaces | ||||||
|  |       security: | ||||||
|  |       - authentik: [] | ||||||
|  |       responses: | ||||||
|  |         '200': | ||||||
|  |           content: | ||||||
|  |             application/json: | ||||||
|  |               schema: | ||||||
|  |                 $ref: '#/components/schemas/Interface' | ||||||
|  |           description: '' | ||||||
|  |         '400': | ||||||
|  |           content: | ||||||
|  |             application/json: | ||||||
|  |               schema: | ||||||
|  |                 $ref: '#/components/schemas/ValidationError' | ||||||
|  |           description: '' | ||||||
|  |         '403': | ||||||
|  |           content: | ||||||
|  |             application/json: | ||||||
|  |               schema: | ||||||
|  |                 $ref: '#/components/schemas/GenericError' | ||||||
|  |           description: '' | ||||||
|  |     put: | ||||||
|  |       operationId: interfaces_update | ||||||
|  |       description: Interface serializer | ||||||
|  |       parameters: | ||||||
|  |       - in: path | ||||||
|  |         name: interface_uuid | ||||||
|  |         schema: | ||||||
|  |           type: string | ||||||
|  |           format: uuid | ||||||
|  |         description: A UUID string identifying this interface. | ||||||
|  |         required: true | ||||||
|  |       tags: | ||||||
|  |       - interfaces | ||||||
|  |       requestBody: | ||||||
|  |         content: | ||||||
|  |           application/json: | ||||||
|  |             schema: | ||||||
|  |               $ref: '#/components/schemas/InterfaceRequest' | ||||||
|  |         required: true | ||||||
|  |       security: | ||||||
|  |       - authentik: [] | ||||||
|  |       responses: | ||||||
|  |         '200': | ||||||
|  |           content: | ||||||
|  |             application/json: | ||||||
|  |               schema: | ||||||
|  |                 $ref: '#/components/schemas/Interface' | ||||||
|  |           description: '' | ||||||
|  |         '400': | ||||||
|  |           content: | ||||||
|  |             application/json: | ||||||
|  |               schema: | ||||||
|  |                 $ref: '#/components/schemas/ValidationError' | ||||||
|  |           description: '' | ||||||
|  |         '403': | ||||||
|  |           content: | ||||||
|  |             application/json: | ||||||
|  |               schema: | ||||||
|  |                 $ref: '#/components/schemas/GenericError' | ||||||
|  |           description: '' | ||||||
|  |     patch: | ||||||
|  |       operationId: interfaces_partial_update | ||||||
|  |       description: Interface serializer | ||||||
|  |       parameters: | ||||||
|  |       - in: path | ||||||
|  |         name: interface_uuid | ||||||
|  |         schema: | ||||||
|  |           type: string | ||||||
|  |           format: uuid | ||||||
|  |         description: A UUID string identifying this interface. | ||||||
|  |         required: true | ||||||
|  |       tags: | ||||||
|  |       - interfaces | ||||||
|  |       requestBody: | ||||||
|  |         content: | ||||||
|  |           application/json: | ||||||
|  |             schema: | ||||||
|  |               $ref: '#/components/schemas/PatchedInterfaceRequest' | ||||||
|  |       security: | ||||||
|  |       - authentik: [] | ||||||
|  |       responses: | ||||||
|  |         '200': | ||||||
|  |           content: | ||||||
|  |             application/json: | ||||||
|  |               schema: | ||||||
|  |                 $ref: '#/components/schemas/Interface' | ||||||
|  |           description: '' | ||||||
|  |         '400': | ||||||
|  |           content: | ||||||
|  |             application/json: | ||||||
|  |               schema: | ||||||
|  |                 $ref: '#/components/schemas/ValidationError' | ||||||
|  |           description: '' | ||||||
|  |         '403': | ||||||
|  |           content: | ||||||
|  |             application/json: | ||||||
|  |               schema: | ||||||
|  |                 $ref: '#/components/schemas/GenericError' | ||||||
|  |           description: '' | ||||||
|  |     delete: | ||||||
|  |       operationId: interfaces_destroy | ||||||
|  |       description: Interface serializer | ||||||
|  |       parameters: | ||||||
|  |       - in: path | ||||||
|  |         name: interface_uuid | ||||||
|  |         schema: | ||||||
|  |           type: string | ||||||
|  |           format: uuid | ||||||
|  |         description: A UUID string identifying this interface. | ||||||
|  |         required: true | ||||||
|  |       tags: | ||||||
|  |       - interfaces | ||||||
|  |       security: | ||||||
|  |       - authentik: [] | ||||||
|  |       responses: | ||||||
|  |         '204': | ||||||
|  |           description: No response body | ||||||
|  |         '400': | ||||||
|  |           content: | ||||||
|  |             application/json: | ||||||
|  |               schema: | ||||||
|  |                 $ref: '#/components/schemas/ValidationError' | ||||||
|  |           description: '' | ||||||
|  |         '403': | ||||||
|  |           content: | ||||||
|  |             application/json: | ||||||
|  |               schema: | ||||||
|  |                 $ref: '#/components/schemas/GenericError' | ||||||
|  |           description: '' | ||||||
|  |   /interfaces/{interface_uuid}/used_by/: | ||||||
|  |     get: | ||||||
|  |       operationId: interfaces_used_by_list | ||||||
|  |       description: Get a list of all objects that use this object | ||||||
|  |       parameters: | ||||||
|  |       - in: path | ||||||
|  |         name: interface_uuid | ||||||
|  |         schema: | ||||||
|  |           type: string | ||||||
|  |           format: uuid | ||||||
|  |         description: A UUID string identifying this interface. | ||||||
|  |         required: true | ||||||
|  |       tags: | ||||||
|  |       - interfaces | ||||||
|  |       security: | ||||||
|  |       - authentik: [] | ||||||
|  |       responses: | ||||||
|  |         '200': | ||||||
|  |           content: | ||||||
|  |             application/json: | ||||||
|  |               schema: | ||||||
|  |                 type: array | ||||||
|  |                 items: | ||||||
|  |                   $ref: '#/components/schemas/UsedBy' | ||||||
|  |           description: '' | ||||||
|  |         '400': | ||||||
|  |           content: | ||||||
|  |             application/json: | ||||||
|  |               schema: | ||||||
|  |                 $ref: '#/components/schemas/ValidationError' | ||||||
|  |           description: '' | ||||||
|  |         '403': | ||||||
|  |           content: | ||||||
|  |             application/json: | ||||||
|  |               schema: | ||||||
|  |                 $ref: '#/components/schemas/GenericError' | ||||||
|  |           description: '' | ||||||
|   /managed/blueprints/: |   /managed/blueprints/: | ||||||
|     get: |     get: | ||||||
|       operationId: managed_blueprints_list |       operationId: managed_blueprints_list | ||||||
| @ -26307,6 +26614,7 @@ components: | |||||||
|       - authentik.admin |       - authentik.admin | ||||||
|       - authentik.api |       - authentik.api | ||||||
|       - authentik.crypto |       - authentik.crypto | ||||||
|  |       - authentik.interfaces | ||||||
|       - authentik.events |       - authentik.events | ||||||
|       - authentik.flows |       - authentik.flows | ||||||
|       - authentik.lib |       - authentik.lib | ||||||
| @ -26357,6 +26665,7 @@ components: | |||||||
|         * `authentik.admin` - authentik Admin |         * `authentik.admin` - authentik Admin | ||||||
|         * `authentik.api` - authentik API |         * `authentik.api` - authentik API | ||||||
|         * `authentik.crypto` - authentik Crypto |         * `authentik.crypto` - authentik Crypto | ||||||
|  |         * `authentik.interfaces` - authentik Interfaces | ||||||
|         * `authentik.events` - authentik Events |         * `authentik.events` - authentik Events | ||||||
|         * `authentik.flows` - authentik Flows |         * `authentik.flows` - authentik Flows | ||||||
|         * `authentik.lib` - authentik lib |         * `authentik.lib` - authentik lib | ||||||
| @ -29074,6 +29383,7 @@ components: | |||||||
|             * `authentik.admin` - authentik Admin |             * `authentik.admin` - authentik Admin | ||||||
|             * `authentik.api` - authentik API |             * `authentik.api` - authentik API | ||||||
|             * `authentik.crypto` - authentik Crypto |             * `authentik.crypto` - authentik Crypto | ||||||
|  |             * `authentik.interfaces` - authentik Interfaces | ||||||
|             * `authentik.events` - authentik Events |             * `authentik.events` - authentik Events | ||||||
|             * `authentik.flows` - authentik Flows |             * `authentik.flows` - authentik Flows | ||||||
|             * `authentik.lib` - authentik lib |             * `authentik.lib` - authentik lib | ||||||
| @ -29184,6 +29494,7 @@ components: | |||||||
|             * `authentik.admin` - authentik Admin |             * `authentik.admin` - authentik Admin | ||||||
|             * `authentik.api` - authentik API |             * `authentik.api` - authentik API | ||||||
|             * `authentik.crypto` - authentik Crypto |             * `authentik.crypto` - authentik Crypto | ||||||
|  |             * `authentik.interfaces` - authentik Interfaces | ||||||
|             * `authentik.events` - authentik Events |             * `authentik.events` - authentik Events | ||||||
|             * `authentik.flows` - authentik Flows |             * `authentik.flows` - authentik Flows | ||||||
|             * `authentik.lib` - authentik lib |             * `authentik.lib` - authentik lib | ||||||
| @ -30297,6 +30608,55 @@ components: | |||||||
|         * `api` - Intent Api |         * `api` - Intent Api | ||||||
|         * `recovery` - Intent Recovery |         * `recovery` - Intent Recovery | ||||||
|         * `app_password` - Intent App Password |         * `app_password` - Intent App Password | ||||||
|  |     Interface: | ||||||
|  |       type: object | ||||||
|  |       description: Interface serializer | ||||||
|  |       properties: | ||||||
|  |         interface_uuid: | ||||||
|  |           type: string | ||||||
|  |           format: uuid | ||||||
|  |           readOnly: true | ||||||
|  |         url_name: | ||||||
|  |           type: string | ||||||
|  |           maxLength: 50 | ||||||
|  |           pattern: ^[-a-zA-Z0-9_]+$ | ||||||
|  |         type: | ||||||
|  |           $ref: '#/components/schemas/InterfaceTypeEnum' | ||||||
|  |         template: | ||||||
|  |           type: string | ||||||
|  |       required: | ||||||
|  |       - interface_uuid | ||||||
|  |       - template | ||||||
|  |       - type | ||||||
|  |       - url_name | ||||||
|  |     InterfaceRequest: | ||||||
|  |       type: object | ||||||
|  |       description: Interface serializer | ||||||
|  |       properties: | ||||||
|  |         url_name: | ||||||
|  |           type: string | ||||||
|  |           minLength: 1 | ||||||
|  |           maxLength: 50 | ||||||
|  |           pattern: ^[-a-zA-Z0-9_]+$ | ||||||
|  |         type: | ||||||
|  |           $ref: '#/components/schemas/InterfaceTypeEnum' | ||||||
|  |         template: | ||||||
|  |           type: string | ||||||
|  |           minLength: 1 | ||||||
|  |       required: | ||||||
|  |       - template | ||||||
|  |       - type | ||||||
|  |       - url_name | ||||||
|  |     InterfaceTypeEnum: | ||||||
|  |       enum: | ||||||
|  |       - user | ||||||
|  |       - admin | ||||||
|  |       - flow | ||||||
|  |       type: string | ||||||
|  |       description: |- | ||||||
|  |         * `user` - User | ||||||
|  |         * `admin` - Admin | ||||||
|  |         * `flow` - Flow | ||||||
|     InvalidResponseActionEnum: |     InvalidResponseActionEnum: | ||||||
|       enum: |       enum: | ||||||
|       - retry |       - retry | ||||||
| @ -33051,6 +33411,41 @@ components: | |||||||
|       required: |       required: | ||||||
|       - pagination |       - pagination | ||||||
|       - results |       - results | ||||||
|  |     PaginatedInterfaceList: | ||||||
|  |       type: object | ||||||
|  |       properties: | ||||||
|  |         pagination: | ||||||
|  |           type: object | ||||||
|  |           properties: | ||||||
|  |             next: | ||||||
|  |               type: number | ||||||
|  |             previous: | ||||||
|  |               type: number | ||||||
|  |             count: | ||||||
|  |               type: number | ||||||
|  |             current: | ||||||
|  |               type: number | ||||||
|  |             total_pages: | ||||||
|  |               type: number | ||||||
|  |             start_index: | ||||||
|  |               type: number | ||||||
|  |             end_index: | ||||||
|  |               type: number | ||||||
|  |           required: | ||||||
|  |           - next | ||||||
|  |           - previous | ||||||
|  |           - count | ||||||
|  |           - current | ||||||
|  |           - total_pages | ||||||
|  |           - start_index | ||||||
|  |           - end_index | ||||||
|  |         results: | ||||||
|  |           type: array | ||||||
|  |           items: | ||||||
|  |             $ref: '#/components/schemas/Interface' | ||||||
|  |       required: | ||||||
|  |       - pagination | ||||||
|  |       - results | ||||||
|     PaginatedInvitationList: |     PaginatedInvitationList: | ||||||
|       type: object |       type: object | ||||||
|       properties: |       properties: | ||||||
| @ -35870,6 +36265,7 @@ components: | |||||||
|             * `authentik.admin` - authentik Admin |             * `authentik.admin` - authentik Admin | ||||||
|             * `authentik.api` - authentik API |             * `authentik.api` - authentik API | ||||||
|             * `authentik.crypto` - authentik Crypto |             * `authentik.crypto` - authentik Crypto | ||||||
|  |             * `authentik.interfaces` - authentik Interfaces | ||||||
|             * `authentik.events` - authentik Events |             * `authentik.events` - authentik Events | ||||||
|             * `authentik.flows` - authentik Flows |             * `authentik.flows` - authentik Flows | ||||||
|             * `authentik.lib` - authentik lib |             * `authentik.lib` - authentik lib | ||||||
| @ -36121,6 +36517,20 @@ components: | |||||||
|           description: Specify which sources should be shown. |           description: Specify which sources should be shown. | ||||||
|         show_source_labels: |         show_source_labels: | ||||||
|           type: boolean |           type: boolean | ||||||
|  |     PatchedInterfaceRequest: | ||||||
|  |       type: object | ||||||
|  |       description: Interface serializer | ||||||
|  |       properties: | ||||||
|  |         url_name: | ||||||
|  |           type: string | ||||||
|  |           minLength: 1 | ||||||
|  |           maxLength: 50 | ||||||
|  |           pattern: ^[-a-zA-Z0-9_]+$ | ||||||
|  |         type: | ||||||
|  |           $ref: '#/components/schemas/InterfaceTypeEnum' | ||||||
|  |         template: | ||||||
|  |           type: string | ||||||
|  |           minLength: 1 | ||||||
|     PatchedInvitationRequest: |     PatchedInvitationRequest: | ||||||
|       type: object |       type: object | ||||||
|       description: Invitation Serializer |       description: Invitation Serializer | ||||||
| @ -37405,6 +37815,18 @@ components: | |||||||
|           type: string |           type: string | ||||||
|           format: uuid |           format: uuid | ||||||
|           nullable: true |           nullable: true | ||||||
|  |         interface_admin: | ||||||
|  |           type: string | ||||||
|  |           format: uuid | ||||||
|  |           nullable: true | ||||||
|  |         interface_user: | ||||||
|  |           type: string | ||||||
|  |           format: uuid | ||||||
|  |           nullable: true | ||||||
|  |         interface_flow: | ||||||
|  |           type: string | ||||||
|  |           format: uuid | ||||||
|  |           nullable: true | ||||||
|         event_retention: |         event_retention: | ||||||
|           type: string |           type: string | ||||||
|           minLength: 1 |           minLength: 1 | ||||||
| @ -40576,6 +40998,18 @@ components: | |||||||
|           type: string |           type: string | ||||||
|           format: uuid |           format: uuid | ||||||
|           nullable: true |           nullable: true | ||||||
|  |         interface_admin: | ||||||
|  |           type: string | ||||||
|  |           format: uuid | ||||||
|  |           nullable: true | ||||||
|  |         interface_user: | ||||||
|  |           type: string | ||||||
|  |           format: uuid | ||||||
|  |           nullable: true | ||||||
|  |         interface_flow: | ||||||
|  |           type: string | ||||||
|  |           format: uuid | ||||||
|  |           nullable: true | ||||||
|         event_retention: |         event_retention: | ||||||
|           type: string |           type: string | ||||||
|           description: 'Events will be deleted after this duration.(Format: weeks=3;days=2;hours=3,seconds=2).' |           description: 'Events will be deleted after this duration.(Format: weeks=3;days=2;hours=3,seconds=2).' | ||||||
| @ -40634,6 +41068,18 @@ components: | |||||||
|           type: string |           type: string | ||||||
|           format: uuid |           format: uuid | ||||||
|           nullable: true |           nullable: true | ||||||
|  |         interface_admin: | ||||||
|  |           type: string | ||||||
|  |           format: uuid | ||||||
|  |           nullable: true | ||||||
|  |         interface_user: | ||||||
|  |           type: string | ||||||
|  |           format: uuid | ||||||
|  |           nullable: true | ||||||
|  |         interface_flow: | ||||||
|  |           type: string | ||||||
|  |           format: uuid | ||||||
|  |           nullable: true | ||||||
|         event_retention: |         event_retention: | ||||||
|           type: string |           type: string | ||||||
|           minLength: 1 |           minLength: 1 | ||||||
|  | |||||||
| @ -33,7 +33,7 @@ class TestFlowsAuthenticator(SeleniumTestCase): | |||||||
|  |  | ||||||
|         flow: Flow = Flow.objects.get(slug="default-authentication-flow") |         flow: Flow = Flow.objects.get(slug="default-authentication-flow") | ||||||
|  |  | ||||||
|         self.driver.get(self.url("authentik_core:if-flow", flow_slug=flow.slug)) |         self.driver.get(self.url("authentik_interfaces:if", if_name="flow", flow_slug=flow.slug)) | ||||||
|         self.login() |         self.login() | ||||||
|  |  | ||||||
|         # Get expected token |         # Get expected token | ||||||
| @ -57,7 +57,7 @@ class TestFlowsAuthenticator(SeleniumTestCase): | |||||||
|         """test TOTP Setup stage""" |         """test TOTP Setup stage""" | ||||||
|         flow: Flow = Flow.objects.get(slug="default-authentication-flow") |         flow: Flow = Flow.objects.get(slug="default-authentication-flow") | ||||||
|  |  | ||||||
|         self.driver.get(self.url("authentik_core:if-flow", flow_slug=flow.slug)) |         self.driver.get(self.url("authentik_interfaces:if", if_name="flow", flow_slug=flow.slug)) | ||||||
|         self.login() |         self.login() | ||||||
|  |  | ||||||
|         self.wait_for_url(self.if_user_url("/library")) |         self.wait_for_url(self.if_user_url("/library")) | ||||||
| @ -103,7 +103,7 @@ class TestFlowsAuthenticator(SeleniumTestCase): | |||||||
|         """test Static OTP Setup stage""" |         """test Static OTP Setup stage""" | ||||||
|         flow: Flow = Flow.objects.get(slug="default-authentication-flow") |         flow: Flow = Flow.objects.get(slug="default-authentication-flow") | ||||||
|  |  | ||||||
|         self.driver.get(self.url("authentik_core:if-flow", flow_slug=flow.slug)) |         self.driver.get(self.url("authentik_interfaces:if", if_name="flow", flow_slug=flow.slug)) | ||||||
|         self.login() |         self.login() | ||||||
|  |  | ||||||
|         self.wait_for_url(self.if_user_url("/library")) |         self.wait_for_url(self.if_user_url("/library")) | ||||||
|  | |||||||
| @ -15,7 +15,8 @@ class TestFlowsLogin(SeleniumTestCase): | |||||||
|         """test default login flow""" |         """test default login flow""" | ||||||
|         self.driver.get( |         self.driver.get( | ||||||
|             self.url( |             self.url( | ||||||
|                 "authentik_core:if-flow", |                 "authentik_interfaces:if", | ||||||
|  |                 if_name="flow", | ||||||
|                 flow_slug="default-authentication-flow", |                 flow_slug="default-authentication-flow", | ||||||
|             ) |             ) | ||||||
|         ) |         ) | ||||||
|  | |||||||
| @ -35,7 +35,8 @@ class TestFlowsStageSetup(SeleniumTestCase): | |||||||
|  |  | ||||||
|         self.driver.get( |         self.driver.get( | ||||||
|             self.url( |             self.url( | ||||||
|                 "authentik_core:if-flow", |                 "authentik_interfaces:if", | ||||||
|  |                 if_name="flow", | ||||||
|                 flow_slug="default-authentication-flow", |                 flow_slug="default-authentication-flow", | ||||||
|             ) |             ) | ||||||
|         ) |         ) | ||||||
|  | |||||||
| @ -299,6 +299,9 @@ export class AdminInterface extends Interface { | |||||||
|                 <ak-sidebar-item path="/core/tenants"> |                 <ak-sidebar-item path="/core/tenants"> | ||||||
|                     <span slot="label">${t`Tenants`}</span> |                     <span slot="label">${t`Tenants`}</span> | ||||||
|                 </ak-sidebar-item> |                 </ak-sidebar-item> | ||||||
|  |                 <ak-sidebar-item path="/interfaces"> | ||||||
|  |                     <span slot="label">${t`Interfaces`}</span> | ||||||
|  |                 </ak-sidebar-item> | ||||||
|                 <ak-sidebar-item path="/crypto/certificates"> |                 <ak-sidebar-item path="/crypto/certificates"> | ||||||
|                     <span slot="label">${t`Certificates`}</span> |                     <span slot="label">${t`Certificates`}</span> | ||||||
|                 </ak-sidebar-item> |                 </ak-sidebar-item> | ||||||
|  | |||||||
| @ -132,6 +132,10 @@ export const ROUTES: Route[] = [ | |||||||
|         await import("@goauthentik/admin/blueprints/BlueprintListPage"); |         await import("@goauthentik/admin/blueprints/BlueprintListPage"); | ||||||
|         return html`<ak-blueprint-list></ak-blueprint-list>`; |         return html`<ak-blueprint-list></ak-blueprint-list>`; | ||||||
|     }), |     }), | ||||||
|  |     new Route(new RegExp("^/interfaces$"), async () => { | ||||||
|  |         await import("@goauthentik/admin/interfaces/InterfaceListPage"); | ||||||
|  |         return html`<ak-interface-list></ak-interface-list>`; | ||||||
|  |     }), | ||||||
|     new Route(new RegExp("^/debug$"), async () => { |     new Route(new RegExp("^/debug$"), async () => { | ||||||
|         await import("@goauthentik/admin/DebugPage"); |         await import("@goauthentik/admin/DebugPage"); | ||||||
|         return html`<ak-admin-debug-page></ak-admin-debug-page>`; |         return html`<ak-admin-debug-page></ak-admin-debug-page>`; | ||||||
|  | |||||||
							
								
								
									
										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, |     FlowsApi, | ||||||
|     FlowsInstancesListDesignationEnum, |     FlowsInstancesListDesignationEnum, | ||||||
|     FlowsInstancesListRequest, |     FlowsInstancesListRequest, | ||||||
|  |     Interface, | ||||||
|  |     InterfacesApi, | ||||||
|  |     InterfacesListRequest, | ||||||
|  |     InterfacesListTypeEnum, | ||||||
|     Tenant, |     Tenant, | ||||||
| } from "@goauthentik/api"; | } from "@goauthentik/api"; | ||||||
|  |  | ||||||
| @ -368,6 +372,107 @@ export class TenantForm extends ModelForm<Tenant, string> { | |||||||
|                     </ak-form-element-horizontal> |                     </ak-form-element-horizontal> | ||||||
|                 </div> |                 </div> | ||||||
|             </ak-form-group> |             </ak-form-group> | ||||||
|  |             <ak-form-group> | ||||||
|  |                 <span slot="header"> ${t`Interfaces`} </span> | ||||||
|  |                 <div slot="body" class="pf-c-form"> | ||||||
|  |                     <ak-form-element-horizontal label=${t`User Interface`} name="interfaceUser"> | ||||||
|  |                         <ak-search-select | ||||||
|  |                             .fetchObjects=${async (query?: string): Promise<Interface[]> => { | ||||||
|  |                                 const args: InterfacesListRequest = { | ||||||
|  |                                     ordering: "url_name", | ||||||
|  |                                     type: InterfacesListTypeEnum.User, | ||||||
|  |                                 }; | ||||||
|  |                                 if (query !== undefined) { | ||||||
|  |                                     args.search = query; | ||||||
|  |                                 } | ||||||
|  |                                 const flows = await new InterfacesApi( | ||||||
|  |                                     DEFAULT_CONFIG, | ||||||
|  |                                 ).interfacesList(args); | ||||||
|  |                                 return flows.results; | ||||||
|  |                             }} | ||||||
|  |                             .renderElement=${(iface: Interface): string => { | ||||||
|  |                                 return iface.urlName; | ||||||
|  |                             }} | ||||||
|  |                             .renderDescription=${(iface: Interface): TemplateResult => { | ||||||
|  |                                 return html`${iface.type}`; | ||||||
|  |                             }} | ||||||
|  |                             .value=${(iface: Interface | undefined): string | undefined => { | ||||||
|  |                                 return iface?.interfaceUuid; | ||||||
|  |                             }} | ||||||
|  |                             .selected=${(iface: Interface): boolean => { | ||||||
|  |                                 return this.instance?.interfaceUser === iface.interfaceUuid; | ||||||
|  |                             }} | ||||||
|  |                             ?blankable=${true} | ||||||
|  |                         > | ||||||
|  |                         </ak-search-select> | ||||||
|  |                         <p class="pf-c-form__helper-text">${t`.`}</p> | ||||||
|  |                     </ak-form-element-horizontal> | ||||||
|  |                     <ak-form-element-horizontal label=${t`Flow Interface`} name="interfaceFlow"> | ||||||
|  |                         <ak-search-select | ||||||
|  |                             .fetchObjects=${async (query?: string): Promise<Interface[]> => { | ||||||
|  |                                 const args: InterfacesListRequest = { | ||||||
|  |                                     ordering: "url_name", | ||||||
|  |                                     type: InterfacesListTypeEnum.Flow, | ||||||
|  |                                 }; | ||||||
|  |                                 if (query !== undefined) { | ||||||
|  |                                     args.search = query; | ||||||
|  |                                 } | ||||||
|  |                                 const flows = await new InterfacesApi( | ||||||
|  |                                     DEFAULT_CONFIG, | ||||||
|  |                                 ).interfacesList(args); | ||||||
|  |                                 return flows.results; | ||||||
|  |                             }} | ||||||
|  |                             .renderElement=${(iface: Interface): string => { | ||||||
|  |                                 return iface.urlName; | ||||||
|  |                             }} | ||||||
|  |                             .renderDescription=${(iface: Interface): TemplateResult => { | ||||||
|  |                                 return html`${iface.type}`; | ||||||
|  |                             }} | ||||||
|  |                             .value=${(iface: Interface | undefined): string | undefined => { | ||||||
|  |                                 return iface?.interfaceUuid; | ||||||
|  |                             }} | ||||||
|  |                             .selected=${(iface: Interface): boolean => { | ||||||
|  |                                 return this.instance?.interfaceFlow === iface.interfaceUuid; | ||||||
|  |                             }} | ||||||
|  |                             ?blankable=${true} | ||||||
|  |                         > | ||||||
|  |                         </ak-search-select> | ||||||
|  |                         <p class="pf-c-form__helper-text">${t`.`}</p> | ||||||
|  |                     </ak-form-element-horizontal> | ||||||
|  |                     <ak-form-element-horizontal label=${t`Admin Interface`} name="interfaceAdmin"> | ||||||
|  |                         <ak-search-select | ||||||
|  |                             .fetchObjects=${async (query?: string): Promise<Interface[]> => { | ||||||
|  |                                 const args: InterfacesListRequest = { | ||||||
|  |                                     ordering: "url_name", | ||||||
|  |                                     type: InterfacesListTypeEnum.Admin, | ||||||
|  |                                 }; | ||||||
|  |                                 if (query !== undefined) { | ||||||
|  |                                     args.search = query; | ||||||
|  |                                 } | ||||||
|  |                                 const flows = await new InterfacesApi( | ||||||
|  |                                     DEFAULT_CONFIG, | ||||||
|  |                                 ).interfacesList(args); | ||||||
|  |                                 return flows.results; | ||||||
|  |                             }} | ||||||
|  |                             .renderElement=${(iface: Interface): string => { | ||||||
|  |                                 return iface.urlName; | ||||||
|  |                             }} | ||||||
|  |                             .renderDescription=${(iface: Interface): TemplateResult => { | ||||||
|  |                                 return html`${iface.type}`; | ||||||
|  |                             }} | ||||||
|  |                             .value=${(iface: Interface | undefined): string | undefined => { | ||||||
|  |                                 return iface?.interfaceUuid; | ||||||
|  |                             }} | ||||||
|  |                             .selected=${(iface: Interface): boolean => { | ||||||
|  |                                 return this.instance?.interfaceAdmin === iface.interfaceUuid; | ||||||
|  |                             }} | ||||||
|  |                             ?blankable=${true} | ||||||
|  |                         > | ||||||
|  |                         </ak-search-select> | ||||||
|  |                         <p class="pf-c-form__helper-text">${t`.`}</p> | ||||||
|  |                     </ak-form-element-horizontal> | ||||||
|  |                 </div> | ||||||
|  |             </ak-form-group> | ||||||
|             <ak-form-group> |             <ak-form-group> | ||||||
|                 <span slot="header"> ${t`Other global settings`} </span> |                 <span slot="header"> ${t`Other global settings`} </span> | ||||||
|                 <div slot="body" class="pf-c-form"> |                 <div slot="body" class="pf-c-form"> | ||||||
|  | |||||||
		Reference in New Issue
	
	Block a user
	