From 828f47754829c4f992e96e9ab2c03a4077364a67 Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Thu, 21 Mar 2024 16:55:55 +0100 Subject: [PATCH] add default app and restrict Signed-off-by: Jens Langhammer --- authentik/brands/api.py | 1 + .../0006_brand_default_application.py | 26 ++++++++++ authentik/brands/models.py | 10 ++++ authentik/core/urls.py | 18 +++---- authentik/core/views/interface.py | 48 ++++++++++++++++++- blueprints/schema.json | 5 ++ schema.yml | 18 +++++++ web/src/admin/brands/BrandForm.ts | 48 ++++++++++++++++++- 8 files changed, 163 insertions(+), 11 deletions(-) create mode 100644 authentik/brands/migrations/0006_brand_default_application.py diff --git a/authentik/brands/api.py b/authentik/brands/api.py index a856cee485..94c0987c84 100644 --- a/authentik/brands/api.py +++ b/authentik/brands/api.py @@ -56,6 +56,7 @@ class BrandSerializer(ModelSerializer): "flow_unenrollment", "flow_user_settings", "flow_device_code", + "default_application", "web_certificate", "attributes", ] diff --git a/authentik/brands/migrations/0006_brand_default_application.py b/authentik/brands/migrations/0006_brand_default_application.py new file mode 100644 index 0000000000..ed349252e3 --- /dev/null +++ b/authentik/brands/migrations/0006_brand_default_application.py @@ -0,0 +1,26 @@ +# Generated by Django 5.0.3 on 2024-03-21 15:42 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("authentik_brands", "0005_tenantuuid_to_branduuid"), + ("authentik_core", "0033_alter_user_options"), + ] + + operations = [ + migrations.AddField( + model_name="brand", + name="default_application", + field=models.ForeignKey( + default=None, + help_text="When set, external users will be redirected to this application after authenticating.", + null=True, + on_delete=django.db.models.deletion.SET_DEFAULT, + to="authentik_core.application", + ), + ), + ] diff --git a/authentik/brands/models.py b/authentik/brands/models.py index dca4f724ad..9de76b2ab6 100644 --- a/authentik/brands/models.py +++ b/authentik/brands/models.py @@ -51,6 +51,16 @@ class Brand(SerializerModel): Flow, null=True, on_delete=models.SET_NULL, related_name="brand_device_code" ) + default_application = models.ForeignKey( + "authentik_core.Application", + null=True, + default=None, + on_delete=models.SET_DEFAULT, + help_text=_( + "When set, external users will be redirected to this application after authenticating." + ), + ) + web_certificate = models.ForeignKey( CertificateKeyPair, null=True, diff --git a/authentik/core/urls.py b/authentik/core/urls.py index 3f3949d0c3..766ae87d5e 100644 --- a/authentik/core/urls.py +++ b/authentik/core/urls.py @@ -6,7 +6,6 @@ from django.conf import settings from django.contrib.auth.decorators import login_required from django.urls import path from django.views.decorators.csrf import ensure_csrf_cookie -from django.views.generic import RedirectView from authentik.core.api.applications import ApplicationViewSet from authentik.core.api.authenticated_sessions import AuthenticatedSessionViewSet @@ -20,7 +19,12 @@ from authentik.core.api.transactional_applications import TransactionalApplicati from authentik.core.api.users import UserViewSet from authentik.core.views import apps from authentik.core.views.debug import AccessDeniedView -from authentik.core.views.interface import FlowInterfaceView, InterfaceView +from authentik.core.views.interface import ( + BrandDefaultRedirectView, + FlowInterfaceView, + InterfaceView, + RootRedirectView, +) from authentik.core.views.session import EndSessionView from authentik.root.asgi_middleware import SessionMiddleware from authentik.root.messages.consumer import MessageConsumer @@ -29,13 +33,11 @@ from authentik.root.middleware import ChannelsLoggingMiddleware urlpatterns = [ path( "", - login_required( - RedirectView.as_view(pattern_name="authentik_core:if-user", query_string=True) - ), + login_required(RootRedirectView.as_view()), name="root-redirect", ), path( - # We have to use this format since everything else uses applications/o or applications/saml + # We have to use this format since everything else uses application/o or application/saml "application/launch//", apps.RedirectToAppLaunch.as_view(), name="application-launch", @@ -43,12 +45,12 @@ urlpatterns = [ # Interfaces path( "if/admin/", - ensure_csrf_cookie(InterfaceView.as_view(template_name="if/admin.html")), + ensure_csrf_cookie(BrandDefaultRedirectView.as_view(template_name="if/admin.html")), name="if-admin", ), path( "if/user/", - ensure_csrf_cookie(InterfaceView.as_view(template_name="if/user.html")), + ensure_csrf_cookie(BrandDefaultRedirectView.as_view(template_name="if/user.html")), name="if-user", ), path( diff --git a/authentik/core/views/interface.py b/authentik/core/views/interface.py index 2a7dbda558..62f02bd2f3 100644 --- a/authentik/core/views/interface.py +++ b/authentik/core/views/interface.py @@ -3,15 +3,43 @@ from json import dumps from typing import Any -from django.shortcuts import get_object_or_404 -from django.views.generic.base import TemplateView +from django.http import HttpRequest +from django.http.response import HttpResponse +from django.shortcuts import get_object_or_404, redirect +from django.utils.translation import gettext as _ +from django.views.generic.base import RedirectView, 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.brands.api import CurrentBrandSerializer +from authentik.brands.models import Brand +from authentik.core.models import UserTypes from authentik.flows.models import Flow +from authentik.policies.denied import AccessDeniedResponse + + +class RootRedirectView(RedirectView): + """Root redirect view, redirect to brand's default application if set""" + + pattern_name = "authentik_core:if-user" + query_string = True + + def redirect_to_app(self, request: HttpRequest): + if request.user.is_authenticated and request.user.type == UserTypes.EXTERNAL: + brand: Brand = request.brand + if brand.default_application: + return redirect( + "authentik_core:application-launch", + application_slug=brand.default_application.slug, + ) + return None + + def dispatch(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse: + if redirect_response := RootRedirectView().redirect_to_app(request): + return redirect_response + return super().dispatch(request, *args, **kwargs) class InterfaceView(TemplateView): @@ -27,6 +55,22 @@ class InterfaceView(TemplateView): return super().get_context_data(**kwargs) +class BrandDefaultRedirectView(InterfaceView): + """By default redirect to default app""" + + def dispatch(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse: + if request.user.is_authenticated and request.user.type == UserTypes.EXTERNAL: + brand: Brand = request.brand + if brand.default_application: + return redirect( + "authentik_core:application-launch", + application_slug=brand.default_application.slug, + ) + response = AccessDeniedResponse(self.request) + response.error_message = _("Interface can only be accessed by internal users.") + return super().dispatch(request, *args, **kwargs) + + class FlowInterfaceView(InterfaceView): """Flow interface""" diff --git a/blueprints/schema.json b/blueprints/schema.json index 942f37a039..fc61eb8865 100644 --- a/blueprints/schema.json +++ b/blueprints/schema.json @@ -7652,6 +7652,11 @@ "type": "integer", "title": "Flow device code" }, + "default_application": { + "type": "integer", + "title": "Default application", + "description": "When set, external users will be redirected to this application after authenticating." + }, "web_certificate": { "type": "integer", "title": "Web certificate", diff --git a/schema.yml b/schema.yml index be2ea52f80..16b13ccaaf 100644 --- a/schema.yml +++ b/schema.yml @@ -31366,6 +31366,12 @@ components: type: string format: uuid nullable: true + default_application: + type: string + format: uuid + nullable: true + description: When set, external users will be redirected to this application + after authenticating. web_certificate: type: string format: uuid @@ -31419,6 +31425,12 @@ components: type: string format: uuid nullable: true + default_application: + type: string + format: uuid + nullable: true + description: When set, external users will be redirected to this application + after authenticating. web_certificate: type: string format: uuid @@ -38553,6 +38565,12 @@ components: type: string format: uuid nullable: true + default_application: + type: string + format: uuid + nullable: true + description: When set, external users will be redirected to this application + after authenticating. web_certificate: type: string format: uuid diff --git a/web/src/admin/brands/BrandForm.ts b/web/src/admin/brands/BrandForm.ts index 114bae13e9..27daa3c9cf 100644 --- a/web/src/admin/brands/BrandForm.ts +++ b/web/src/admin/brands/BrandForm.ts @@ -15,7 +15,13 @@ import { msg } from "@lit/localize"; import { TemplateResult, html } from "lit"; import { customElement } from "lit/decorators.js"; -import { Brand, CoreApi, FlowsInstancesListDesignationEnum } from "@goauthentik/api"; +import { + Application, + Brand, + CoreApi, + CoreApplicationsListRequest, + FlowsInstancesListDesignationEnum, +} from "@goauthentik/api"; @customElement("ak-brand-form") export class BrandForm extends ModelForm { @@ -137,6 +143,46 @@ export class BrandForm extends ModelForm { + + + ${msg("External user settings")} +
+ + => { + const args: CoreApplicationsListRequest = { + ordering: "name", + superuserFullList: true, + }; + if (query !== undefined) { + args.search = query; + } + const users = await new CoreApi( + DEFAULT_CONFIG, + ).coreApplicationsList(args); + return users.results; + }} + .renderElement=${(item: Application): string => { + return item.name; + }} + .renderDescription=${(item: Application): TemplateResult => { + return html`${item.slug}`; + }} + .value=${(item: Application | undefined): string | undefined => { + return item?.pk; + }} + .selected=${(item: Application): boolean => { + return item.pk === this.instance?.defaultApplication; + }} + > + + +
+
+ ${msg("Default flows")}