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