add tenant migration, migrate default urls and redirects
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
This commit is contained in:
parent
e39c460e3a
commit
246a6c7384
|
@ -25,7 +25,8 @@ from authentik.flows.planner import (
|
|||
)
|
||||
from authentik.flows.stage import StageView
|
||||
from authentik.flows.views.executor import NEXT_ARG_NAME, SESSION_KEY_GET, SESSION_KEY_PLAN
|
||||
from authentik.lib.utils.urls import redirect_with_qs
|
||||
from authentik.interfaces.models import InterfaceType
|
||||
from authentik.interfaces.views import redirect_to_default_interface
|
||||
from authentik.lib.views import bad_request_message
|
||||
from authentik.policies.denied import AccessDeniedResponse
|
||||
from authentik.policies.utils import delete_none_keys
|
||||
|
@ -226,7 +227,7 @@ class SourceFlowManager:
|
|||
# Ensure redirect is carried through when user was trying to
|
||||
# authorize application
|
||||
final_redirect = self.request.session.get(SESSION_KEY_GET, {}).get(
|
||||
NEXT_ARG_NAME, "authentik_core:if-user"
|
||||
NEXT_ARG_NAME, "authentik_core:root-redirect"
|
||||
)
|
||||
kwargs.update(
|
||||
{
|
||||
|
@ -253,9 +254,9 @@ class SourceFlowManager:
|
|||
for stage in stages:
|
||||
plan.append_stage(stage)
|
||||
self.request.session[SESSION_KEY_PLAN] = plan
|
||||
return redirect_with_qs(
|
||||
"authentik_core:if-flow",
|
||||
self.request.GET,
|
||||
return redirect_to_default_interface(
|
||||
self.request,
|
||||
InterfaceType.FLOW,
|
||||
flow_slug=flow.slug,
|
||||
)
|
||||
|
||||
|
@ -299,8 +300,9 @@ class SourceFlowManager:
|
|||
_("Successfully linked %(source)s!" % {"source": self.source.name}),
|
||||
)
|
||||
return redirect(
|
||||
# Not ideal that we don't directly redirect to the configured user interface
|
||||
reverse(
|
||||
"authentik_core:if-user",
|
||||
"authentik_core:root-redirect",
|
||||
)
|
||||
+ f"#/settings;page-{self.source.slug}"
|
||||
)
|
||||
|
|
|
@ -59,4 +59,6 @@ class TestImpersonation(TestCase):
|
|||
self.client.force_login(self.other_user)
|
||||
|
||||
response = self.client.get(reverse("authentik_core:impersonate-end"))
|
||||
self.assertRedirects(response, reverse("authentik_core:if-user"))
|
||||
self.assertRedirects(
|
||||
response, reverse("authentik_interfaces:if", kwargs={"if_name", "user"})
|
||||
)
|
||||
|
|
|
@ -3,27 +3,30 @@ from channels.auth import AuthMiddleware
|
|||
from channels.sessions import CookieMiddleware
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django.urls import path
|
||||
from django.views.decorators.csrf import ensure_csrf_cookie
|
||||
from django.views.generic import RedirectView
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
|
||||
from authentik.core.views import apps, impersonate
|
||||
from authentik.core.views.debug import AccessDeniedView
|
||||
from authentik.core.views.session import EndSessionView
|
||||
from authentik.interfaces.models import InterfaceType
|
||||
from authentik.interfaces.views import RedirectToInterface
|
||||
from authentik.root.asgi_middleware import SessionMiddleware
|
||||
from authentik.root.messages.consumer import MessageConsumer
|
||||
|
||||
|
||||
def placeholder_view(request: HttpRequest, *args, **kwargs) -> HttpResponse:
|
||||
"""Empty view used as placeholder
|
||||
|
||||
(Mounted to websocket endpoints and used by e2e tests)"""
|
||||
return HttpResponse(status_code=200)
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
path(
|
||||
"",
|
||||
login_required(
|
||||
RedirectView.as_view(pattern_name="authentik_core:if-user", query_string=True)
|
||||
),
|
||||
login_required(RedirectToInterface.as_view(type=InterfaceType.USER)),
|
||||
name="root-redirect",
|
||||
),
|
||||
path(
|
||||
|
|
|
@ -20,7 +20,8 @@ from authentik.flows.views.executor import (
|
|||
SESSION_KEY_PLAN,
|
||||
ToDefaultFlow,
|
||||
)
|
||||
from authentik.lib.utils.urls import redirect_with_qs
|
||||
from authentik.interfaces.models import InterfaceType
|
||||
from authentik.interfaces.views import redirect_to_default_interface
|
||||
from authentik.stages.consent.stage import (
|
||||
PLAN_CONTEXT_CONSENT_HEADER,
|
||||
PLAN_CONTEXT_CONSENT_PERMISSIONS,
|
||||
|
@ -59,7 +60,7 @@ class RedirectToAppLaunch(View):
|
|||
raise Http404
|
||||
plan.insert_stage(in_memory_stage(RedirectToAppStage))
|
||||
request.session[SESSION_KEY_PLAN] = plan
|
||||
return redirect_with_qs("authentik_core:if-flow", request.GET, flow_slug=flow.slug)
|
||||
return redirect_to_default_interface(request, InterfaceType.FLOW, flow_slug=flow.slug)
|
||||
|
||||
|
||||
class RedirectToAppStage(ChallengeStageView):
|
||||
|
|
|
@ -35,7 +35,7 @@ class ImpersonateInitView(View):
|
|||
|
||||
Event.new(EventAction.IMPERSONATION_STARTED).from_http(request, user_to_be)
|
||||
|
||||
return redirect("authentik_core:if-user")
|
||||
return redirect("authentik_core:root-redirect")
|
||||
|
||||
|
||||
class ImpersonateEndView(View):
|
||||
|
@ -48,7 +48,7 @@ class ImpersonateEndView(View):
|
|||
or SESSION_KEY_IMPERSONATE_ORIGINAL_USER not in request.session
|
||||
):
|
||||
LOGGER.debug("Can't end impersonation", user=request.user)
|
||||
return redirect("authentik_core:if-user")
|
||||
return redirect("authentik_core:root-redirect")
|
||||
|
||||
original_user = request.session[SESSION_KEY_IMPERSONATE_ORIGINAL_USER]
|
||||
|
||||
|
|
|
@ -53,6 +53,8 @@ from authentik.flows.planner import (
|
|||
FlowPlanner,
|
||||
)
|
||||
from authentik.flows.stage import AccessDeniedChallengeView, StageView
|
||||
from authentik.interfaces.models import InterfaceType
|
||||
from authentik.interfaces.views import redirect_to_default_interface
|
||||
from authentik.lib.sentry import SentryIgnoredException
|
||||
from authentik.lib.utils.errors import exception_to_string
|
||||
from authentik.lib.utils.reflection import all_subclasses, class_to_path
|
||||
|
@ -512,7 +514,7 @@ class ToDefaultFlow(View):
|
|||
flow_slug=flow.slug,
|
||||
)
|
||||
del self.request.session[SESSION_KEY_PLAN]
|
||||
return redirect_with_qs("authentik_core:if-flow", request.GET, flow_slug=flow.slug)
|
||||
return redirect_to_default_interface(request, InterfaceType.FLOW, flow_slug=flow.slug)
|
||||
|
||||
|
||||
def to_stage_response(request: HttpRequest, source: HttpResponse) -> HttpResponse:
|
||||
|
@ -583,8 +585,8 @@ class ConfigureFlowInitView(LoginRequiredMixin, View):
|
|||
LOGGER.warning("Flow not applicable to user")
|
||||
raise Http404
|
||||
request.session[SESSION_KEY_PLAN] = plan
|
||||
return redirect_with_qs(
|
||||
"authentik_core:if-flow",
|
||||
self.request.GET,
|
||||
return redirect_to_default_interface(
|
||||
self.request,
|
||||
InterfaceType.FLOW,
|
||||
flow_slug=stage.configure_flow.slug,
|
||||
)
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
"""interfaces API"""
|
||||
from rest_framework.serializers import ModelSerializer
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
|
@ -5,6 +6,8 @@ from authentik.interfaces.models import Interface
|
|||
|
||||
|
||||
class InterfaceSerializer(ModelSerializer):
|
||||
"""Interface serializer"""
|
||||
|
||||
class Meta:
|
||||
model = Interface
|
||||
fields = [
|
||||
|
@ -16,5 +19,8 @@ class InterfaceSerializer(ModelSerializer):
|
|||
|
||||
|
||||
class InterfaceViewSet(ModelViewSet):
|
||||
"""Interface serializer"""
|
||||
|
||||
queryset = Interface.objects.all()
|
||||
serializer_class = InterfaceSerializer
|
||||
filterset_fields = ["url_name", "type", "template"]
|
||||
|
|
|
@ -20,7 +20,7 @@ class Migration(migrations.Migration):
|
|||
default=uuid.uuid4, editable=False, primary_key=True, serialize=False
|
||||
),
|
||||
),
|
||||
("url_name", models.SlugField()),
|
||||
("url_name", models.SlugField(unique=True)),
|
||||
(
|
||||
"type",
|
||||
models.TextField(
|
||||
|
|
|
@ -21,7 +21,7 @@ class Interface(SerializerModel):
|
|||
|
||||
interface_uuid = models.UUIDField(primary_key=True, editable=False, default=uuid4)
|
||||
|
||||
url_name = models.SlugField()
|
||||
url_name = models.SlugField(unique=True)
|
||||
|
||||
type = models.TextField(choices=InterfaceType.choices)
|
||||
template = models.TextField()
|
||||
|
|
|
@ -10,7 +10,5 @@ urlpatterns = [
|
|||
kwargs={"flow_slug": None},
|
||||
name="if",
|
||||
),
|
||||
path(
|
||||
"<slug:if_name>/<slug:flow_slug>/", InterfaceView.as_view(), name="if"
|
||||
),
|
||||
path("<slug:if_name>/<slug:flow_slug>/", InterfaceView.as_view(), name="if"),
|
||||
]
|
||||
|
|
|
@ -1,23 +1,25 @@
|
|||
"""Interface views"""
|
||||
from json import dumps
|
||||
from typing import Any
|
||||
from typing import Any, Optional
|
||||
|
||||
from django.http import Http404, HttpRequest, HttpResponse
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.template import Template, TemplateSyntaxError, engines
|
||||
from django.template.response import TemplateResponse
|
||||
from django.views import View
|
||||
from rest_framework.request import Request
|
||||
from django.views.decorators.cache import cache_page
|
||||
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 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 redirect_with_qs
|
||||
from authentik.tenants.api import CurrentTenantSerializer
|
||||
from authentik.tenants.models import Tenant
|
||||
|
||||
|
||||
def template_from_string(template_string: str) -> Template:
|
||||
|
@ -32,6 +34,38 @@ def template_from_string(template_string: str) -> Template:
|
|||
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)
|
||||
|
||||
|
||||
class RedirectToInterface(View):
|
||||
"""Redirect to tenant's configured view for specified type"""
|
||||
|
||||
type: Optional[InterfaceType] = None
|
||||
|
||||
def dispatch(self, request: HttpRequest, **kwargs: Any) -> HttpResponse:
|
||||
tenant: Tenant = request.tenant
|
||||
interface: Interface = None
|
||||
|
||||
if self.type == InterfaceType.USER:
|
||||
interface = tenant.interface_user
|
||||
if self.type == InterfaceType.ADMIN:
|
||||
interface = tenant.interface_admin
|
||||
if self.type == InterfaceType.FLOW:
|
||||
interface = tenant.interface_flow
|
||||
|
||||
if not interface:
|
||||
raise Http404()
|
||||
return redirect_with_qs(
|
||||
"authentik_interfaces:if",
|
||||
self.request.GET,
|
||||
if_name=interface.url_name,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
|
||||
@method_decorator(ensure_csrf_cookie, name="dispatch")
|
||||
@method_decorator(cache_page(60 * 10), name="dispatch")
|
||||
class InterfaceView(View):
|
||||
|
|
|
@ -22,4 +22,4 @@ class UseTokenView(View):
|
|||
login(request, token.user, backend=BACKEND_INBUILT)
|
||||
token.delete()
|
||||
messages.warning(request, _("Used recovery-link to authenticate."))
|
||||
return redirect("authentik_core:if-user")
|
||||
return redirect("authentik_core:root-redirect")
|
||||
|
|
|
@ -72,7 +72,7 @@ class InitiateView(View):
|
|||
# Ensure redirect is carried through when user was trying to
|
||||
# authorize application
|
||||
final_redirect = self.request.session.get(SESSION_KEY_GET, {}).get(
|
||||
NEXT_ARG_NAME, "authentik_core:if-user"
|
||||
NEXT_ARG_NAME, "authentik_core:root-redirect"
|
||||
)
|
||||
kwargs.update(
|
||||
{
|
||||
|
|
|
@ -54,6 +54,9 @@ class TenantSerializer(ModelSerializer):
|
|||
"flow_unenrollment",
|
||||
"flow_user_settings",
|
||||
"flow_device_code",
|
||||
"interface_admin",
|
||||
"interface_user",
|
||||
"interface_flow",
|
||||
"event_retention",
|
||||
"web_certificate",
|
||||
"attributes",
|
||||
|
@ -120,6 +123,9 @@ class TenantViewSet(UsedByMixin, ModelViewSet):
|
|||
"flow_unenrollment",
|
||||
"flow_user_settings",
|
||||
"flow_device_code",
|
||||
"interface_admin",
|
||||
"interface_user",
|
||||
"interface_flow",
|
||||
"event_retention",
|
||||
"web_certificate",
|
||||
]
|
||||
|
|
|
@ -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_discover
|
||||
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_discover.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),
|
||||
]
|
|
@ -8,6 +8,7 @@ from structlog.stdlib import get_logger
|
|||
|
||||
from authentik.crypto.models import CertificateKeyPair
|
||||
from authentik.flows.models import Flow
|
||||
from authentik.interfaces.models import Interface
|
||||
from authentik.lib.models import SerializerModel
|
||||
from authentik.lib.utils.time import timedelta_string_validator
|
||||
|
||||
|
@ -51,6 +52,25 @@ class Tenant(SerializerModel):
|
|||
Flow, null=True, on_delete=models.SET_NULL, related_name="tenant_device_code"
|
||||
)
|
||||
|
||||
interface_flow = models.ForeignKey(
|
||||
Interface,
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
related_name="tenant_flow",
|
||||
)
|
||||
interface_user = models.ForeignKey(
|
||||
Interface,
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
related_name="tenant_user",
|
||||
)
|
||||
interface_admin = models.ForeignKey(
|
||||
Interface,
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
related_name="tenant_admin",
|
||||
)
|
||||
|
||||
event_retention = models.TextField(
|
||||
default="days=365",
|
||||
validators=[timedelta_string_validator],
|
||||
|
|
|
@ -2,6 +2,11 @@ metadata:
|
|||
name: Default - Tenant
|
||||
version: 1
|
||||
entries:
|
||||
- model: authentik_blueprints.metaapplyblueprint
|
||||
attrs:
|
||||
identifiers:
|
||||
name: System - Interfaces
|
||||
required: false
|
||||
- model: authentik_blueprints.metaapplyblueprint
|
||||
attrs:
|
||||
identifiers:
|
||||
|
@ -21,6 +26,9 @@ entries:
|
|||
flow_authentication: !Find [authentik_flows.flow, [slug, default-authentication-flow]]
|
||||
flow_invalidation: !Find [authentik_flows.flow, [slug, default-invalidation-flow]]
|
||||
flow_user_settings: !Find [authentik_flows.flow, [slug, default-user-settings-flow]]
|
||||
interface_admin: !Find [authentik_interfaces.Interface, [type, admin]]
|
||||
interface_user: !Find [authentik_interfaces.Interface, [type, user]]
|
||||
interface_flow: !Find [authentik_interfaces.Interface, [type, flow]]
|
||||
identifiers:
|
||||
domain: authentik-default
|
||||
default: True
|
||||
|
|
Reference in a new issue