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.outposts.apps import MANAGED_OUTPOST | ||||
| from authentik.outposts.models import Outpost | ||||
| from authentik.tenants.utils import get_tenant | ||||
|  | ||||
|  | ||||
| class RuntimeDict(TypedDict): | ||||
| @ -77,7 +78,7 @@ class SystemSerializer(PassiveSerializer): | ||||
|  | ||||
|     def get_tenant(self, request: Request) -> str: | ||||
|         """Currently active tenant""" | ||||
|         return str(request._request.tenant) | ||||
|         return str(get_tenant(request)) | ||||
|  | ||||
|     def get_server_time(self, request: Request) -> datetime: | ||||
|         """Current server time""" | ||||
|  | ||||
| @ -33,6 +33,7 @@ from authentik.flows.api.flows import FlowViewSet | ||||
| from authentik.flows.api.stages import StageViewSet | ||||
| from authentik.flows.views.executor import FlowExecutorView | ||||
| from authentik.flows.views.inspector import FlowInspectorView | ||||
| from authentik.interfaces.api import InterfaceViewSet | ||||
| from authentik.outposts.api.outposts import OutpostViewSet | ||||
| from authentik.outposts.api.service_connections import ( | ||||
|     DockerServiceConnectionViewSet, | ||||
| @ -123,6 +124,8 @@ router.register("core/user_consent", UserConsentViewSet) | ||||
| router.register("core/tokens", TokenViewSet) | ||||
| router.register("core/tenants", TenantViewSet) | ||||
|  | ||||
| router.register("interfaces", InterfaceViewSet) | ||||
|  | ||||
| router.register("outposts/instances", OutpostViewSet) | ||||
| router.register("outposts/service_connections/all", ServiceConnectionViewSet) | ||||
| router.register("outposts/service_connections/docker", DockerServiceConnectionViewSet) | ||||
|  | ||||
| @ -10,7 +10,6 @@ from django.db.models.functions import ExtractHour | ||||
| from django.db.models.query import QuerySet | ||||
| from django.db.transaction import atomic | ||||
| from django.db.utils import IntegrityError | ||||
| from django.urls import reverse_lazy | ||||
| from django.utils.http import urlencode | ||||
| from django.utils.text import slugify | ||||
| from django.utils.timezone import now | ||||
| @ -72,10 +71,12 @@ from authentik.flows.exceptions import FlowNonApplicableException | ||||
| from authentik.flows.models import FlowToken | ||||
| from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlanner | ||||
| from authentik.flows.views.executor import QS_KEY_TOKEN | ||||
| from authentik.interfaces.models import InterfaceType | ||||
| from authentik.interfaces.views import reverse_interface | ||||
| from authentik.stages.email.models import EmailStage | ||||
| from authentik.stages.email.tasks import send_mails | ||||
| from authentik.stages.email.utils import TemplateEmailMessage | ||||
| from authentik.tenants.models import Tenant | ||||
| from authentik.tenants.utils import get_tenant | ||||
|  | ||||
| LOGGER = get_logger() | ||||
|  | ||||
| @ -321,7 +322,7 @@ class UserViewSet(UsedByMixin, ModelViewSet): | ||||
|     def _create_recovery_link(self) -> tuple[Optional[str], Optional[Token]]: | ||||
|         """Create a recovery link (when the current tenant has a recovery flow set), | ||||
|         that can either be shown to an admin or sent to the user directly""" | ||||
|         tenant: Tenant = self.request._request.tenant | ||||
|         tenant = get_tenant(self.request) | ||||
|         # Check that there is a recovery flow, if not return an error | ||||
|         flow = tenant.flow_recovery | ||||
|         if not flow: | ||||
| @ -350,8 +351,12 @@ class UserViewSet(UsedByMixin, ModelViewSet): | ||||
|         ) | ||||
|         querystring = urlencode({QS_KEY_TOKEN: token.key}) | ||||
|         link = self.request.build_absolute_uri( | ||||
|             reverse_lazy("authentik_core:if-flow", kwargs={"flow_slug": flow.slug}) | ||||
|             + f"?{querystring}" | ||||
|             reverse_interface( | ||||
|                 self.request, | ||||
|                 InterfaceType.FLOW, | ||||
|                 flow_slug=flow.slug, | ||||
|             ), | ||||
|             +f"?{querystring}", | ||||
|         ) | ||||
|         return link, token | ||||
|  | ||||
|  | ||||
| @ -33,6 +33,7 @@ from authentik.lib.models import ( | ||||
| ) | ||||
| from authentik.lib.utils.http import get_client_ip | ||||
| from authentik.policies.models import PolicyBindingModel | ||||
| from authentik.tenants.utils import get_tenant | ||||
|  | ||||
| LOGGER = get_logger() | ||||
| USER_ATTRIBUTE_DEBUG = "goauthentik.io/user/debug" | ||||
| @ -168,7 +169,7 @@ class User(SerializerModel, GuardianUserMixin, AbstractUser): | ||||
|         including the users attributes""" | ||||
|         final_attributes = {} | ||||
|         if request and hasattr(request, "tenant"): | ||||
|             always_merger.merge(final_attributes, request.tenant.attributes) | ||||
|             always_merger.merge(final_attributes, get_tenant(request).attributes) | ||||
|         for group in self.ak_groups.all().order_by("name"): | ||||
|             always_merger.merge(final_attributes, group.attributes) | ||||
|         always_merger.merge(final_attributes, self.attributes) | ||||
| @ -227,7 +228,7 @@ class User(SerializerModel, GuardianUserMixin, AbstractUser): | ||||
|         except Exception as exc: | ||||
|             LOGGER.warning("Failed to get default locale", exc=exc) | ||||
|         if request: | ||||
|             return request.tenant.locale | ||||
|             return get_tenant(request).default_locale | ||||
|         return "" | ||||
|  | ||||
|     @property | ||||
|  | ||||
| @ -25,7 +25,8 @@ from authentik.flows.planner import ( | ||||
| ) | ||||
| from authentik.flows.stage import StageView | ||||
| from authentik.flows.views.executor import NEXT_ARG_NAME, SESSION_KEY_GET, SESSION_KEY_PLAN | ||||
| from authentik.lib.utils.urls import redirect_with_qs | ||||
| from authentik.interfaces.models import InterfaceType | ||||
| from authentik.interfaces.views import redirect_to_default_interface | ||||
| from authentik.lib.views import bad_request_message | ||||
| from authentik.policies.denied import AccessDeniedResponse | ||||
| from authentik.policies.utils import delete_none_keys | ||||
| @ -226,7 +227,7 @@ class SourceFlowManager: | ||||
|         # Ensure redirect is carried through when user was trying to | ||||
|         # authorize application | ||||
|         final_redirect = self.request.session.get(SESSION_KEY_GET, {}).get( | ||||
|             NEXT_ARG_NAME, "authentik_core:if-user" | ||||
|             NEXT_ARG_NAME, "authentik_core:root-redirect" | ||||
|         ) | ||||
|         kwargs.update( | ||||
|             { | ||||
| @ -253,9 +254,9 @@ class SourceFlowManager: | ||||
|             for stage in stages: | ||||
|                 plan.append_stage(stage) | ||||
|         self.request.session[SESSION_KEY_PLAN] = plan | ||||
|         return redirect_with_qs( | ||||
|             "authentik_core:if-flow", | ||||
|             self.request.GET, | ||||
|         return redirect_to_default_interface( | ||||
|             self.request, | ||||
|             InterfaceType.FLOW, | ||||
|             flow_slug=flow.slug, | ||||
|         ) | ||||
|  | ||||
| @ -299,8 +300,9 @@ class SourceFlowManager: | ||||
|             _("Successfully linked %(source)s!" % {"source": self.source.name}), | ||||
|         ) | ||||
|         return redirect( | ||||
|             # Not ideal that we don't directly redirect to the configured user interface | ||||
|             reverse( | ||||
|                 "authentik_core:if-user", | ||||
|                 "authentik_core:root-redirect", | ||||
|             ) | ||||
|             + f"#/settings;page-{self.source.slug}" | ||||
|         ) | ||||
|  | ||||
| @ -59,4 +59,6 @@ class TestImpersonation(TestCase): | ||||
|         self.client.force_login(self.other_user) | ||||
|  | ||||
|         response = self.client.get(reverse("authentik_core:impersonate-end")) | ||||
|         self.assertRedirects(response, reverse("authentik_core:if-user")) | ||||
|         self.assertRedirects( | ||||
|             response, reverse("authentik_interfaces:if", kwargs={"if_name": "user"}) | ||||
|         ) | ||||
|  | ||||
| @ -3,23 +3,30 @@ from channels.auth import AuthMiddleware | ||||
| from channels.sessions import CookieMiddleware | ||||
| from django.conf import settings | ||||
| from django.contrib.auth.decorators import login_required | ||||
| from django.http import HttpRequest, HttpResponse | ||||
| from django.urls import path | ||||
| from django.views.decorators.csrf import ensure_csrf_cookie | ||||
| from django.views.generic import RedirectView | ||||
|  | ||||
| from authentik.core.views import apps, impersonate | ||||
| from authentik.core.views.debug import AccessDeniedView | ||||
| from authentik.core.views.interface import FlowInterfaceView, InterfaceView | ||||
| from authentik.core.views.session import EndSessionView | ||||
| from authentik.interfaces.models import InterfaceType | ||||
| from authentik.interfaces.views import RedirectToInterface | ||||
| from authentik.root.asgi_middleware import SessionMiddleware | ||||
| from authentik.root.messages.consumer import MessageConsumer | ||||
|  | ||||
|  | ||||
| def placeholder_view(request: HttpRequest, *args, **kwargs) -> HttpResponse: | ||||
|     """Empty view used as placeholder | ||||
|  | ||||
|     (Mounted to websocket endpoints and used by e2e tests)""" | ||||
|     return HttpResponse(status_code=200) | ||||
|  | ||||
|  | ||||
| urlpatterns = [ | ||||
|     path( | ||||
|         "", | ||||
|         login_required( | ||||
|             RedirectView.as_view(pattern_name="authentik_core:if-user", query_string=True) | ||||
|         ), | ||||
|         login_required(RedirectToInterface.as_view(type=InterfaceType.USER)), | ||||
|         name="root-redirect", | ||||
|     ), | ||||
|     path( | ||||
| @ -40,31 +47,16 @@ urlpatterns = [ | ||||
|         name="impersonate-end", | ||||
|     ), | ||||
|     # Interfaces | ||||
|     path( | ||||
|         "if/admin/", | ||||
|         ensure_csrf_cookie(InterfaceView.as_view(template_name="if/admin.html")), | ||||
|         name="if-admin", | ||||
|     ), | ||||
|     path( | ||||
|         "if/user/", | ||||
|         ensure_csrf_cookie(InterfaceView.as_view(template_name="if/user.html")), | ||||
|         name="if-user", | ||||
|     ), | ||||
|     path( | ||||
|         "if/flow/<slug:flow_slug>/", | ||||
|         ensure_csrf_cookie(FlowInterfaceView.as_view()), | ||||
|         name="if-flow", | ||||
|     ), | ||||
|     path( | ||||
|         "if/session-end/<slug:application_slug>/", | ||||
|         ensure_csrf_cookie(EndSessionView.as_view()), | ||||
|         name="if-session-end", | ||||
|     ), | ||||
|     # Fallback for WS | ||||
|     path("ws/outpost/<uuid:pk>/", InterfaceView.as_view(template_name="if/admin.html")), | ||||
|     path("ws/outpost/<uuid:pk>/", placeholder_view), | ||||
|     path( | ||||
|         "ws/client/", | ||||
|         InterfaceView.as_view(template_name="if/admin.html"), | ||||
|         placeholder_view, | ||||
|     ), | ||||
| ] | ||||
|  | ||||
|  | ||||
| @ -20,11 +20,13 @@ from authentik.flows.views.executor import ( | ||||
|     SESSION_KEY_PLAN, | ||||
|     ToDefaultFlow, | ||||
| ) | ||||
| from authentik.lib.utils.urls import redirect_with_qs | ||||
| from authentik.interfaces.models import InterfaceType | ||||
| from authentik.interfaces.views import redirect_to_default_interface | ||||
| from authentik.stages.consent.stage import ( | ||||
|     PLAN_CONTEXT_CONSENT_HEADER, | ||||
|     PLAN_CONTEXT_CONSENT_PERMISSIONS, | ||||
| ) | ||||
| from authentik.tenants.utils import get_tenant | ||||
|  | ||||
|  | ||||
| class RedirectToAppLaunch(View): | ||||
| @ -59,7 +61,7 @@ class RedirectToAppLaunch(View): | ||||
|             raise Http404 | ||||
|         plan.insert_stage(in_memory_stage(RedirectToAppStage)) | ||||
|         request.session[SESSION_KEY_PLAN] = plan | ||||
|         return redirect_with_qs("authentik_core:if-flow", request.GET, flow_slug=flow.slug) | ||||
|         return redirect_to_default_interface(request, InterfaceType.FLOW, flow_slug=flow.slug) | ||||
|  | ||||
|  | ||||
| class RedirectToAppStage(ChallengeStageView): | ||||
|  | ||||
| @ -35,7 +35,7 @@ class ImpersonateInitView(View): | ||||
|  | ||||
|         Event.new(EventAction.IMPERSONATION_STARTED).from_http(request, user_to_be) | ||||
|  | ||||
|         return redirect("authentik_core:if-user") | ||||
|         return redirect("authentik_core:root-redirect") | ||||
|  | ||||
|  | ||||
| class ImpersonateEndView(View): | ||||
| @ -48,7 +48,7 @@ class ImpersonateEndView(View): | ||||
|             or SESSION_KEY_IMPERSONATE_ORIGINAL_USER not in request.session | ||||
|         ): | ||||
|             LOGGER.debug("Can't end impersonation", user=request.user) | ||||
|             return redirect("authentik_core:if-user") | ||||
|             return redirect("authentik_core:root-redirect") | ||||
|  | ||||
|         original_user = request.session[SESSION_KEY_IMPERSONATE_ORIGINAL_USER] | ||||
|  | ||||
|  | ||||
| @ -1,36 +0,0 @@ | ||||
| """Interface views""" | ||||
| from json import dumps | ||||
| from typing import Any | ||||
|  | ||||
| from django.shortcuts import get_object_or_404 | ||||
| from django.views.generic.base import TemplateView | ||||
| from rest_framework.request import Request | ||||
|  | ||||
| from authentik import get_build_hash | ||||
| from authentik.admin.tasks import LOCAL_VERSION | ||||
| from authentik.api.v3.config import ConfigView | ||||
| from authentik.flows.models import Flow | ||||
| from authentik.tenants.api import CurrentTenantSerializer | ||||
|  | ||||
|  | ||||
| class InterfaceView(TemplateView): | ||||
|     """Base interface view""" | ||||
|  | ||||
|     def get_context_data(self, **kwargs: Any) -> dict[str, Any]: | ||||
|         kwargs["config_json"] = dumps(ConfigView(request=Request(self.request)).get_config().data) | ||||
|         kwargs["tenant_json"] = dumps(CurrentTenantSerializer(self.request.tenant).data) | ||||
|         kwargs["version_family"] = f"{LOCAL_VERSION.major}.{LOCAL_VERSION.minor}" | ||||
|         kwargs["version_subdomain"] = f"version-{LOCAL_VERSION.major}-{LOCAL_VERSION.minor}" | ||||
|         kwargs["build"] = get_build_hash() | ||||
|         return super().get_context_data(**kwargs) | ||||
|  | ||||
|  | ||||
| class FlowInterfaceView(InterfaceView): | ||||
|     """Flow interface""" | ||||
|  | ||||
|     template_name = "if/flow.html" | ||||
|  | ||||
|     def get_context_data(self, **kwargs: Any) -> dict[str, Any]: | ||||
|         kwargs["flow"] = get_object_or_404(Flow, slug=self.kwargs.get("flow_slug")) | ||||
|         kwargs["inspector"] = "inspector" in self.request.GET | ||||
|         return super().get_context_data(**kwargs) | ||||
| @ -41,8 +41,7 @@ from authentik.lib.utils.http import get_client_ip, get_http_session | ||||
| from authentik.lib.utils.time import timedelta_from_string | ||||
| from authentik.policies.models import PolicyBindingModel | ||||
| from authentik.stages.email.utils import TemplateEmailMessage | ||||
| from authentik.tenants.models import Tenant | ||||
| from authentik.tenants.utils import DEFAULT_TENANT | ||||
| from authentik.tenants.utils import get_fallback_tenant, get_tenant | ||||
|  | ||||
| LOGGER = get_logger() | ||||
| if TYPE_CHECKING: | ||||
| @ -57,7 +56,7 @@ def default_event_duration(): | ||||
|  | ||||
| def default_tenant(): | ||||
|     """Get a default value for tenant""" | ||||
|     return sanitize_dict(model_to_dict(DEFAULT_TENANT)) | ||||
|     return sanitize_dict(model_to_dict(get_fallback_tenant())) | ||||
|  | ||||
|  | ||||
| class NotificationTransportError(SentryIgnoredException): | ||||
| @ -227,7 +226,7 @@ class Event(SerializerModel, ExpiringModel): | ||||
|                 wrapped = self.context["http_request"]["args"][QS_QUERY] | ||||
|                 self.context["http_request"]["args"] = QueryDict(wrapped) | ||||
|         if hasattr(request, "tenant"): | ||||
|             tenant: Tenant = request.tenant | ||||
|             tenant = get_tenant(request) | ||||
|             # Because self.created only gets set on save, we can't use it's value here | ||||
|             # hence we set self.created to now and then use it | ||||
|             self.created = now() | ||||
|  | ||||
| @ -25,6 +25,8 @@ from authentik.flows.exceptions import FlowNonApplicableException | ||||
| from authentik.flows.models import Flow | ||||
| from authentik.flows.planner import CACHE_PREFIX, PLAN_CONTEXT_PENDING_USER, FlowPlanner, cache_key | ||||
| from authentik.flows.views.executor import SESSION_KEY_HISTORY, SESSION_KEY_PLAN | ||||
| from authentik.interfaces.models import InterfaceType | ||||
| from authentik.interfaces.views import reverse_interface | ||||
| from authentik.lib.utils.file import ( | ||||
|     FilePathSerializer, | ||||
|     FileUploadSerializer, | ||||
| @ -294,7 +296,11 @@ class FlowViewSet(UsedByMixin, ModelViewSet): | ||||
|         return Response( | ||||
|             { | ||||
|                 "link": request._request.build_absolute_uri( | ||||
|                     reverse("authentik_core:if-flow", kwargs={"flow_slug": flow.slug}) | ||||
|                     reverse_interface( | ||||
|                         request, | ||||
|                         InterfaceType.FLOW, | ||||
|                         flow_slug=flow.slug, | ||||
|                     ), | ||||
|                 ) | ||||
|             } | ||||
|         ) | ||||
|  | ||||
| @ -7,6 +7,8 @@ from authentik.core.tests.utils import create_test_flow | ||||
| from authentik.flows.models import Flow, FlowDesignation | ||||
| from authentik.flows.planner import FlowPlan | ||||
| from authentik.flows.views.executor import SESSION_KEY_APPLICATION_PRE, SESSION_KEY_PLAN | ||||
| from authentik.interfaces.models import InterfaceType | ||||
| from authentik.interfaces.tests import reverse_interface | ||||
| from authentik.lib.generators import generate_id | ||||
| from authentik.providers.oauth2.models import OAuth2Provider | ||||
|  | ||||
| @ -21,7 +23,10 @@ class TestHelperView(TestCase): | ||||
|         response = self.client.get( | ||||
|             reverse("authentik_flows:default-invalidation"), | ||||
|         ) | ||||
|         expected_url = reverse("authentik_core:if-flow", kwargs={"flow_slug": flow.slug}) | ||||
|         expected_url = reverse_interface( | ||||
|             InterfaceType.FLOW, | ||||
|             flow_slug=flow.slug, | ||||
|         ) | ||||
|         self.assertEqual(response.status_code, 302) | ||||
|         self.assertEqual(response.url, expected_url) | ||||
|  | ||||
| @ -72,6 +77,9 @@ class TestHelperView(TestCase): | ||||
|         response = self.client.get( | ||||
|             reverse("authentik_flows:default-invalidation"), | ||||
|         ) | ||||
|         expected_url = reverse("authentik_core:if-flow", kwargs={"flow_slug": flow.slug}) | ||||
|         expected_url = reverse_interface( | ||||
|             InterfaceType.FLOW, | ||||
|             flow_slug=flow.slug, | ||||
|         ) | ||||
|         self.assertEqual(response.status_code, 302) | ||||
|         self.assertEqual(response.url, expected_url) | ||||
|  | ||||
| @ -53,12 +53,14 @@ from authentik.flows.planner import ( | ||||
|     FlowPlanner, | ||||
| ) | ||||
| from authentik.flows.stage import AccessDeniedChallengeView, StageView | ||||
| from authentik.interfaces.models import InterfaceType | ||||
| from authentik.interfaces.views import redirect_to_default_interface | ||||
| from authentik.lib.sentry import SentryIgnoredException | ||||
| from authentik.lib.utils.errors import exception_to_string | ||||
| from authentik.lib.utils.reflection import all_subclasses, class_to_path | ||||
| from authentik.lib.utils.urls import is_url_absolute, redirect_with_qs | ||||
| from authentik.policies.engine import PolicyEngine | ||||
| from authentik.tenants.models import Tenant | ||||
| from authentik.tenants.utils import get_tenant | ||||
|  | ||||
| LOGGER = get_logger() | ||||
| # Argument used to redirect user after login | ||||
| @ -479,7 +481,7 @@ class ToDefaultFlow(View): | ||||
|  | ||||
|     def get_flow(self) -> Flow: | ||||
|         """Get a flow for the selected designation""" | ||||
|         tenant: Tenant = self.request.tenant | ||||
|         tenant = get_tenant(self.request) | ||||
|         flow = None | ||||
|         # First, attempt to get default flow from tenant | ||||
|         if self.designation == FlowDesignation.AUTHENTICATION: | ||||
| @ -512,7 +514,7 @@ class ToDefaultFlow(View): | ||||
|                     flow_slug=flow.slug, | ||||
|                 ) | ||||
|                 del self.request.session[SESSION_KEY_PLAN] | ||||
|         return redirect_with_qs("authentik_core:if-flow", request.GET, flow_slug=flow.slug) | ||||
|         return redirect_to_default_interface(request, InterfaceType.FLOW, flow_slug=flow.slug) | ||||
|  | ||||
|  | ||||
| def to_stage_response(request: HttpRequest, source: HttpResponse) -> HttpResponse: | ||||
| @ -583,8 +585,8 @@ class ConfigureFlowInitView(LoginRequiredMixin, View): | ||||
|             LOGGER.warning("Flow not applicable to user") | ||||
|             raise Http404 | ||||
|         request.session[SESSION_KEY_PLAN] = plan | ||||
|         return redirect_with_qs( | ||||
|             "authentik_core:if-flow", | ||||
|             self.request.GET, | ||||
|         return redirect_to_default_interface( | ||||
|             self.request, | ||||
|             InterfaceType.FLOW, | ||||
|             flow_slug=stage.configure_flow.slug, | ||||
|         ) | ||||
|  | ||||
							
								
								
									
										0
									
								
								authentik/interfaces/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								authentik/interfaces/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										28
									
								
								authentik/interfaces/api.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								authentik/interfaces/api.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,28 @@ | ||||
| """interfaces API""" | ||||
| from rest_framework.serializers import ModelSerializer | ||||
| from rest_framework.viewsets import ModelViewSet | ||||
|  | ||||
| from authentik.core.api.used_by import UsedByMixin | ||||
| from authentik.interfaces.models import Interface | ||||
|  | ||||
|  | ||||
| class InterfaceSerializer(ModelSerializer): | ||||
|     """Interface serializer""" | ||||
|  | ||||
|     class Meta: | ||||
|         model = Interface | ||||
|         fields = [ | ||||
|             "interface_uuid", | ||||
|             "url_name", | ||||
|             "type", | ||||
|             "template", | ||||
|         ] | ||||
|  | ||||
|  | ||||
| class InterfaceViewSet(UsedByMixin, ModelViewSet): | ||||
|     """Interface serializer""" | ||||
|  | ||||
|     queryset = Interface.objects.all() | ||||
|     serializer_class = InterfaceSerializer | ||||
|     filterset_fields = ["url_name", "type", "template"] | ||||
|     search_fields = ["url_name", "type", "template"] | ||||
							
								
								
									
										12
									
								
								authentik/interfaces/apps.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								authentik/interfaces/apps.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,12 @@ | ||||
| """authentik interfaces app config""" | ||||
| from authentik.blueprints.apps import ManagedAppConfig | ||||
|  | ||||
|  | ||||
| class AuthentikInterfacesConfig(ManagedAppConfig): | ||||
|     """authentik interfaces app config""" | ||||
|  | ||||
|     name = "authentik.interfaces" | ||||
|     label = "authentik_interfaces" | ||||
|     verbose_name = "authentik Interfaces" | ||||
|     mountpoint = "if/" | ||||
|     default = True | ||||
							
								
								
									
										36
									
								
								authentik/interfaces/migrations/0001_initial.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								authentik/interfaces/migrations/0001_initial.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,36 @@ | ||||
| # Generated by Django 4.1.7 on 2023-02-16 11:01 | ||||
|  | ||||
| import uuid | ||||
|  | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|     initial = True | ||||
|  | ||||
|     dependencies = [] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.CreateModel( | ||||
|             name="Interface", | ||||
|             fields=[ | ||||
|                 ( | ||||
|                     "interface_uuid", | ||||
|                     models.UUIDField( | ||||
|                         default=uuid.uuid4, editable=False, primary_key=True, serialize=False | ||||
|                     ), | ||||
|                 ), | ||||
|                 ("url_name", models.SlugField(unique=True)), | ||||
|                 ( | ||||
|                     "type", | ||||
|                     models.TextField( | ||||
|                         choices=[("user", "User"), ("admin", "Admin"), ("flow", "Flow")] | ||||
|                     ), | ||||
|                 ), | ||||
|                 ("template", models.TextField()), | ||||
|             ], | ||||
|             options={ | ||||
|                 "abstract": False, | ||||
|             }, | ||||
|         ), | ||||
|     ] | ||||
							
								
								
									
										0
									
								
								authentik/interfaces/migrations/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								authentik/interfaces/migrations/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										33
									
								
								authentik/interfaces/models.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								authentik/interfaces/models.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,33 @@ | ||||
| """Interface models""" | ||||
| from typing import Type | ||||
| from uuid import uuid4 | ||||
|  | ||||
| from django.db import models | ||||
| from rest_framework.serializers import BaseSerializer | ||||
|  | ||||
| from authentik.lib.models import SerializerModel | ||||
|  | ||||
|  | ||||
| class InterfaceType(models.TextChoices): | ||||
|     """Interface types""" | ||||
|  | ||||
|     USER = "user" | ||||
|     ADMIN = "admin" | ||||
|     FLOW = "flow" | ||||
|  | ||||
|  | ||||
| class Interface(SerializerModel): | ||||
|     """Interface""" | ||||
|  | ||||
|     interface_uuid = models.UUIDField(primary_key=True, editable=False, default=uuid4) | ||||
|  | ||||
|     url_name = models.SlugField(unique=True) | ||||
|  | ||||
|     type = models.TextField(choices=InterfaceType.choices) | ||||
|     template = models.TextField() | ||||
|  | ||||
|     @property | ||||
|     def serializer(self) -> Type[BaseSerializer]: | ||||
|         from authentik.interfaces.api import InterfaceSerializer | ||||
|  | ||||
|         return InterfaceSerializer | ||||
							
								
								
									
										12
									
								
								authentik/interfaces/tests.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								authentik/interfaces/tests.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,12 @@ | ||||
| """Interface tests""" | ||||
| from django.test import RequestFactory | ||||
|  | ||||
| from authentik.interfaces.models import InterfaceType | ||||
| from authentik.interfaces.views import reverse_interface as full_reverse_interface | ||||
|  | ||||
|  | ||||
| def reverse_interface(interface_type: InterfaceType, **kwargs): | ||||
|     """reverse_interface wrapper for tests""" | ||||
|     factory = RequestFactory() | ||||
|     request = factory.get("/") | ||||
|     return full_reverse_interface(request, interface_type, **kwargs) | ||||
							
								
								
									
										14
									
								
								authentik/interfaces/urls.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								authentik/interfaces/urls.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,14 @@ | ||||
| """Interface urls""" | ||||
| from django.urls import path | ||||
|  | ||||
| from authentik.interfaces.views import InterfaceView | ||||
|  | ||||
| urlpatterns = [ | ||||
|     path( | ||||
|         "<slug:if_name>/", | ||||
|         InterfaceView.as_view(), | ||||
|         kwargs={"flow_slug": None}, | ||||
|         name="if", | ||||
|     ), | ||||
|     path("<slug:if_name>/<slug:flow_slug>/", InterfaceView.as_view(), name="if"), | ||||
| ] | ||||
							
								
								
									
										113
									
								
								authentik/interfaces/views.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										113
									
								
								authentik/interfaces/views.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,113 @@ | ||||
| """Interface views""" | ||||
| from json import dumps | ||||
| from typing import Any, Optional | ||||
| from urllib.parse import urlencode | ||||
|  | ||||
| from django.http import Http404, HttpRequest, HttpResponse, QueryDict | ||||
| from django.shortcuts import get_object_or_404, redirect | ||||
| from django.template import Template, TemplateSyntaxError, engines | ||||
| from django.template.response import TemplateResponse | ||||
| from django.utils.decorators import method_decorator | ||||
| from django.views import View | ||||
| from django.views.decorators.cache import cache_page | ||||
| from django.views.decorators.csrf import ensure_csrf_cookie | ||||
| from rest_framework.request import Request | ||||
| from structlog.stdlib import get_logger | ||||
|  | ||||
| from authentik import get_build_hash | ||||
| from authentik.admin.tasks import LOCAL_VERSION | ||||
| from authentik.api.v3.config import ConfigView | ||||
| from authentik.flows.models import Flow | ||||
| from authentik.interfaces.models import Interface, InterfaceType | ||||
| from authentik.lib.utils.urls import reverse_with_qs | ||||
| from authentik.tenants.api import CurrentTenantSerializer | ||||
| from authentik.tenants.utils import get_tenant | ||||
|  | ||||
| LOGGER = get_logger() | ||||
|  | ||||
|  | ||||
| def template_from_string(template_string: str) -> Template: | ||||
|     """Render template from string""" | ||||
|     chain = [] | ||||
|     engine_list = engines.all() | ||||
|     for engine in engine_list: | ||||
|         try: | ||||
|             return engine.from_string(template_string) | ||||
|         except TemplateSyntaxError as exc: | ||||
|             chain.append(exc) | ||||
|     raise TemplateSyntaxError(template_string, chain=chain) | ||||
|  | ||||
|  | ||||
| def redirect_to_default_interface(request: HttpRequest, interface_type: InterfaceType, **kwargs): | ||||
|     """Shortcut to inline redirect to default interface, | ||||
|     keeping GET parameters of the passed request""" | ||||
|     return RedirectToInterface.as_view(type=interface_type)(request, **kwargs) | ||||
|  | ||||
|  | ||||
| def reverse_interface( | ||||
|     request: HttpRequest, interface_type: InterfaceType, query: Optional[QueryDict] = None, **kwargs | ||||
| ): | ||||
|     """Reverse URL to configured default interface""" | ||||
|     tenant = get_tenant(request) | ||||
|     interface: Interface = None | ||||
|  | ||||
|     if interface_type == InterfaceType.USER: | ||||
|         interface = tenant.interface_user | ||||
|     if interface_type == InterfaceType.ADMIN: | ||||
|         interface = tenant.interface_admin | ||||
|     if interface_type == InterfaceType.FLOW: | ||||
|         interface = tenant.interface_flow | ||||
|  | ||||
|     if not interface: | ||||
|         LOGGER.warning("No interface found", type=interface_type, tenant=tenant) | ||||
|         raise Http404() | ||||
|     kwargs["if_name"] = interface.url_name | ||||
|     return reverse_with_qs( | ||||
|         "authentik_interfaces:if", | ||||
|         query=query or request.GET, | ||||
|         kwargs=kwargs, | ||||
|     ) | ||||
|  | ||||
|  | ||||
| class RedirectToInterface(View): | ||||
|     """Redirect to tenant's configured view for specified type""" | ||||
|  | ||||
|     type: Optional[InterfaceType] = None | ||||
|  | ||||
|     def dispatch(self, request: HttpRequest, **kwargs: Any) -> HttpResponse: | ||||
|         target = reverse_interface(request, self.type, **kwargs) | ||||
|         if self.request.GET: | ||||
|             target += "?" + urlencode(self.request.GET.items()) | ||||
|         return redirect(target) | ||||
|  | ||||
|  | ||||
| @method_decorator(ensure_csrf_cookie, name="dispatch") | ||||
| @method_decorator(cache_page(60 * 10), name="dispatch") | ||||
| class InterfaceView(View): | ||||
|     """General interface view""" | ||||
|  | ||||
|     def get_context_data(self) -> dict[str, Any]: | ||||
|         """Get template context""" | ||||
|         return { | ||||
|             "config_json": dumps(ConfigView(request=Request(self.request)).get_config().data), | ||||
|             "tenant_json": dumps(CurrentTenantSerializer(get_tenant(self.request)).data), | ||||
|             "version_family": f"{LOCAL_VERSION.major}.{LOCAL_VERSION.minor}", | ||||
|             "version_subdomain": f"version-{LOCAL_VERSION.major}-{LOCAL_VERSION.minor}", | ||||
|             "build": get_build_hash(), | ||||
|         } | ||||
|  | ||||
|     def type_flow(self, context: dict[str, Any]): | ||||
|         """Special handling for flow interfaces""" | ||||
|         if self.kwargs.get("flow_slug", None) is None: | ||||
|             raise Http404() | ||||
|         context["flow"] = get_object_or_404(Flow, slug=self.kwargs.get("flow_slug")) | ||||
|         context["inspector"] = "inspector" in self.request.GET | ||||
|  | ||||
|     def dispatch(self, request: HttpRequest, if_name: str, **kwargs: Any) -> HttpResponse: | ||||
|         context = self.get_context_data() | ||||
|         # TODO: Cache | ||||
|         interface: Interface = get_object_or_404(Interface, url_name=if_name) | ||||
|         if interface.type == InterfaceType.FLOW: | ||||
|             self.type_flow(context) | ||||
|         template = template_from_string(interface.template) | ||||
|         return TemplateResponse(request, template, context) | ||||
| @ -12,6 +12,7 @@ from authentik.lib.utils.http import get_http_session | ||||
| from authentik.policies.models import Policy | ||||
| from authentik.policies.types import PolicyRequest, PolicyResult | ||||
| from authentik.stages.prompt.stage import PLAN_CONTEXT_PROMPT | ||||
| from authentik.tenants.utils import get_tenant | ||||
|  | ||||
| LOGGER = get_logger() | ||||
| RE_LOWER = re.compile("[a-z]") | ||||
| @ -143,7 +144,8 @@ class PasswordPolicy(Policy): | ||||
|             user_inputs.append(request.user.name) | ||||
|             user_inputs.append(request.user.email) | ||||
|         if request.http_request: | ||||
|             user_inputs.append(request.http_request.tenant.branding_title) | ||||
|             tenant = get_tenant(request.http_request) | ||||
|             user_inputs.append(tenant.branding_title) | ||||
|         # Only calculate result for the first 100 characters, as with over 100 char | ||||
|         # long passwords we can be reasonably sure that they'll surpass the score anyways | ||||
|         # See https://github.com/dropbox/zxcvbn#runtime-latency | ||||
|  | ||||
| @ -39,8 +39,9 @@ class TesOAuth2DeviceInit(OAuthTestCase): | ||||
|         self.assertEqual( | ||||
|             res.url, | ||||
|             reverse( | ||||
|                 "authentik_core:if-flow", | ||||
|                 "authentik_interfaces:if", | ||||
|                 kwargs={ | ||||
|                     "if_name": "flow", | ||||
|                     "flow_slug": self.device_flow.slug, | ||||
|                 }, | ||||
|             ), | ||||
| @ -68,8 +69,9 @@ class TesOAuth2DeviceInit(OAuthTestCase): | ||||
|         self.assertEqual( | ||||
|             res.url, | ||||
|             reverse( | ||||
|                 "authentik_core:if-flow", | ||||
|                 "authentik_interfaces:if", | ||||
|                 kwargs={ | ||||
|                     "if_name": "flow", | ||||
|                     "flow_slug": self.provider.authorization_flow.slug, | ||||
|                 }, | ||||
|             ) | ||||
|  | ||||
| @ -29,8 +29,9 @@ from authentik.flows.models import in_memory_stage | ||||
| from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, PLAN_CONTEXT_SSO, FlowPlanner | ||||
| from authentik.flows.stage import StageView | ||||
| from authentik.flows.views.executor import SESSION_KEY_PLAN | ||||
| from authentik.interfaces.models import InterfaceType | ||||
| from authentik.interfaces.views import redirect_to_default_interface | ||||
| from authentik.lib.utils.time import timedelta_from_string | ||||
| from authentik.lib.utils.urls import redirect_with_qs | ||||
| from authentik.lib.views import bad_request_message | ||||
| from authentik.policies.types import PolicyRequest | ||||
| from authentik.policies.views import PolicyAccessView, RequestValidationError | ||||
| @ -404,9 +405,9 @@ class AuthorizationFlowInitView(PolicyAccessView): | ||||
|         plan.append_stage(in_memory_stage(OAuthFulfillmentStage)) | ||||
|  | ||||
|         self.request.session[SESSION_KEY_PLAN] = plan | ||||
|         return redirect_with_qs( | ||||
|             "authentik_core:if-flow", | ||||
|             self.request.GET, | ||||
|         return redirect_to_default_interface( | ||||
|             self.request, | ||||
|             InterfaceType.FLOW, | ||||
|             flow_slug=self.provider.authorization_flow.slug, | ||||
|         ) | ||||
|  | ||||
|  | ||||
| @ -15,7 +15,8 @@ from authentik.flows.models import in_memory_stage | ||||
| from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, PLAN_CONTEXT_SSO, FlowPlanner | ||||
| from authentik.flows.stage import ChallengeStageView | ||||
| from authentik.flows.views.executor import SESSION_KEY_PLAN | ||||
| from authentik.lib.utils.urls import redirect_with_qs | ||||
| from authentik.interfaces.models import InterfaceType | ||||
| from authentik.interfaces.views import redirect_to_default_interface | ||||
| from authentik.providers.oauth2.models import DeviceToken, OAuth2Provider | ||||
| from authentik.providers.oauth2.views.device_finish import ( | ||||
|     PLAN_CONTEXT_DEVICE, | ||||
| @ -26,7 +27,7 @@ from authentik.stages.consent.stage import ( | ||||
|     PLAN_CONTEXT_CONSENT_HEADER, | ||||
|     PLAN_CONTEXT_CONSENT_PERMISSIONS, | ||||
| ) | ||||
| from authentik.tenants.models import Tenant | ||||
| from authentik.tenants.utils import get_tenant | ||||
|  | ||||
| LOGGER = get_logger() | ||||
| QS_KEY_CODE = "code"  # nosec | ||||
| @ -77,9 +78,9 @@ def validate_code(code: int, request: HttpRequest) -> Optional[HttpResponse]: | ||||
|         return None | ||||
|     plan.insert_stage(in_memory_stage(OAuthDeviceCodeFinishStage)) | ||||
|     request.session[SESSION_KEY_PLAN] = plan | ||||
|     return redirect_with_qs( | ||||
|         "authentik_core:if-flow", | ||||
|         request.GET, | ||||
|     return redirect_to_default_interface( | ||||
|         request, | ||||
|         InterfaceType.FLOW, | ||||
|         flow_slug=token.provider.authorization_flow.slug, | ||||
|     ) | ||||
|  | ||||
| @ -88,7 +89,7 @@ class DeviceEntryView(View): | ||||
|     """View used to initiate the device-code flow, url entered by endusers""" | ||||
|  | ||||
|     def dispatch(self, request: HttpRequest) -> HttpResponse: | ||||
|         tenant: Tenant = request.tenant | ||||
|         tenant = get_tenant(request) | ||||
|         device_flow = tenant.flow_device_code | ||||
|         if not device_flow: | ||||
|             LOGGER.info("Tenant has no device code flow configured", tenant=tenant) | ||||
| @ -110,9 +111,9 @@ class DeviceEntryView(View): | ||||
|         plan.append_stage(in_memory_stage(OAuthDeviceCodeStage)) | ||||
|  | ||||
|         self.request.session[SESSION_KEY_PLAN] = plan | ||||
|         return redirect_with_qs( | ||||
|             "authentik_core:if-flow", | ||||
|             self.request.GET, | ||||
|         return redirect_to_default_interface( | ||||
|             self.request, | ||||
|             InterfaceType.FLOW, | ||||
|             flow_slug=device_flow.slug, | ||||
|         ) | ||||
|  | ||||
|  | ||||
| @ -9,6 +9,7 @@ from django.views.decorators.csrf import csrf_exempt | ||||
| from authentik.providers.oauth2.constants import SCOPE_GITHUB_ORG_READ, SCOPE_GITHUB_USER_EMAIL | ||||
| from authentik.providers.oauth2.models import RefreshToken | ||||
| from authentik.providers.oauth2.utils import protected_resource_view | ||||
| from authentik.tenants.utils import get_tenant | ||||
|  | ||||
|  | ||||
| @method_decorator(csrf_exempt, name="dispatch") | ||||
| @ -76,6 +77,7 @@ class GitHubUserTeamsView(View): | ||||
|     def get(self, request: HttpRequest, token: RefreshToken) -> HttpResponse: | ||||
|         """Emulate GitHub's /user/teams API Endpoint""" | ||||
|         user = token.user | ||||
|         tenant = get_tenant(request) | ||||
|  | ||||
|         orgs_response = [] | ||||
|         for org in user.ak_groups.all(): | ||||
| @ -97,7 +99,7 @@ class GitHubUserTeamsView(View): | ||||
|                 "created_at": "", | ||||
|                 "updated_at": "", | ||||
|                 "organization": { | ||||
|                     "login": slugify(request.tenant.branding_title), | ||||
|                     "login": slugify(tenant.branding_title), | ||||
|                     "id": 1, | ||||
|                     "node_id": "", | ||||
|                     "url": "", | ||||
| @ -109,7 +111,7 @@ class GitHubUserTeamsView(View): | ||||
|                     "public_members_url": "", | ||||
|                     "avatar_url": "", | ||||
|                     "description": "", | ||||
|                     "name": request.tenant.branding_title, | ||||
|                     "name": tenant.branding_title, | ||||
|                     "company": "", | ||||
|                     "blog": "", | ||||
|                     "location": "", | ||||
|  | ||||
| @ -15,7 +15,8 @@ from authentik.flows.exceptions import FlowNonApplicableException | ||||
| from authentik.flows.models import in_memory_stage | ||||
| from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, PLAN_CONTEXT_SSO, FlowPlanner | ||||
| from authentik.flows.views.executor import SESSION_KEY_PLAN, SESSION_KEY_POST | ||||
| from authentik.lib.utils.urls import redirect_with_qs | ||||
| from authentik.interfaces.models import InterfaceType | ||||
| from authentik.interfaces.views import redirect_to_default_interface | ||||
| from authentik.lib.views import bad_request_message | ||||
| from authentik.policies.views import PolicyAccessView | ||||
| from authentik.providers.saml.exceptions import CannotHandleAssertion | ||||
| @ -76,9 +77,9 @@ class SAMLSSOView(PolicyAccessView): | ||||
|             raise Http404 | ||||
|         plan.append_stage(in_memory_stage(SAMLFlowFinalView)) | ||||
|         request.session[SESSION_KEY_PLAN] = plan | ||||
|         return redirect_with_qs( | ||||
|             "authentik_core:if-flow", | ||||
|             request.GET, | ||||
|         return redirect_to_default_interface( | ||||
|             request, | ||||
|             InterfaceType.FLOW, | ||||
|             flow_slug=self.provider.authorization_flow.slug, | ||||
|         ) | ||||
|  | ||||
|  | ||||
| @ -22,4 +22,4 @@ class UseTokenView(View): | ||||
|         login(request, token.user, backend=BACKEND_INBUILT) | ||||
|         token.delete() | ||||
|         messages.warning(request, _("Used recovery-link to authenticate.")) | ||||
|         return redirect("authentik_core:if-user") | ||||
|         return redirect("authentik_core:root-redirect") | ||||
|  | ||||
| @ -65,6 +65,7 @@ INSTALLED_APPS = [ | ||||
|     "authentik.admin", | ||||
|     "authentik.api", | ||||
|     "authentik.crypto", | ||||
|     "authentik.interfaces", | ||||
|     "authentik.events", | ||||
|     "authentik.flows", | ||||
|     "authentik.lib", | ||||
|  | ||||
| @ -32,7 +32,8 @@ from authentik.flows.planner import ( | ||||
| ) | ||||
| from authentik.flows.stage import ChallengeStageView | ||||
| from authentik.flows.views.executor import NEXT_ARG_NAME, SESSION_KEY_GET, SESSION_KEY_PLAN | ||||
| from authentik.lib.utils.urls import redirect_with_qs | ||||
| from authentik.interfaces.models import InterfaceType | ||||
| from authentik.interfaces.views import redirect_to_default_interface | ||||
| from authentik.lib.views import bad_request_message | ||||
| from authentik.providers.saml.utils.encoding import nice64 | ||||
| from authentik.sources.saml.exceptions import MissingSAMLResponse, UnsupportedNameIDFormat | ||||
| @ -72,7 +73,7 @@ class InitiateView(View): | ||||
|         # Ensure redirect is carried through when user was trying to | ||||
|         # authorize application | ||||
|         final_redirect = self.request.session.get(SESSION_KEY_GET, {}).get( | ||||
|             NEXT_ARG_NAME, "authentik_core:if-user" | ||||
|             NEXT_ARG_NAME, "authentik_core:root-redirect" | ||||
|         ) | ||||
|         kwargs.update( | ||||
|             { | ||||
| @ -91,9 +92,9 @@ class InitiateView(View): | ||||
|         for stage in stages_to_append: | ||||
|             plan.append_stage(stage) | ||||
|         self.request.session[SESSION_KEY_PLAN] = plan | ||||
|         return redirect_with_qs( | ||||
|             "authentik_core:if-flow", | ||||
|             self.request.GET, | ||||
|         return redirect_to_default_interface( | ||||
|             self.request, | ||||
|             InterfaceType.FLOW, | ||||
|             flow_slug=source.pre_authentication_flow.slug, | ||||
|         ) | ||||
|  | ||||
|  | ||||
| @ -17,6 +17,7 @@ from authentik.flows.challenge import ( | ||||
| from authentik.flows.stage import ChallengeStageView | ||||
| from authentik.stages.authenticator_totp.models import AuthenticatorTOTPStage | ||||
| from authentik.stages.authenticator_totp.settings import OTP_TOTP_ISSUER | ||||
| from authentik.tenants.utils import get_tenant | ||||
|  | ||||
| SESSION_TOTP_DEVICE = "totp_device" | ||||
|  | ||||
| @ -57,7 +58,7 @@ class AuthenticatorTOTPStageView(ChallengeStageView): | ||||
|             data={ | ||||
|                 "type": ChallengeTypes.NATIVE.value, | ||||
|                 "config_url": device.config_url.replace( | ||||
|                     OTP_TOTP_ISSUER, quote(self.request.tenant.branding_title) | ||||
|                     OTP_TOTP_ISSUER, quote(get_tenant(self.request).branding_title) | ||||
|                 ), | ||||
|             } | ||||
|         ) | ||||
|  | ||||
| @ -33,6 +33,7 @@ from authentik.stages.authenticator_validate.models import AuthenticatorValidate | ||||
| from authentik.stages.authenticator_webauthn.models import UserVerification, WebAuthnDevice | ||||
| from authentik.stages.authenticator_webauthn.stage import SESSION_KEY_WEBAUTHN_CHALLENGE | ||||
| from authentik.stages.authenticator_webauthn.utils import get_origin, get_rp_id | ||||
| from authentik.tenants.utils import get_tenant | ||||
|  | ||||
| LOGGER = get_logger() | ||||
|  | ||||
| @ -187,7 +188,7 @@ def validate_challenge_duo(device_pk: int, stage_view: StageView, user: User) -> | ||||
|             type=__( | ||||
|                 "%(brand_name)s Login request" | ||||
|                 % { | ||||
|                     "brand_name": stage_view.request.tenant.branding_title, | ||||
|                     "brand_name": get_tenant(stage_view.request).branding_title, | ||||
|                 } | ||||
|             ), | ||||
|             display_username=user.username, | ||||
|  | ||||
| @ -19,7 +19,7 @@ from authentik.stages.authenticator_duo.models import AuthenticatorDuoStage, Duo | ||||
| from authentik.stages.authenticator_validate.challenge import validate_challenge_duo | ||||
| from authentik.stages.authenticator_validate.models import AuthenticatorValidateStage, DeviceClasses | ||||
| from authentik.stages.user_login.models import UserLoginStage | ||||
| from authentik.tenants.utils import get_tenant_for_request | ||||
| from authentik.tenants.utils import lookup_tenant_for_request | ||||
|  | ||||
|  | ||||
| class AuthenticatorValidateStageDuoTests(FlowTestCase): | ||||
| @ -36,7 +36,7 @@ class AuthenticatorValidateStageDuoTests(FlowTestCase): | ||||
|         middleware = SessionMiddleware(dummy_get_response) | ||||
|         middleware.process_request(request) | ||||
|         request.session.save() | ||||
|         setattr(request, "tenant", get_tenant_for_request(request)) | ||||
|         setattr(request, "tenant", lookup_tenant_for_request(request)) | ||||
|  | ||||
|         stage = AuthenticatorDuoStage.objects.create( | ||||
|             name=generate_id(), | ||||
|  | ||||
| @ -29,6 +29,7 @@ from authentik.flows.challenge import ( | ||||
| from authentik.flows.stage import ChallengeStageView | ||||
| from authentik.stages.authenticator_webauthn.models import AuthenticateWebAuthnStage, WebAuthnDevice | ||||
| from authentik.stages.authenticator_webauthn.utils import get_origin, get_rp_id | ||||
| from authentik.tenants.utils import get_tenant | ||||
|  | ||||
| SESSION_KEY_WEBAUTHN_CHALLENGE = "authentik/stages/authenticator_webauthn/challenge" | ||||
|  | ||||
| @ -92,7 +93,7 @@ class AuthenticatorWebAuthnStageView(ChallengeStageView): | ||||
|  | ||||
|         registration_options: PublicKeyCredentialCreationOptions = generate_registration_options( | ||||
|             rp_id=get_rp_id(self.request), | ||||
|             rp_name=self.request.tenant.branding_title, | ||||
|             rp_name=get_tenant(self.request).branding_title, | ||||
|             user_id=user.uid, | ||||
|             user_name=user.username, | ||||
|             user_display_name=user.name, | ||||
|  | ||||
| @ -3,7 +3,6 @@ from datetime import timedelta | ||||
|  | ||||
| from django.contrib import messages | ||||
| from django.http import HttpRequest, HttpResponse | ||||
| from django.urls import reverse | ||||
| from django.utils.http import urlencode | ||||
| from django.utils.text import slugify | ||||
| from django.utils.timezone import now | ||||
| @ -16,6 +15,8 @@ from authentik.flows.models import FlowToken | ||||
| from authentik.flows.planner import PLAN_CONTEXT_IS_RESTORED, PLAN_CONTEXT_PENDING_USER | ||||
| from authentik.flows.stage import ChallengeStageView | ||||
| from authentik.flows.views.executor import QS_KEY_TOKEN | ||||
| from authentik.interfaces.models import InterfaceType | ||||
| from authentik.interfaces.views import reverse_interface | ||||
| from authentik.stages.email.models import EmailStage | ||||
| from authentik.stages.email.tasks import send_mails | ||||
| from authentik.stages.email.utils import TemplateEmailMessage | ||||
| @ -47,9 +48,10 @@ class EmailStageView(ChallengeStageView): | ||||
|  | ||||
|     def get_full_url(self, **kwargs) -> str: | ||||
|         """Get full URL to be used in template""" | ||||
|         base_url = reverse( | ||||
|             "authentik_core:if-flow", | ||||
|             kwargs={"flow_slug": self.executor.flow.slug}, | ||||
|         base_url = reverse_interface( | ||||
|             self.request, | ||||
|             InterfaceType.FLOW, | ||||
|             flow_slug=self.executor.flow.slug, | ||||
|         ) | ||||
|         relative_url = f"{base_url}?{urlencode(kwargs)}" | ||||
|         return self.request.build_absolute_uri(relative_url) | ||||
|  | ||||
| @ -7,6 +7,7 @@ from django.core.mail.backends.locmem import EmailBackend | ||||
| from django.urls import reverse | ||||
| from rest_framework.test import APITestCase | ||||
|  | ||||
| from authentik.blueprints.tests import apply_blueprint | ||||
| from authentik.core.tests.utils import create_test_admin_user, create_test_flow | ||||
| from authentik.events.models import Event, EventAction | ||||
| from authentik.flows.markers import StageMarker | ||||
| @ -29,6 +30,7 @@ class TestEmailStageSending(APITestCase): | ||||
|         ) | ||||
|         self.binding = FlowStageBinding.objects.create(target=self.flow, stage=self.stage, order=2) | ||||
|  | ||||
|     @apply_blueprint("system/interfaces.yaml") | ||||
|     def test_pending_user(self): | ||||
|         """Test with pending user""" | ||||
|         plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()]) | ||||
| @ -54,6 +56,7 @@ class TestEmailStageSending(APITestCase): | ||||
|             self.assertEqual(event.context["to_email"], [self.user.email]) | ||||
|             self.assertEqual(event.context["from_email"], "system@authentik.local") | ||||
|  | ||||
|     @apply_blueprint("system/interfaces.yaml") | ||||
|     def test_send_error(self): | ||||
|         """Test error during sending (sending will be retried)""" | ||||
|         plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()]) | ||||
|  | ||||
| @ -7,6 +7,7 @@ from django.core.mail.backends.smtp import EmailBackend as SMTPEmailBackend | ||||
| from django.urls import reverse | ||||
| from django.utils.http import urlencode | ||||
|  | ||||
| from authentik.blueprints.tests import apply_blueprint | ||||
| from authentik.core.tests.utils import create_test_admin_user, create_test_flow | ||||
| from authentik.flows.markers import StageMarker | ||||
| from authentik.flows.models import FlowDesignation, FlowStageBinding, FlowToken | ||||
| @ -74,6 +75,7 @@ class TestEmailStage(FlowTestCase): | ||||
|         response = self.client.get(url) | ||||
|         self.assertEqual(response.status_code, 200) | ||||
|  | ||||
|     @apply_blueprint("system/interfaces.yaml") | ||||
|     @patch( | ||||
|         "authentik.stages.email.models.EmailStage.backend_class", | ||||
|         PropertyMock(return_value=EmailBackend), | ||||
| @ -123,6 +125,7 @@ class TestEmailStage(FlowTestCase): | ||||
|         with self.settings(EMAIL_HOST=host): | ||||
|             self.assertEqual(EmailStage(use_global_settings=True).backend.host, host) | ||||
|  | ||||
|     @apply_blueprint("system/interfaces.yaml") | ||||
|     def test_token(self): | ||||
|         """Test with token""" | ||||
|         # Make sure token exists | ||||
|  | ||||
| @ -26,8 +26,9 @@ from authentik.flows.models import FlowDesignation | ||||
| from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER | ||||
| from authentik.flows.stage import PLAN_CONTEXT_PENDING_USER_IDENTIFIER, ChallengeStageView | ||||
| from authentik.flows.views.executor import SESSION_KEY_APPLICATION_PRE, SESSION_KEY_GET | ||||
| from authentik.interfaces.models import InterfaceType | ||||
| from authentik.interfaces.views import reverse_interface | ||||
| from authentik.lib.utils.http import get_client_ip | ||||
| from authentik.lib.utils.urls import reverse_with_qs | ||||
| from authentik.sources.oauth.types.apple import AppleLoginChallenge | ||||
| from authentik.sources.plex.models import PlexAuthenticationChallenge | ||||
| from authentik.stages.identification.models import IdentificationStage | ||||
| @ -205,22 +206,25 @@ class IdentificationStageView(ChallengeStageView): | ||||
|         get_qs = self.request.session.get(SESSION_KEY_GET, self.request.GET) | ||||
|         # Check for related enrollment and recovery flow, add URL to view | ||||
|         if current_stage.enrollment_flow: | ||||
|             challenge.initial_data["enroll_url"] = reverse_with_qs( | ||||
|                 "authentik_core:if-flow", | ||||
|             challenge.initial_data["enroll_url"] = reverse_interface( | ||||
|                 self.request, | ||||
|                 InterfaceType.FLOW, | ||||
|                 query=get_qs, | ||||
|                 kwargs={"flow_slug": current_stage.enrollment_flow.slug}, | ||||
|                 flow_slug=current_stage.enrollment_flow.slug, | ||||
|             ) | ||||
|         if current_stage.recovery_flow: | ||||
|             challenge.initial_data["recovery_url"] = reverse_with_qs( | ||||
|                 "authentik_core:if-flow", | ||||
|             challenge.initial_data["recovery_url"] = reverse_interface( | ||||
|                 self.request, | ||||
|                 InterfaceType.FLOW, | ||||
|                 query=get_qs, | ||||
|                 kwargs={"flow_slug": current_stage.recovery_flow.slug}, | ||||
|                 flow_slug=current_stage.recovery_flow.slug, | ||||
|             ) | ||||
|         if current_stage.passwordless_flow: | ||||
|             challenge.initial_data["passwordless_url"] = reverse_with_qs( | ||||
|                 "authentik_core:if-flow", | ||||
|             challenge.initial_data["passwordless_url"] = reverse_interface( | ||||
|                 self.request, | ||||
|                 InterfaceType.FLOW, | ||||
|                 query=get_qs, | ||||
|                 kwargs={"flow_slug": current_stage.passwordless_flow.slug}, | ||||
|                 flow_slug=current_stage.passwordless_flow.slug, | ||||
|             ) | ||||
|  | ||||
|         # Check all enabled source, add them if they have a UI Login button. | ||||
|  | ||||
| @ -5,6 +5,8 @@ from authentik.core.tests.utils import create_test_admin_user, create_test_flow | ||||
| from authentik.flows.challenge import ChallengeTypes | ||||
| from authentik.flows.models import FlowDesignation, FlowStageBinding | ||||
| from authentik.flows.tests import FlowTestCase | ||||
| from authentik.interfaces.models import InterfaceType | ||||
| from authentik.interfaces.tests import reverse_interface | ||||
| from authentik.sources.oauth.models import OAuthSource | ||||
| from authentik.stages.identification.models import IdentificationStage, UserFields | ||||
| from authentik.stages.password import BACKEND_INBUILT | ||||
| @ -166,9 +168,9 @@ class TestIdentificationStage(FlowTestCase): | ||||
|             component="ak-stage-identification", | ||||
|             user_fields=["email"], | ||||
|             password_fields=False, | ||||
|             enroll_url=reverse( | ||||
|                 "authentik_core:if-flow", | ||||
|                 kwargs={"flow_slug": flow.slug}, | ||||
|             enroll_url=reverse_interface( | ||||
|                 InterfaceType.FLOW, | ||||
|                 flow_slug=flow.slug, | ||||
|             ), | ||||
|             show_source_labels=False, | ||||
|             primary_action="Log in", | ||||
| @ -204,9 +206,9 @@ class TestIdentificationStage(FlowTestCase): | ||||
|             component="ak-stage-identification", | ||||
|             user_fields=["email"], | ||||
|             password_fields=False, | ||||
|             recovery_url=reverse( | ||||
|                 "authentik_core:if-flow", | ||||
|                 kwargs={"flow_slug": flow.slug}, | ||||
|             recovery_url=reverse_interface( | ||||
|                 InterfaceType.FLOW, | ||||
|                 flow_slug=flow.slug, | ||||
|             ), | ||||
|             show_source_labels=False, | ||||
|             primary_action="Log in", | ||||
|  | ||||
| @ -5,7 +5,6 @@ from django.contrib.auth import _clean_credentials | ||||
| from django.contrib.auth.backends import BaseBackend | ||||
| from django.core.exceptions import PermissionDenied | ||||
| from django.http import HttpRequest, HttpResponse | ||||
| from django.urls import reverse | ||||
| from django.utils.translation import gettext as _ | ||||
| from rest_framework.exceptions import ErrorDetail, ValidationError | ||||
| from rest_framework.fields import CharField | ||||
| @ -23,6 +22,8 @@ from authentik.flows.challenge import ( | ||||
| from authentik.flows.models import Flow, FlowDesignation, Stage | ||||
| from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER | ||||
| from authentik.flows.stage import ChallengeStageView | ||||
| from authentik.interfaces.models import InterfaceType | ||||
| from authentik.interfaces.views import reverse_interface | ||||
| from authentik.lib.utils.reflection import path_to_class | ||||
| from authentik.stages.password.models import PasswordStage | ||||
|  | ||||
| @ -95,11 +96,12 @@ class PasswordStageView(ChallengeStageView): | ||||
|                 "type": ChallengeTypes.NATIVE.value, | ||||
|             } | ||||
|         ) | ||||
|         recovery_flow = Flow.objects.filter(designation=FlowDesignation.RECOVERY) | ||||
|         if recovery_flow.exists(): | ||||
|             recover_url = reverse( | ||||
|                 "authentik_core:if-flow", | ||||
|                 kwargs={"flow_slug": recovery_flow.first().slug}, | ||||
|         recovery_flow = Flow.objects.filter(designation=FlowDesignation.RECOVERY).first() | ||||
|         if recovery_flow: | ||||
|             recover_url = reverse_interface( | ||||
|                 self.request, | ||||
|                 InterfaceType.FLOW, | ||||
|                 flow_slug=recovery_flow.slug, | ||||
|             ) | ||||
|             challenge.initial_data["recovery_url"] = self.request.build_absolute_uri(recover_url) | ||||
|         return challenge | ||||
|  | ||||
| @ -18,6 +18,7 @@ from authentik.core.api.used_by import UsedByMixin | ||||
| from authentik.core.api.utils import PassiveSerializer | ||||
| from authentik.lib.config import CONFIG | ||||
| from authentik.tenants.models import Tenant | ||||
| from authentik.tenants.utils import get_tenant | ||||
|  | ||||
|  | ||||
| class FooterLinkSerializer(PassiveSerializer): | ||||
| @ -54,6 +55,9 @@ class TenantSerializer(ModelSerializer): | ||||
|             "flow_unenrollment", | ||||
|             "flow_user_settings", | ||||
|             "flow_device_code", | ||||
|             "interface_admin", | ||||
|             "interface_user", | ||||
|             "interface_flow", | ||||
|             "event_retention", | ||||
|             "web_certificate", | ||||
|             "attributes", | ||||
| @ -120,6 +124,9 @@ class TenantViewSet(UsedByMixin, ModelViewSet): | ||||
|         "flow_unenrollment", | ||||
|         "flow_user_settings", | ||||
|         "flow_device_code", | ||||
|         "interface_admin", | ||||
|         "interface_user", | ||||
|         "interface_flow", | ||||
|         "event_retention", | ||||
|         "web_certificate", | ||||
|     ] | ||||
| @ -133,5 +140,4 @@ class TenantViewSet(UsedByMixin, ModelViewSet): | ||||
|     @action(methods=["GET"], detail=False, permission_classes=[AllowAny]) | ||||
|     def current(self, request: Request) -> Response: | ||||
|         """Get current tenant""" | ||||
|         tenant: Tenant = request._request.tenant | ||||
|         return Response(CurrentTenantSerializer(tenant).data) | ||||
|         return Response(CurrentTenantSerializer(get_tenant(request)).data) | ||||
|  | ||||
| @ -6,7 +6,7 @@ from django.http.response import HttpResponse | ||||
| from django.utils.translation import activate | ||||
| from sentry_sdk.api import set_tag | ||||
|  | ||||
| from authentik.tenants.utils import get_tenant_for_request | ||||
| from authentik.tenants.utils import lookup_tenant_for_request | ||||
|  | ||||
|  | ||||
| class TenantMiddleware: | ||||
| @ -19,7 +19,7 @@ class TenantMiddleware: | ||||
|  | ||||
|     def __call__(self, request: HttpRequest) -> HttpResponse: | ||||
|         if not hasattr(request, "tenant"): | ||||
|             tenant = get_tenant_for_request(request) | ||||
|             tenant = lookup_tenant_for_request(request) | ||||
|             setattr(request, "tenant", tenant) | ||||
|             set_tag("authentik.tenant_uuid", tenant.tenant_uuid.hex) | ||||
|             set_tag("authentik.tenant_domain", tenant.domain) | ||||
|  | ||||
| @ -0,0 +1,78 @@ | ||||
| # Generated by Django 4.1.7 on 2023-02-21 14:18 | ||||
|  | ||||
| import django.db.models.deletion | ||||
| from django.apps.registry import Apps | ||||
| from django.db import migrations, models | ||||
| from django.db.backends.base.schema import BaseDatabaseSchemaEditor | ||||
|  | ||||
|  | ||||
| def migrate_set_default(apps: Apps, schema_editor: BaseDatabaseSchemaEditor): | ||||
|     Tenant = apps.get_model("authentik_tenants", "tenant") | ||||
|     Interface = apps.get_model("authentik_interfaces", "Interface") | ||||
|     db_alias = schema_editor.connection.alias | ||||
|  | ||||
|     from authentik.blueprints.models import BlueprintInstance | ||||
|     from authentik.blueprints.v1.importer import Importer | ||||
|     from authentik.blueprints.v1.tasks import blueprints_discovery | ||||
|     from authentik.interfaces.models import InterfaceType | ||||
|  | ||||
|     # If we don't have any tenants yet, we don't need wait for the default interface blueprint | ||||
|     if not Tenant.objects.using(db_alias).exists(): | ||||
|         return | ||||
|  | ||||
|     interface_blueprint = BlueprintInstance.objects.filter(path="system/interfaces.yaml").first() | ||||
|     if not interface_blueprint: | ||||
|         blueprints_discovery.delay().get() | ||||
|         interface_blueprint = BlueprintInstance.objects.filter( | ||||
|             path="system/interfaces.yaml" | ||||
|         ).first() | ||||
|     if not interface_blueprint: | ||||
|         raise ValueError("Failed to apply system/interfaces.yaml blueprint") | ||||
|     Importer(interface_blueprint.retrieve()).apply() | ||||
|  | ||||
|     for tenant in Tenant.objects.using(db_alias).all(): | ||||
|         tenant.interface_admin = Interface.objects.filter(type=InterfaceType.ADMIN).first() | ||||
|         tenant.interface_user = Interface.objects.filter(type=InterfaceType.USER).first() | ||||
|         tenant.interface_flow = Interface.objects.filter(type=InterfaceType.FLOW).first() | ||||
|         tenant.save() | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|     dependencies = [ | ||||
|         ("authentik_interfaces", "0001_initial"), | ||||
|         ("authentik_tenants", "0004_tenant_flow_device_code"), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AddField( | ||||
|             model_name="tenant", | ||||
|             name="interface_admin", | ||||
|             field=models.ForeignKey( | ||||
|                 null=True, | ||||
|                 on_delete=django.db.models.deletion.SET_NULL, | ||||
|                 related_name="tenant_admin", | ||||
|                 to="authentik_interfaces.interface", | ||||
|             ), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name="tenant", | ||||
|             name="interface_flow", | ||||
|             field=models.ForeignKey( | ||||
|                 null=True, | ||||
|                 on_delete=django.db.models.deletion.SET_NULL, | ||||
|                 related_name="tenant_flow", | ||||
|                 to="authentik_interfaces.interface", | ||||
|             ), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name="tenant", | ||||
|             name="interface_user", | ||||
|             field=models.ForeignKey( | ||||
|                 null=True, | ||||
|                 on_delete=django.db.models.deletion.SET_NULL, | ||||
|                 related_name="tenant_user", | ||||
|                 to="authentik_interfaces.interface", | ||||
|             ), | ||||
|         ), | ||||
|         migrations.RunPython(migrate_set_default), | ||||
|     ] | ||||
| @ -7,7 +7,6 @@ from rest_framework.serializers import Serializer | ||||
| from structlog.stdlib import get_logger | ||||
|  | ||||
| from authentik.crypto.models import CertificateKeyPair | ||||
| from authentik.flows.models import Flow | ||||
| from authentik.lib.models import SerializerModel | ||||
| from authentik.lib.utils.time import timedelta_string_validator | ||||
|  | ||||
| @ -33,22 +32,59 @@ class Tenant(SerializerModel): | ||||
|     branding_favicon = models.TextField(default="/static/dist/assets/icons/icon.png") | ||||
|  | ||||
|     flow_authentication = models.ForeignKey( | ||||
|         Flow, null=True, on_delete=models.SET_NULL, related_name="tenant_authentication" | ||||
|         "authentik_flows.Flow", | ||||
|         null=True, | ||||
|         on_delete=models.SET_NULL, | ||||
|         related_name="tenant_authentication", | ||||
|     ) | ||||
|     flow_invalidation = models.ForeignKey( | ||||
|         Flow, null=True, on_delete=models.SET_NULL, related_name="tenant_invalidation" | ||||
|         "authentik_flows.Flow", | ||||
|         null=True, | ||||
|         on_delete=models.SET_NULL, | ||||
|         related_name="tenant_invalidation", | ||||
|     ) | ||||
|     flow_recovery = models.ForeignKey( | ||||
|         Flow, null=True, on_delete=models.SET_NULL, related_name="tenant_recovery" | ||||
|         "authentik_flows.Flow", null=True, on_delete=models.SET_NULL, related_name="tenant_recovery" | ||||
|     ) | ||||
|     flow_unenrollment = models.ForeignKey( | ||||
|         Flow, null=True, on_delete=models.SET_NULL, related_name="tenant_unenrollment" | ||||
|         "authentik_flows.Flow", | ||||
|         null=True, | ||||
|         on_delete=models.SET_NULL, | ||||
|         related_name="tenant_unenrollment", | ||||
|     ) | ||||
|     flow_user_settings = models.ForeignKey( | ||||
|         Flow, null=True, on_delete=models.SET_NULL, related_name="tenant_user_settings" | ||||
|         "authentik_flows.Flow", | ||||
|         null=True, | ||||
|         on_delete=models.SET_NULL, | ||||
|         related_name="tenant_user_settings", | ||||
|     ) | ||||
|     flow_device_code = models.ForeignKey( | ||||
|         Flow, null=True, on_delete=models.SET_NULL, related_name="tenant_device_code" | ||||
|         "authentik_flows.Flow", | ||||
|         null=True, | ||||
|         on_delete=models.SET_NULL, | ||||
|         related_name="tenant_device_code", | ||||
|     ) | ||||
|  | ||||
|     interface_flow = models.ForeignKey( | ||||
|         "authentik_interfaces.Interface", | ||||
|         default=None, | ||||
|         on_delete=models.SET_NULL, | ||||
|         null=True, | ||||
|         related_name="tenant_flow", | ||||
|     ) | ||||
|     interface_user = models.ForeignKey( | ||||
|         "authentik_interfaces.Interface", | ||||
|         default=None, | ||||
|         on_delete=models.SET_NULL, | ||||
|         null=True, | ||||
|         related_name="tenant_user", | ||||
|     ) | ||||
|     interface_admin = models.ForeignKey( | ||||
|         "authentik_interfaces.Interface", | ||||
|         default=None, | ||||
|         on_delete=models.SET_NULL, | ||||
|         null=True, | ||||
|         related_name="tenant_admin", | ||||
|     ) | ||||
|  | ||||
|     event_retention = models.TextField( | ||||
|  | ||||
| @ -75,7 +75,7 @@ class TestTenants(APITestCase): | ||||
|         ) | ||||
|         factory = RequestFactory() | ||||
|         request = factory.get("/") | ||||
|         request.tenant = tenant | ||||
|         setattr(request, "tenant", tenant) | ||||
|         event = Event.new(action=EventAction.SYSTEM_EXCEPTION, message="test").from_http(request) | ||||
|         self.assertEqual(event.expires.day, (event.created + timedelta_from_string("weeks=3")).day) | ||||
|         self.assertEqual( | ||||
|  | ||||
| @ -4,17 +4,41 @@ from typing import Any | ||||
| from django.db.models import F, Q | ||||
| from django.db.models import Value as V | ||||
| from django.http.request import HttpRequest | ||||
| from rest_framework.request import Request | ||||
| from sentry_sdk.hub import Hub | ||||
|  | ||||
| from authentik import get_full_version | ||||
| from authentik.interfaces.models import Interface, InterfaceType | ||||
| from authentik.lib.config import CONFIG | ||||
| from authentik.tenants.models import Tenant | ||||
|  | ||||
| _q_default = Q(default=True) | ||||
| DEFAULT_TENANT = Tenant(domain="fallback") | ||||
|  | ||||
|  | ||||
| def get_tenant_for_request(request: HttpRequest) -> Tenant: | ||||
| def get_fallback_tenant(): | ||||
|     """Get fallback tenant""" | ||||
|  | ||||
|     fallback_interface = Interface( | ||||
|         url_name="fallback", | ||||
|         type=InterfaceType.FLOW, | ||||
|         template="Fallback interface", | ||||
|     ) | ||||
|     return Tenant( | ||||
|         domain="fallback", | ||||
|         interface_flow=fallback_interface, | ||||
|         interface_user=fallback_interface, | ||||
|         interface_admin=fallback_interface, | ||||
|     ) | ||||
|  | ||||
|  | ||||
| def get_tenant(request: HttpRequest | Request) -> "Tenant": | ||||
|     """Get the request's tenant, falls back to a fallback tenant object""" | ||||
|     if isinstance(request, Request): | ||||
|         request = request._request | ||||
|     return getattr(request, "tenant", get_fallback_tenant()) | ||||
|  | ||||
|  | ||||
| def lookup_tenant_for_request(request: HttpRequest) -> "Tenant": | ||||
|     """Get tenant object for current request""" | ||||
|     db_tenants = ( | ||||
|         Tenant.objects.annotate(host_domain=V(request.get_host())) | ||||
| @ -23,13 +47,13 @@ def get_tenant_for_request(request: HttpRequest) -> Tenant: | ||||
|     ) | ||||
|     tenants = list(db_tenants.all()) | ||||
|     if len(tenants) < 1: | ||||
|         return DEFAULT_TENANT | ||||
|         return get_fallback_tenant() | ||||
|     return tenants[0] | ||||
|  | ||||
|  | ||||
| def context_processor(request: HttpRequest) -> dict[str, Any]: | ||||
|     """Context Processor that injects tenant object into every template""" | ||||
|     tenant = getattr(request, "tenant", DEFAULT_TENANT) | ||||
|     tenant = getattr(request, "tenant", get_fallback_tenant()) | ||||
|     trace = "" | ||||
|     span = Hub.current.scope.span | ||||
|     if span: | ||||
|  | ||||
| @ -2,6 +2,11 @@ metadata: | ||||
|   name: Default - Tenant | ||||
| version: 1 | ||||
| entries: | ||||
| - model: authentik_blueprints.metaapplyblueprint | ||||
|   attrs: | ||||
|     identifiers: | ||||
|       name: System - Interfaces | ||||
|     required: false | ||||
| - model: authentik_blueprints.metaapplyblueprint | ||||
|   attrs: | ||||
|     identifiers: | ||||
| @ -21,6 +26,9 @@ entries: | ||||
|     flow_authentication: !Find [authentik_flows.flow, [slug, default-authentication-flow]] | ||||
|     flow_invalidation: !Find [authentik_flows.flow, [slug, default-invalidation-flow]] | ||||
|     flow_user_settings: !Find [authentik_flows.flow, [slug, default-user-settings-flow]] | ||||
|     interface_admin: !Find [authentik_interfaces.Interface, [type, admin]] | ||||
|     interface_user: !Find [authentik_interfaces.Interface, [type, user]] | ||||
|     interface_flow: !Find [authentik_interfaces.Interface, [type, flow]] | ||||
|   identifiers: | ||||
|     domain: authentik-default | ||||
|     default: True | ||||
|  | ||||
| @ -61,6 +61,7 @@ | ||||
|                             "authentik_events.notificationwebhookmapping", | ||||
|                             "authentik_flows.flow", | ||||
|                             "authentik_flows.flowstagebinding", | ||||
|                             "authentik_interfaces.interface", | ||||
|                             "authentik_outposts.dockerserviceconnection", | ||||
|                             "authentik_outposts.kubernetesserviceconnection", | ||||
|                             "authentik_outposts.outpost", | ||||
|  | ||||
							
								
								
									
										139
									
								
								blueprints/system/interfaces.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										139
									
								
								blueprints/system/interfaces.yaml
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,139 @@ | ||||
| version: 1 | ||||
| metadata: | ||||
|   labels: | ||||
|     blueprints.goauthentik.io/system: "true" | ||||
|   name: System - Interfaces | ||||
| entries: | ||||
|   - model: authentik_interfaces.interface | ||||
|     identifiers: | ||||
|       url_name: user | ||||
|       type: user | ||||
|     attrs: | ||||
|       template: | | ||||
|         {% extends "base/skeleton.html" %} | ||||
|  | ||||
|         {% load static %} | ||||
|         {% load i18n %} | ||||
|  | ||||
|         {% block head %} | ||||
|         <script src="{% static 'dist/user/UserInterface.js' %}?version={{ version }}" type="module"></script> | ||||
|         <meta name="theme-color" content="#151515" media="(prefers-color-scheme: light)"> | ||||
|         <meta name="theme-color" content="#151515" media="(prefers-color-scheme: dark)"> | ||||
|         <link rel="icon" href="{{ tenant.branding_favicon }}"> | ||||
|         <link rel="shortcut icon" href="{{ tenant.branding_favicon }}"> | ||||
|         {% include "base/header_js.html" %} | ||||
|         {% endblock %} | ||||
|  | ||||
|         {% block body %} | ||||
|         <ak-message-container></ak-message-container> | ||||
|         <ak-interface-user> | ||||
|             <section class="ak-static-page pf-c-page__main-section pf-m-no-padding-mobile pf-m-xl"> | ||||
|                 <div class="pf-c-empty-state" style="height: 100vh;"> | ||||
|                     <div class="pf-c-empty-state__content"> | ||||
|                         <span class="pf-c-spinner pf-m-xl pf-c-empty-state__icon" role="progressbar" aria-valuetext="{% trans 'Loading...' %}"> | ||||
|                             <span class="pf-c-spinner__clipper"></span> | ||||
|                             <span class="pf-c-spinner__lead-ball"></span> | ||||
|                             <span class="pf-c-spinner__tail-ball"></span> | ||||
|                         </span> | ||||
|                         <h1 class="pf-c-title pf-m-lg"> | ||||
|                             {% trans "Loading..." %} | ||||
|                         </h1> | ||||
|                     </div> | ||||
|                 </div> | ||||
|             </section> | ||||
|         </ak-interface-user> | ||||
|         {% endblock %} | ||||
|   - model: authentik_interfaces.interface | ||||
|     identifiers: | ||||
|       url_name: admin | ||||
|       type: admin | ||||
|     attrs: | ||||
|       template: | | ||||
|         {% extends "base/skeleton.html" %} | ||||
|  | ||||
|         {% load static %} | ||||
|         {% load i18n %} | ||||
|  | ||||
|         {% block head %} | ||||
|         <script src="{% static 'dist/admin/AdminInterface.js' %}?version={{ version }}" type="module"></script> | ||||
|         <meta name="theme-color" content="#18191a" media="(prefers-color-scheme: dark)"> | ||||
|         <meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)"> | ||||
|         <link rel="icon" href="{{ tenant.branding_favicon }}"> | ||||
|         <link rel="shortcut icon" href="{{ tenant.branding_favicon }}"> | ||||
|         {% include "base/header_js.html" %} | ||||
|         {% endblock %} | ||||
|  | ||||
|         {% block body %} | ||||
|         <ak-message-container></ak-message-container> | ||||
|         <ak-interface-admin> | ||||
|             <section class="ak-static-page pf-c-page__main-section pf-m-no-padding-mobile pf-m-xl"> | ||||
|                 <div class="pf-c-empty-state" style="height: 100vh;"> | ||||
|                     <div class="pf-c-empty-state__content"> | ||||
|                         <span class="pf-c-spinner pf-m-xl pf-c-empty-state__icon" role="progressbar" aria-valuetext="{% trans 'Loading...' %}"> | ||||
|                             <span class="pf-c-spinner__clipper"></span> | ||||
|                             <span class="pf-c-spinner__lead-ball"></span> | ||||
|                             <span class="pf-c-spinner__tail-ball"></span> | ||||
|                         </span> | ||||
|                         <h1 class="pf-c-title pf-m-lg"> | ||||
|                             {% trans "Loading..." %} | ||||
|                         </h1> | ||||
|                     </div> | ||||
|                 </div> | ||||
|             </section> | ||||
|         </ak-interface-admin> | ||||
|         {% endblock %} | ||||
|   - model: authentik_interfaces.interface | ||||
|     identifiers: | ||||
|       url_name: flow | ||||
|       type: flow | ||||
|     attrs: | ||||
|       template: | | ||||
|         {% extends "base/skeleton.html" %} | ||||
|  | ||||
|         {% load static %} | ||||
|         {% load i18n %} | ||||
|  | ||||
|         {% block head_before %} | ||||
|         {{ block.super }} | ||||
|         <link rel="prefetch" href="{{ flow.background_url }}" /> | ||||
|         <link rel="icon" href="{{ tenant.branding_favicon }}"> | ||||
|         <link rel="shortcut icon" href="{{ tenant.branding_favicon }}"> | ||||
|         {% if flow.compatibility_mode and not inspector %} | ||||
|         <script>ShadyDOM = { force: !navigator.webdriver };</script> | ||||
|         {% endif %} | ||||
|         {% include "base/header_js.html" %} | ||||
|         <script> | ||||
|         window.authentik.flow = { | ||||
|             "layout": "{{ flow.layout }}", | ||||
|         }; | ||||
|         </script> | ||||
|         {% endblock %} | ||||
|  | ||||
|         {% block head %} | ||||
|         <script src="{% static 'dist/flow/FlowInterface.js' %}?version={{ version }}" type="module"></script> | ||||
|         <style> | ||||
|         :root { | ||||
|             --ak-flow-background: url("{{ flow.background_url }}"); | ||||
|         } | ||||
|         </style> | ||||
|         {% endblock %} | ||||
|  | ||||
|         {% block body %} | ||||
|         <ak-message-container></ak-message-container> | ||||
|         <ak-flow-executor> | ||||
|             <section class="ak-static-page pf-c-page__main-section pf-m-no-padding-mobile pf-m-xl"> | ||||
|                 <div class="pf-c-empty-state" style="height: 100vh;"> | ||||
|                     <div class="pf-c-empty-state__content"> | ||||
|                         <span class="pf-c-spinner pf-m-xl pf-c-empty-state__icon" role="progressbar" aria-valuetext="{% trans 'Loading...' %}"> | ||||
|                             <span class="pf-c-spinner__clipper"></span> | ||||
|                             <span class="pf-c-spinner__lead-ball"></span> | ||||
|                             <span class="pf-c-spinner__tail-ball"></span> | ||||
|                         </span> | ||||
|                         <h1 class="pf-c-title pf-m-lg"> | ||||
|                             {% trans "Loading..." %} | ||||
|                         </h1> | ||||
|                     </div> | ||||
|                 </div> | ||||
|             </section> | ||||
|         </ak-flow-executor> | ||||
|         {% endblock %} | ||||
							
								
								
									
										446
									
								
								schema.yml
									
									
									
									
									
								
							
							
						
						
									
										446
									
								
								schema.yml
									
									
									
									
									
								
							| @ -3676,6 +3676,24 @@ paths: | ||||
|         description: flow_user_settings | ||||
|         schema: | ||||
|           type: string | ||||
|       - name: interface_admin | ||||
|         required: false | ||||
|         in: query | ||||
|         description: interface_admin | ||||
|         schema: | ||||
|           type: string | ||||
|       - name: interface_flow | ||||
|         required: false | ||||
|         in: query | ||||
|         description: interface_flow | ||||
|         schema: | ||||
|           type: string | ||||
|       - name: interface_user | ||||
|         required: false | ||||
|         in: query | ||||
|         description: interface_user | ||||
|         schema: | ||||
|           type: string | ||||
|       - name: ordering | ||||
|         required: false | ||||
|         in: query | ||||
| @ -7731,6 +7749,295 @@ paths: | ||||
|               schema: | ||||
|                 $ref: '#/components/schemas/GenericError' | ||||
|           description: '' | ||||
|   /interfaces/: | ||||
|     get: | ||||
|       operationId: interfaces_list | ||||
|       description: Interface serializer | ||||
|       parameters: | ||||
|       - name: ordering | ||||
|         required: false | ||||
|         in: query | ||||
|         description: Which field to use when ordering the results. | ||||
|         schema: | ||||
|           type: string | ||||
|       - name: page | ||||
|         required: false | ||||
|         in: query | ||||
|         description: A page number within the paginated result set. | ||||
|         schema: | ||||
|           type: integer | ||||
|       - name: page_size | ||||
|         required: false | ||||
|         in: query | ||||
|         description: Number of results to return per page. | ||||
|         schema: | ||||
|           type: integer | ||||
|       - name: search | ||||
|         required: false | ||||
|         in: query | ||||
|         description: A search term. | ||||
|         schema: | ||||
|           type: string | ||||
|       - in: query | ||||
|         name: template | ||||
|         schema: | ||||
|           type: string | ||||
|       - in: query | ||||
|         name: type | ||||
|         schema: | ||||
|           type: string | ||||
|           enum: | ||||
|           - admin | ||||
|           - flow | ||||
|           - user | ||||
|         description: |- | ||||
|           * `user` - User | ||||
|           * `admin` - Admin | ||||
|           * `flow` - Flow | ||||
| 
 | ||||
|           * `user` - User | ||||
|           * `admin` - Admin | ||||
|           * `flow` - Flow | ||||
|       - in: query | ||||
|         name: url_name | ||||
|         schema: | ||||
|           type: string | ||||
|       tags: | ||||
|       - interfaces | ||||
|       security: | ||||
|       - authentik: [] | ||||
|       responses: | ||||
|         '200': | ||||
|           content: | ||||
|             application/json: | ||||
|               schema: | ||||
|                 $ref: '#/components/schemas/PaginatedInterfaceList' | ||||
|           description: '' | ||||
|         '400': | ||||
|           content: | ||||
|             application/json: | ||||
|               schema: | ||||
|                 $ref: '#/components/schemas/ValidationError' | ||||
|           description: '' | ||||
|         '403': | ||||
|           content: | ||||
|             application/json: | ||||
|               schema: | ||||
|                 $ref: '#/components/schemas/GenericError' | ||||
|           description: '' | ||||
|     post: | ||||
|       operationId: interfaces_create | ||||
|       description: Interface serializer | ||||
|       tags: | ||||
|       - interfaces | ||||
|       requestBody: | ||||
|         content: | ||||
|           application/json: | ||||
|             schema: | ||||
|               $ref: '#/components/schemas/InterfaceRequest' | ||||
|         required: true | ||||
|       security: | ||||
|       - authentik: [] | ||||
|       responses: | ||||
|         '201': | ||||
|           content: | ||||
|             application/json: | ||||
|               schema: | ||||
|                 $ref: '#/components/schemas/Interface' | ||||
|           description: '' | ||||
|         '400': | ||||
|           content: | ||||
|             application/json: | ||||
|               schema: | ||||
|                 $ref: '#/components/schemas/ValidationError' | ||||
|           description: '' | ||||
|         '403': | ||||
|           content: | ||||
|             application/json: | ||||
|               schema: | ||||
|                 $ref: '#/components/schemas/GenericError' | ||||
|           description: '' | ||||
|   /interfaces/{interface_uuid}/: | ||||
|     get: | ||||
|       operationId: interfaces_retrieve | ||||
|       description: Interface serializer | ||||
|       parameters: | ||||
|       - in: path | ||||
|         name: interface_uuid | ||||
|         schema: | ||||
|           type: string | ||||
|           format: uuid | ||||
|         description: A UUID string identifying this interface. | ||||
|         required: true | ||||
|       tags: | ||||
|       - interfaces | ||||
|       security: | ||||
|       - authentik: [] | ||||
|       responses: | ||||
|         '200': | ||||
|           content: | ||||
|             application/json: | ||||
|               schema: | ||||
|                 $ref: '#/components/schemas/Interface' | ||||
|           description: '' | ||||
|         '400': | ||||
|           content: | ||||
|             application/json: | ||||
|               schema: | ||||
|                 $ref: '#/components/schemas/ValidationError' | ||||
|           description: '' | ||||
|         '403': | ||||
|           content: | ||||
|             application/json: | ||||
|               schema: | ||||
|                 $ref: '#/components/schemas/GenericError' | ||||
|           description: '' | ||||
|     put: | ||||
|       operationId: interfaces_update | ||||
|       description: Interface serializer | ||||
|       parameters: | ||||
|       - in: path | ||||
|         name: interface_uuid | ||||
|         schema: | ||||
|           type: string | ||||
|           format: uuid | ||||
|         description: A UUID string identifying this interface. | ||||
|         required: true | ||||
|       tags: | ||||
|       - interfaces | ||||
|       requestBody: | ||||
|         content: | ||||
|           application/json: | ||||
|             schema: | ||||
|               $ref: '#/components/schemas/InterfaceRequest' | ||||
|         required: true | ||||
|       security: | ||||
|       - authentik: [] | ||||
|       responses: | ||||
|         '200': | ||||
|           content: | ||||
|             application/json: | ||||
|               schema: | ||||
|                 $ref: '#/components/schemas/Interface' | ||||
|           description: '' | ||||
|         '400': | ||||
|           content: | ||||
|             application/json: | ||||
|               schema: | ||||
|                 $ref: '#/components/schemas/ValidationError' | ||||
|           description: '' | ||||
|         '403': | ||||
|           content: | ||||
|             application/json: | ||||
|               schema: | ||||
|                 $ref: '#/components/schemas/GenericError' | ||||
|           description: '' | ||||
|     patch: | ||||
|       operationId: interfaces_partial_update | ||||
|       description: Interface serializer | ||||
|       parameters: | ||||
|       - in: path | ||||
|         name: interface_uuid | ||||
|         schema: | ||||
|           type: string | ||||
|           format: uuid | ||||
|         description: A UUID string identifying this interface. | ||||
|         required: true | ||||
|       tags: | ||||
|       - interfaces | ||||
|       requestBody: | ||||
|         content: | ||||
|           application/json: | ||||
|             schema: | ||||
|               $ref: '#/components/schemas/PatchedInterfaceRequest' | ||||
|       security: | ||||
|       - authentik: [] | ||||
|       responses: | ||||
|         '200': | ||||
|           content: | ||||
|             application/json: | ||||
|               schema: | ||||
|                 $ref: '#/components/schemas/Interface' | ||||
|           description: '' | ||||
|         '400': | ||||
|           content: | ||||
|             application/json: | ||||
|               schema: | ||||
|                 $ref: '#/components/schemas/ValidationError' | ||||
|           description: '' | ||||
|         '403': | ||||
|           content: | ||||
|             application/json: | ||||
|               schema: | ||||
|                 $ref: '#/components/schemas/GenericError' | ||||
|           description: '' | ||||
|     delete: | ||||
|       operationId: interfaces_destroy | ||||
|       description: Interface serializer | ||||
|       parameters: | ||||
|       - in: path | ||||
|         name: interface_uuid | ||||
|         schema: | ||||
|           type: string | ||||
|           format: uuid | ||||
|         description: A UUID string identifying this interface. | ||||
|         required: true | ||||
|       tags: | ||||
|       - interfaces | ||||
|       security: | ||||
|       - authentik: [] | ||||
|       responses: | ||||
|         '204': | ||||
|           description: No response body | ||||
|         '400': | ||||
|           content: | ||||
|             application/json: | ||||
|               schema: | ||||
|                 $ref: '#/components/schemas/ValidationError' | ||||
|           description: '' | ||||
|         '403': | ||||
|           content: | ||||
|             application/json: | ||||
|               schema: | ||||
|                 $ref: '#/components/schemas/GenericError' | ||||
|           description: '' | ||||
|   /interfaces/{interface_uuid}/used_by/: | ||||
|     get: | ||||
|       operationId: interfaces_used_by_list | ||||
|       description: Get a list of all objects that use this object | ||||
|       parameters: | ||||
|       - in: path | ||||
|         name: interface_uuid | ||||
|         schema: | ||||
|           type: string | ||||
|           format: uuid | ||||
|         description: A UUID string identifying this interface. | ||||
|         required: true | ||||
|       tags: | ||||
|       - interfaces | ||||
|       security: | ||||
|       - authentik: [] | ||||
|       responses: | ||||
|         '200': | ||||
|           content: | ||||
|             application/json: | ||||
|               schema: | ||||
|                 type: array | ||||
|                 items: | ||||
|                   $ref: '#/components/schemas/UsedBy' | ||||
|           description: '' | ||||
|         '400': | ||||
|           content: | ||||
|             application/json: | ||||
|               schema: | ||||
|                 $ref: '#/components/schemas/ValidationError' | ||||
|           description: '' | ||||
|         '403': | ||||
|           content: | ||||
|             application/json: | ||||
|               schema: | ||||
|                 $ref: '#/components/schemas/GenericError' | ||||
|           description: '' | ||||
|   /managed/blueprints/: | ||||
|     get: | ||||
|       operationId: managed_blueprints_list | ||||
| @ -26307,6 +26614,7 @@ components: | ||||
|       - authentik.admin | ||||
|       - authentik.api | ||||
|       - authentik.crypto | ||||
|       - authentik.interfaces | ||||
|       - authentik.events | ||||
|       - authentik.flows | ||||
|       - authentik.lib | ||||
| @ -26357,6 +26665,7 @@ components: | ||||
|         * `authentik.admin` - authentik Admin | ||||
|         * `authentik.api` - authentik API | ||||
|         * `authentik.crypto` - authentik Crypto | ||||
|         * `authentik.interfaces` - authentik Interfaces | ||||
|         * `authentik.events` - authentik Events | ||||
|         * `authentik.flows` - authentik Flows | ||||
|         * `authentik.lib` - authentik lib | ||||
| @ -29074,6 +29383,7 @@ components: | ||||
|             * `authentik.admin` - authentik Admin | ||||
|             * `authentik.api` - authentik API | ||||
|             * `authentik.crypto` - authentik Crypto | ||||
|             * `authentik.interfaces` - authentik Interfaces | ||||
|             * `authentik.events` - authentik Events | ||||
|             * `authentik.flows` - authentik Flows | ||||
|             * `authentik.lib` - authentik lib | ||||
| @ -29184,6 +29494,7 @@ components: | ||||
|             * `authentik.admin` - authentik Admin | ||||
|             * `authentik.api` - authentik API | ||||
|             * `authentik.crypto` - authentik Crypto | ||||
|             * `authentik.interfaces` - authentik Interfaces | ||||
|             * `authentik.events` - authentik Events | ||||
|             * `authentik.flows` - authentik Flows | ||||
|             * `authentik.lib` - authentik lib | ||||
| @ -30297,6 +30608,55 @@ components: | ||||
|         * `api` - Intent Api | ||||
|         * `recovery` - Intent Recovery | ||||
|         * `app_password` - Intent App Password | ||||
|     Interface: | ||||
|       type: object | ||||
|       description: Interface serializer | ||||
|       properties: | ||||
|         interface_uuid: | ||||
|           type: string | ||||
|           format: uuid | ||||
|           readOnly: true | ||||
|         url_name: | ||||
|           type: string | ||||
|           maxLength: 50 | ||||
|           pattern: ^[-a-zA-Z0-9_]+$ | ||||
|         type: | ||||
|           $ref: '#/components/schemas/InterfaceTypeEnum' | ||||
|         template: | ||||
|           type: string | ||||
|       required: | ||||
|       - interface_uuid | ||||
|       - template | ||||
|       - type | ||||
|       - url_name | ||||
|     InterfaceRequest: | ||||
|       type: object | ||||
|       description: Interface serializer | ||||
|       properties: | ||||
|         url_name: | ||||
|           type: string | ||||
|           minLength: 1 | ||||
|           maxLength: 50 | ||||
|           pattern: ^[-a-zA-Z0-9_]+$ | ||||
|         type: | ||||
|           $ref: '#/components/schemas/InterfaceTypeEnum' | ||||
|         template: | ||||
|           type: string | ||||
|           minLength: 1 | ||||
|       required: | ||||
|       - template | ||||
|       - type | ||||
|       - url_name | ||||
|     InterfaceTypeEnum: | ||||
|       enum: | ||||
|       - user | ||||
|       - admin | ||||
|       - flow | ||||
|       type: string | ||||
|       description: |- | ||||
|         * `user` - User | ||||
|         * `admin` - Admin | ||||
|         * `flow` - Flow | ||||
|     InvalidResponseActionEnum: | ||||
|       enum: | ||||
|       - retry | ||||
| @ -33051,6 +33411,41 @@ components: | ||||
|       required: | ||||
|       - pagination | ||||
|       - results | ||||
|     PaginatedInterfaceList: | ||||
|       type: object | ||||
|       properties: | ||||
|         pagination: | ||||
|           type: object | ||||
|           properties: | ||||
|             next: | ||||
|               type: number | ||||
|             previous: | ||||
|               type: number | ||||
|             count: | ||||
|               type: number | ||||
|             current: | ||||
|               type: number | ||||
|             total_pages: | ||||
|               type: number | ||||
|             start_index: | ||||
|               type: number | ||||
|             end_index: | ||||
|               type: number | ||||
|           required: | ||||
|           - next | ||||
|           - previous | ||||
|           - count | ||||
|           - current | ||||
|           - total_pages | ||||
|           - start_index | ||||
|           - end_index | ||||
|         results: | ||||
|           type: array | ||||
|           items: | ||||
|             $ref: '#/components/schemas/Interface' | ||||
|       required: | ||||
|       - pagination | ||||
|       - results | ||||
|     PaginatedInvitationList: | ||||
|       type: object | ||||
|       properties: | ||||
| @ -35870,6 +36265,7 @@ components: | ||||
|             * `authentik.admin` - authentik Admin | ||||
|             * `authentik.api` - authentik API | ||||
|             * `authentik.crypto` - authentik Crypto | ||||
|             * `authentik.interfaces` - authentik Interfaces | ||||
|             * `authentik.events` - authentik Events | ||||
|             * `authentik.flows` - authentik Flows | ||||
|             * `authentik.lib` - authentik lib | ||||
| @ -36121,6 +36517,20 @@ components: | ||||
|           description: Specify which sources should be shown. | ||||
|         show_source_labels: | ||||
|           type: boolean | ||||
|     PatchedInterfaceRequest: | ||||
|       type: object | ||||
|       description: Interface serializer | ||||
|       properties: | ||||
|         url_name: | ||||
|           type: string | ||||
|           minLength: 1 | ||||
|           maxLength: 50 | ||||
|           pattern: ^[-a-zA-Z0-9_]+$ | ||||
|         type: | ||||
|           $ref: '#/components/schemas/InterfaceTypeEnum' | ||||
|         template: | ||||
|           type: string | ||||
|           minLength: 1 | ||||
|     PatchedInvitationRequest: | ||||
|       type: object | ||||
|       description: Invitation Serializer | ||||
| @ -37405,6 +37815,18 @@ components: | ||||
|           type: string | ||||
|           format: uuid | ||||
|           nullable: true | ||||
|         interface_admin: | ||||
|           type: string | ||||
|           format: uuid | ||||
|           nullable: true | ||||
|         interface_user: | ||||
|           type: string | ||||
|           format: uuid | ||||
|           nullable: true | ||||
|         interface_flow: | ||||
|           type: string | ||||
|           format: uuid | ||||
|           nullable: true | ||||
|         event_retention: | ||||
|           type: string | ||||
|           minLength: 1 | ||||
| @ -40576,6 +40998,18 @@ components: | ||||
|           type: string | ||||
|           format: uuid | ||||
|           nullable: true | ||||
|         interface_admin: | ||||
|           type: string | ||||
|           format: uuid | ||||
|           nullable: true | ||||
|         interface_user: | ||||
|           type: string | ||||
|           format: uuid | ||||
|           nullable: true | ||||
|         interface_flow: | ||||
|           type: string | ||||
|           format: uuid | ||||
|           nullable: true | ||||
|         event_retention: | ||||
|           type: string | ||||
|           description: 'Events will be deleted after this duration.(Format: weeks=3;days=2;hours=3,seconds=2).' | ||||
| @ -40634,6 +41068,18 @@ components: | ||||
|           type: string | ||||
|           format: uuid | ||||
|           nullable: true | ||||
|         interface_admin: | ||||
|           type: string | ||||
|           format: uuid | ||||
|           nullable: true | ||||
|         interface_user: | ||||
|           type: string | ||||
|           format: uuid | ||||
|           nullable: true | ||||
|         interface_flow: | ||||
|           type: string | ||||
|           format: uuid | ||||
|           nullable: true | ||||
|         event_retention: | ||||
|           type: string | ||||
|           minLength: 1 | ||||
|  | ||||
| @ -33,7 +33,7 @@ class TestFlowsAuthenticator(SeleniumTestCase): | ||||
|  | ||||
|         flow: Flow = Flow.objects.get(slug="default-authentication-flow") | ||||
|  | ||||
|         self.driver.get(self.url("authentik_core:if-flow", flow_slug=flow.slug)) | ||||
|         self.driver.get(self.url("authentik_interfaces:if", if_name="flow", flow_slug=flow.slug)) | ||||
|         self.login() | ||||
|  | ||||
|         # Get expected token | ||||
| @ -57,7 +57,7 @@ class TestFlowsAuthenticator(SeleniumTestCase): | ||||
|         """test TOTP Setup stage""" | ||||
|         flow: Flow = Flow.objects.get(slug="default-authentication-flow") | ||||
|  | ||||
|         self.driver.get(self.url("authentik_core:if-flow", flow_slug=flow.slug)) | ||||
|         self.driver.get(self.url("authentik_interfaces:if", if_name="flow", flow_slug=flow.slug)) | ||||
|         self.login() | ||||
|  | ||||
|         self.wait_for_url(self.if_user_url("/library")) | ||||
| @ -103,7 +103,7 @@ class TestFlowsAuthenticator(SeleniumTestCase): | ||||
|         """test Static OTP Setup stage""" | ||||
|         flow: Flow = Flow.objects.get(slug="default-authentication-flow") | ||||
|  | ||||
|         self.driver.get(self.url("authentik_core:if-flow", flow_slug=flow.slug)) | ||||
|         self.driver.get(self.url("authentik_interfaces:if", if_name="flow", flow_slug=flow.slug)) | ||||
|         self.login() | ||||
|  | ||||
|         self.wait_for_url(self.if_user_url("/library")) | ||||
|  | ||||
| @ -15,7 +15,8 @@ class TestFlowsLogin(SeleniumTestCase): | ||||
|         """test default login flow""" | ||||
|         self.driver.get( | ||||
|             self.url( | ||||
|                 "authentik_core:if-flow", | ||||
|                 "authentik_interfaces:if", | ||||
|                 if_name="flow", | ||||
|                 flow_slug="default-authentication-flow", | ||||
|             ) | ||||
|         ) | ||||
|  | ||||
| @ -35,7 +35,8 @@ class TestFlowsStageSetup(SeleniumTestCase): | ||||
|  | ||||
|         self.driver.get( | ||||
|             self.url( | ||||
|                 "authentik_core:if-flow", | ||||
|                 "authentik_interfaces:if", | ||||
|                 if_name="flow", | ||||
|                 flow_slug="default-authentication-flow", | ||||
|             ) | ||||
|         ) | ||||
|  | ||||
| @ -299,6 +299,9 @@ export class AdminInterface extends Interface { | ||||
|                 <ak-sidebar-item path="/core/tenants"> | ||||
|                     <span slot="label">${t`Tenants`}</span> | ||||
|                 </ak-sidebar-item> | ||||
|                 <ak-sidebar-item path="/interfaces"> | ||||
|                     <span slot="label">${t`Interfaces`}</span> | ||||
|                 </ak-sidebar-item> | ||||
|                 <ak-sidebar-item path="/crypto/certificates"> | ||||
|                     <span slot="label">${t`Certificates`}</span> | ||||
|                 </ak-sidebar-item> | ||||
|  | ||||
| @ -132,6 +132,10 @@ export const ROUTES: Route[] = [ | ||||
|         await import("@goauthentik/admin/blueprints/BlueprintListPage"); | ||||
|         return html`<ak-blueprint-list></ak-blueprint-list>`; | ||||
|     }), | ||||
|     new Route(new RegExp("^/interfaces$"), async () => { | ||||
|         await import("@goauthentik/admin/interfaces/InterfaceListPage"); | ||||
|         return html`<ak-interface-list></ak-interface-list>`; | ||||
|     }), | ||||
|     new Route(new RegExp("^/debug$"), async () => { | ||||
|         await import("@goauthentik/admin/DebugPage"); | ||||
|         return html`<ak-admin-debug-page></ak-admin-debug-page>`; | ||||
|  | ||||
							
								
								
									
										90
									
								
								web/src/admin/interfaces/InterfaceForm.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										90
									
								
								web/src/admin/interfaces/InterfaceForm.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,90 @@ | ||||
| import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | ||||
| import { first } from "@goauthentik/common/utils"; | ||||
| import "@goauthentik/elements/CodeMirror"; | ||||
| import "@goauthentik/elements/forms/HorizontalFormElement"; | ||||
| import { ModelForm } from "@goauthentik/elements/forms/ModelForm"; | ||||
| import "@goauthentik/elements/forms/Radio"; | ||||
|  | ||||
| import { t } from "@lingui/macro"; | ||||
|  | ||||
| import { TemplateResult, html } from "lit"; | ||||
| import { customElement } from "lit/decorators.js"; | ||||
| import { ifDefined } from "lit/directives/if-defined.js"; | ||||
|  | ||||
| import { Interface, InterfaceTypeEnum, InterfacesApi } from "@goauthentik/api"; | ||||
|  | ||||
| @customElement("ak-interface-form") | ||||
| export class InterfaceForm extends ModelForm<Interface, string> { | ||||
|     loadInstance(pk: string): Promise<Interface> { | ||||
|         return new InterfacesApi(DEFAULT_CONFIG).interfacesRetrieve({ | ||||
|             interfaceUuid: pk, | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     getSuccessMessage(): string { | ||||
|         if (this.instance) { | ||||
|             return t`Successfully updated interface.`; | ||||
|         } else { | ||||
|             return t`Successfully created interface.`; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     send = (data: Interface): Promise<Interface> => { | ||||
|         if (this.instance?.interfaceUuid) { | ||||
|             return new InterfacesApi(DEFAULT_CONFIG).interfacesUpdate({ | ||||
|                 interfaceUuid: this.instance.interfaceUuid, | ||||
|                 interfaceRequest: data, | ||||
|             }); | ||||
|         } else { | ||||
|             return new InterfacesApi(DEFAULT_CONFIG).interfacesCreate({ | ||||
|                 interfaceRequest: data, | ||||
|             }); | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     renderForm(): TemplateResult { | ||||
|         return html`<form class="pf-c-form pf-m-horizontal"> | ||||
|             <ak-form-element-horizontal label=${t`URL Name`} ?required=${true} name="urlName"> | ||||
|                 <input | ||||
|                     type="text" | ||||
|                     value="${first(this.instance?.urlName, "")}" | ||||
|                     class="pf-c-form-control" | ||||
|                     required | ||||
|                 /> | ||||
|                 <p class="pf-c-form__helper-text"> | ||||
|                     ${t`Name used in the URL when accessing this interface.`} | ||||
|                 </p> | ||||
|             </ak-form-element-horizontal> | ||||
|             <ak-form-element-horizontal label=${t`Type`} ?required=${true} name="type"> | ||||
|                 <ak-radio | ||||
|                     .options=${[ | ||||
|                         { | ||||
|                             label: t`Enduser interface`, | ||||
|                             value: InterfaceTypeEnum.User, | ||||
|                             default: true, | ||||
|                         }, | ||||
|                         { | ||||
|                             label: t`Flow interface`, | ||||
|                             value: InterfaceTypeEnum.Flow, | ||||
|                         }, | ||||
|                         { | ||||
|                             label: t`Admin interface`, | ||||
|                             value: InterfaceTypeEnum.Admin, | ||||
|                         }, | ||||
|                     ]} | ||||
|                     .value=${this.instance?.type} | ||||
|                 > | ||||
|                 </ak-radio> | ||||
|                 <p class="pf-c-form__helper-text"> | ||||
|                     ${t`Configure how authentik will use this interface.`} | ||||
|                 </p> | ||||
|             </ak-form-element-horizontal> | ||||
|             <ak-form-element-horizontal label=${t`Template`} ?required=${true} name="template" | ||||
|                 ><ak-codemirror | ||||
|                     mode="html" | ||||
|                     value="${ifDefined(this.instance?.template)}" | ||||
|                 ></ak-codemirror> | ||||
|             </ak-form-element-horizontal> | ||||
|         </form>`; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										101
									
								
								web/src/admin/interfaces/InterfaceListPage.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										101
									
								
								web/src/admin/interfaces/InterfaceListPage.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,101 @@ | ||||
| import "@goauthentik/admin/interfaces/InterfaceForm"; | ||||
| import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | ||||
| import { uiConfig } from "@goauthentik/common/ui/config"; | ||||
| import "@goauthentik/elements/buttons/SpinnerButton"; | ||||
| import "@goauthentik/elements/forms/DeleteBulkForm"; | ||||
| import "@goauthentik/elements/forms/ModalForm"; | ||||
| import { PaginatedResponse } from "@goauthentik/elements/table/Table"; | ||||
| import { TableColumn } from "@goauthentik/elements/table/Table"; | ||||
| import { TablePage } from "@goauthentik/elements/table/TablePage"; | ||||
|  | ||||
| import { t } from "@lingui/macro"; | ||||
|  | ||||
| import { TemplateResult, html } from "lit"; | ||||
| import { customElement, property } from "lit/decorators.js"; | ||||
|  | ||||
| import { Interface, InterfacesApi } from "@goauthentik/api"; | ||||
|  | ||||
| @customElement("ak-interface-list") | ||||
| export class InterfaceListPage extends TablePage<Interface> { | ||||
|     searchEnabled(): boolean { | ||||
|         return true; | ||||
|     } | ||||
|     pageTitle(): string { | ||||
|         return t`Interfaces`; | ||||
|     } | ||||
|     pageDescription(): string { | ||||
|         return t`Manage custom interfaces for authentik`; | ||||
|     } | ||||
|     pageIcon(): string { | ||||
|         return "fa fa-home"; | ||||
|     } | ||||
|  | ||||
|     checkbox = true; | ||||
|  | ||||
|     @property() | ||||
|     order = "url_name"; | ||||
|  | ||||
|     async apiEndpoint(page: number): Promise<PaginatedResponse<Interface>> { | ||||
|         return new InterfacesApi(DEFAULT_CONFIG).interfacesList({ | ||||
|             ordering: this.order, | ||||
|             page: page, | ||||
|             pageSize: (await uiConfig()).pagination.perPage, | ||||
|             search: this.search || "", | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     columns(): TableColumn[] { | ||||
|         return [new TableColumn(t`URL Name`, "url_name"), new TableColumn(t`Actions`)]; | ||||
|     } | ||||
|  | ||||
|     renderToolbarSelected(): TemplateResult { | ||||
|         const disabled = this.selectedElements.length < 1; | ||||
|         return html`<ak-forms-delete-bulk | ||||
|             objectLabel=${t`Interface(s)`} | ||||
|             .objects=${this.selectedElements} | ||||
|             .metadata=${(item: Interface) => { | ||||
|                 return [{ key: t`Domain`, value: item.urlName }]; | ||||
|             }} | ||||
|             .usedBy=${(item: Interface) => { | ||||
|                 return new InterfacesApi(DEFAULT_CONFIG).interfacesUsedByList({ | ||||
|                     interfaceUuid: item.interfaceUuid, | ||||
|                 }); | ||||
|             }} | ||||
|             .delete=${(item: Interface) => { | ||||
|                 return new InterfacesApi(DEFAULT_CONFIG).interfacesDestroy({ | ||||
|                     interfaceUuid: item.interfaceUuid, | ||||
|                 }); | ||||
|             }} | ||||
|         > | ||||
|             <button ?disabled=${disabled} slot="trigger" class="pf-c-button pf-m-danger"> | ||||
|                 ${t`Delete`} | ||||
|             </button> | ||||
|         </ak-forms-delete-bulk>`; | ||||
|     } | ||||
|  | ||||
|     row(item: Interface): TemplateResult[] { | ||||
|         return [ | ||||
|             html`${item.urlName}`, | ||||
|             html`<ak-forms-modal> | ||||
|                 <span slot="submit"> ${t`Update`} </span> | ||||
|                 <span slot="header"> ${t`Update Interface`} </span> | ||||
|                 <ak-interface-form slot="form" .instancePk=${item.interfaceUuid}> | ||||
|                 </ak-interface-form> | ||||
|                 <button slot="trigger" class="pf-c-button pf-m-plain"> | ||||
|                     <i class="fas fa-edit"></i> | ||||
|                 </button> | ||||
|             </ak-forms-modal>`, | ||||
|         ]; | ||||
|     } | ||||
|  | ||||
|     renderObjectCreate(): TemplateResult { | ||||
|         return html` | ||||
|             <ak-forms-modal> | ||||
|                 <span slot="submit"> ${t`Create`} </span> | ||||
|                 <span slot="header"> ${t`Create Interface`} </span> | ||||
|                 <ak-interface-form slot="form"> </ak-interface-form> | ||||
|                 <button slot="trigger" class="pf-c-button pf-m-primary">${t`Create`}</button> | ||||
|             </ak-forms-modal> | ||||
|         `; | ||||
|     } | ||||
| } | ||||
| @ -23,6 +23,10 @@ import { | ||||
|     FlowsApi, | ||||
|     FlowsInstancesListDesignationEnum, | ||||
|     FlowsInstancesListRequest, | ||||
|     Interface, | ||||
|     InterfacesApi, | ||||
|     InterfacesListRequest, | ||||
|     InterfacesListTypeEnum, | ||||
|     Tenant, | ||||
| } from "@goauthentik/api"; | ||||
|  | ||||
| @ -368,6 +372,107 @@ export class TenantForm extends ModelForm<Tenant, string> { | ||||
|                     </ak-form-element-horizontal> | ||||
|                 </div> | ||||
|             </ak-form-group> | ||||
|             <ak-form-group> | ||||
|                 <span slot="header"> ${t`Interfaces`} </span> | ||||
|                 <div slot="body" class="pf-c-form"> | ||||
|                     <ak-form-element-horizontal label=${t`User Interface`} name="interfaceUser"> | ||||
|                         <ak-search-select | ||||
|                             .fetchObjects=${async (query?: string): Promise<Interface[]> => { | ||||
|                                 const args: InterfacesListRequest = { | ||||
|                                     ordering: "url_name", | ||||
|                                     type: InterfacesListTypeEnum.User, | ||||
|                                 }; | ||||
|                                 if (query !== undefined) { | ||||
|                                     args.search = query; | ||||
|                                 } | ||||
|                                 const flows = await new InterfacesApi( | ||||
|                                     DEFAULT_CONFIG, | ||||
|                                 ).interfacesList(args); | ||||
|                                 return flows.results; | ||||
|                             }} | ||||
|                             .renderElement=${(iface: Interface): string => { | ||||
|                                 return iface.urlName; | ||||
|                             }} | ||||
|                             .renderDescription=${(iface: Interface): TemplateResult => { | ||||
|                                 return html`${iface.type}`; | ||||
|                             }} | ||||
|                             .value=${(iface: Interface | undefined): string | undefined => { | ||||
|                                 return iface?.interfaceUuid; | ||||
|                             }} | ||||
|                             .selected=${(iface: Interface): boolean => { | ||||
|                                 return this.instance?.interfaceUser === iface.interfaceUuid; | ||||
|                             }} | ||||
|                             ?blankable=${true} | ||||
|                         > | ||||
|                         </ak-search-select> | ||||
|                         <p class="pf-c-form__helper-text">${t`.`}</p> | ||||
|                     </ak-form-element-horizontal> | ||||
|                     <ak-form-element-horizontal label=${t`Flow Interface`} name="interfaceFlow"> | ||||
|                         <ak-search-select | ||||
|                             .fetchObjects=${async (query?: string): Promise<Interface[]> => { | ||||
|                                 const args: InterfacesListRequest = { | ||||
|                                     ordering: "url_name", | ||||
|                                     type: InterfacesListTypeEnum.Flow, | ||||
|                                 }; | ||||
|                                 if (query !== undefined) { | ||||
|                                     args.search = query; | ||||
|                                 } | ||||
|                                 const flows = await new InterfacesApi( | ||||
|                                     DEFAULT_CONFIG, | ||||
|                                 ).interfacesList(args); | ||||
|                                 return flows.results; | ||||
|                             }} | ||||
|                             .renderElement=${(iface: Interface): string => { | ||||
|                                 return iface.urlName; | ||||
|                             }} | ||||
|                             .renderDescription=${(iface: Interface): TemplateResult => { | ||||
|                                 return html`${iface.type}`; | ||||
|                             }} | ||||
|                             .value=${(iface: Interface | undefined): string | undefined => { | ||||
|                                 return iface?.interfaceUuid; | ||||
|                             }} | ||||
|                             .selected=${(iface: Interface): boolean => { | ||||
|                                 return this.instance?.interfaceFlow === iface.interfaceUuid; | ||||
|                             }} | ||||
|                             ?blankable=${true} | ||||
|                         > | ||||
|                         </ak-search-select> | ||||
|                         <p class="pf-c-form__helper-text">${t`.`}</p> | ||||
|                     </ak-form-element-horizontal> | ||||
|                     <ak-form-element-horizontal label=${t`Admin Interface`} name="interfaceAdmin"> | ||||
|                         <ak-search-select | ||||
|                             .fetchObjects=${async (query?: string): Promise<Interface[]> => { | ||||
|                                 const args: InterfacesListRequest = { | ||||
|                                     ordering: "url_name", | ||||
|                                     type: InterfacesListTypeEnum.Admin, | ||||
|                                 }; | ||||
|                                 if (query !== undefined) { | ||||
|                                     args.search = query; | ||||
|                                 } | ||||
|                                 const flows = await new InterfacesApi( | ||||
|                                     DEFAULT_CONFIG, | ||||
|                                 ).interfacesList(args); | ||||
|                                 return flows.results; | ||||
|                             }} | ||||
|                             .renderElement=${(iface: Interface): string => { | ||||
|                                 return iface.urlName; | ||||
|                             }} | ||||
|                             .renderDescription=${(iface: Interface): TemplateResult => { | ||||
|                                 return html`${iface.type}`; | ||||
|                             }} | ||||
|                             .value=${(iface: Interface | undefined): string | undefined => { | ||||
|                                 return iface?.interfaceUuid; | ||||
|                             }} | ||||
|                             .selected=${(iface: Interface): boolean => { | ||||
|                                 return this.instance?.interfaceAdmin === iface.interfaceUuid; | ||||
|                             }} | ||||
|                             ?blankable=${true} | ||||
|                         > | ||||
|                         </ak-search-select> | ||||
|                         <p class="pf-c-form__helper-text">${t`.`}</p> | ||||
|                     </ak-form-element-horizontal> | ||||
|                 </div> | ||||
|             </ak-form-group> | ||||
|             <ak-form-group> | ||||
|                 <span slot="header"> ${t`Other global settings`} </span> | ||||
|                 <div slot="body" class="pf-c-form"> | ||||
|  | ||||
		Reference in New Issue
	
	Block a user
	