diff --git a/authentik/api/v2/config.py b/authentik/api/v2/config.py index d1f45167a..37b5287dd 100644 --- a/authentik/api/v2/config.py +++ b/authentik/api/v2/config.py @@ -14,13 +14,6 @@ from authentik.core.api.utils import PassiveSerializer from authentik.lib.config import CONFIG -class FooterLinkSerializer(PassiveSerializer): - """Links returned in Config API""" - - href = CharField(read_only=True) - name = CharField(read_only=True) - - class Capabilities(models.TextChoices): """Define capabilities which influence which APIs can/should be used""" @@ -30,10 +23,6 @@ class Capabilities(models.TextChoices): class ConfigSerializer(PassiveSerializer): """Serialize authentik Config into DRF Object""" - branding_logo = CharField(read_only=True) - branding_title = CharField(read_only=True) - ui_footer_links = ListField(child=FooterLinkSerializer(), read_only=True) - error_reporting_enabled = BooleanField(read_only=True) error_reporting_environment = CharField(read_only=True) error_reporting_send_pii = BooleanField(read_only=True) @@ -59,12 +48,9 @@ class ConfigView(APIView): """Retrive public configuration options""" config = ConfigSerializer( { - "branding_logo": CONFIG.y("authentik.branding.logo"), - "branding_title": CONFIG.y("authentik.branding.title"), "error_reporting_enabled": CONFIG.y("error_reporting.enabled"), "error_reporting_environment": CONFIG.y("error_reporting.environment"), "error_reporting_send_pii": CONFIG.y("error_reporting.send_pii"), - "ui_footer_links": CONFIG.y("authentik.footer_links"), "capabilities": self.get_capabilities(), } ) diff --git a/authentik/api/v2/urls.py b/authentik/api/v2/urls.py index 6dce33066..dade31d5e 100644 --- a/authentik/api/v2/urls.py +++ b/authentik/api/v2/urls.py @@ -100,6 +100,7 @@ from authentik.stages.user_delete.api import UserDeleteStageViewSet from authentik.stages.user_login.api import UserLoginStageViewSet from authentik.stages.user_logout.api import UserLogoutStageViewSet from authentik.stages.user_write.api import UserWriteStageViewSet +from authentik.tenants.api import TenantViewSet router = routers.DefaultRouter() @@ -111,6 +112,7 @@ router.register("core/groups", GroupViewSet) router.register("core/users", UserViewSet) router.register("core/user_consent", UserConsentViewSet) router.register("core/tokens", TokenViewSet) +router.register("core/tenants", TenantViewSet) router.register("outposts/instances", OutpostViewSet) router.register("outposts/service_connections/all", ServiceConnectionViewSet) diff --git a/authentik/lib/default.yml b/authentik/lib/default.yml index 4cc831e88..993e09156 100644 --- a/authentik/lib/default.yml +++ b/authentik/lib/default.yml @@ -48,9 +48,6 @@ outposts: authentik: avatars: gravatar # gravatar or none geoip: "" - branding: - title: authentik - logo: /static/dist/assets/icons/icon_left_brand.svg # Optionally add links to the footer on the login page footer_links: - name: Documentation diff --git a/authentik/managed/apps.py b/authentik/managed/apps.py index ee33fce1b..f12d2d304 100644 --- a/authentik/managed/apps.py +++ b/authentik/managed/apps.py @@ -6,7 +6,7 @@ class AuthentikManagedConfig(AppConfig): """authentik Managed app""" name = "authentik.managed" - label = "authentik_Managed" + label = "authentik_managed" verbose_name = "authentik Managed" def ready(self) -> None: diff --git a/authentik/policies/event_matcher/migrations/0015_alter_eventmatcherpolicy_app.py b/authentik/policies/event_matcher/migrations/0015_alter_eventmatcherpolicy_app.py new file mode 100644 index 000000000..4513d2b42 --- /dev/null +++ b/authentik/policies/event_matcher/migrations/0015_alter_eventmatcherpolicy_app.py @@ -0,0 +1,90 @@ +# Generated by Django 3.2.3 on 2021-05-25 12:58 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("authentik_policies_event_matcher", "0014_alter_eventmatcherpolicy_app"), + ] + + operations = [ + migrations.AlterField( + model_name="eventmatcherpolicy", + name="app", + field=models.TextField( + blank=True, + choices=[ + ("authentik.admin", "authentik Admin"), + ("authentik.api", "authentik API"), + ("authentik.events", "authentik Events"), + ("authentik.crypto", "authentik Crypto"), + ("authentik.flows", "authentik Flows"), + ("authentik.outposts", "authentik Outpost"), + ("authentik.lib", "authentik lib"), + ("authentik.policies", "authentik Policies"), + ("authentik.policies.dummy", "authentik Policies.Dummy"), + ( + "authentik.policies.event_matcher", + "authentik Policies.Event Matcher", + ), + ("authentik.policies.expiry", "authentik Policies.Expiry"), + ("authentik.policies.expression", "authentik Policies.Expression"), + ("authentik.policies.hibp", "authentik Policies.HaveIBeenPwned"), + ("authentik.policies.password", "authentik Policies.Password"), + ("authentik.policies.reputation", "authentik Policies.Reputation"), + ("authentik.providers.proxy", "authentik Providers.Proxy"), + ("authentik.providers.ldap", "authentik Providers.LDAP"), + ("authentik.providers.oauth2", "authentik Providers.OAuth2"), + ("authentik.providers.saml", "authentik Providers.SAML"), + ("authentik.recovery", "authentik Recovery"), + ("authentik.sources.ldap", "authentik Sources.LDAP"), + ("authentik.sources.oauth", "authentik Sources.OAuth"), + ("authentik.sources.plex", "authentik Sources.Plex"), + ("authentik.sources.saml", "authentik Sources.SAML"), + ( + "authentik.stages.authenticator_duo", + "authentik Stages.Authenticator.Duo", + ), + ( + "authentik.stages.authenticator_static", + "authentik Stages.Authenticator.Static", + ), + ( + "authentik.stages.authenticator_totp", + "authentik Stages.Authenticator.TOTP", + ), + ( + "authentik.stages.authenticator_validate", + "authentik Stages.Authenticator.Validate", + ), + ( + "authentik.stages.authenticator_webauthn", + "authentik Stages.Authenticator.WebAuthn", + ), + ("authentik.stages.captcha", "authentik Stages.Captcha"), + ("authentik.stages.consent", "authentik Stages.Consent"), + ("authentik.stages.deny", "authentik Stages.Deny"), + ("authentik.stages.dummy", "authentik Stages.Dummy"), + ("authentik.stages.email", "authentik Stages.Email"), + ( + "authentik.stages.identification", + "authentik Stages.Identification", + ), + ("authentik.stages.invitation", "authentik Stages.User Invitation"), + ("authentik.stages.password", "authentik Stages.Password"), + ("authentik.stages.prompt", "authentik Stages.Prompt"), + ("authentik.stages.user_delete", "authentik Stages.User Delete"), + ("authentik.stages.user_login", "authentik Stages.User Login"), + ("authentik.stages.user_logout", "authentik Stages.User Logout"), + ("authentik.stages.user_write", "authentik Stages.User Write"), + ("authentik.tenants", "authentik Tenants"), + ("authentik.core", "authentik Core"), + ("authentik.managed", "authentik Managed"), + ], + default="", + help_text="Match events created by selected application. When left empty, all applications are matched.", + ), + ), + ] diff --git a/authentik/root/settings.py b/authentik/root/settings.py index 86ebb8656..5ceb0554f 100644 --- a/authentik/root/settings.py +++ b/authentik/root/settings.py @@ -127,6 +127,7 @@ INSTALLED_APPS = [ "authentik.stages.user_login", "authentik.stages.user_logout", "authentik.stages.user_write", + "authentik.tenants", "rest_framework", "django_filters", "drf_spectacular", @@ -208,6 +209,7 @@ MIDDLEWARE = [ "django.contrib.sessions.middleware.SessionMiddleware", "django.contrib.auth.middleware.AuthenticationMiddleware", "authentik.core.middleware.RequestIDMiddleware", + "authentik.tenants.middleware.TenantMiddleware", "authentik.events.middleware.AuditMiddleware", "django.middleware.security.SecurityMiddleware", "django.middleware.common.CommonMiddleware", diff --git a/authentik/tenants/__init__.py b/authentik/tenants/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/authentik/tenants/api.py b/authentik/tenants/api.py new file mode 100644 index 000000000..5aa8b31e5 --- /dev/null +++ b/authentik/tenants/api.py @@ -0,0 +1,74 @@ +"""Serializer for tenant models""" +from drf_spectacular.utils import extend_schema +from rest_framework.decorators import action +from rest_framework.fields import CharField, ListField +from rest_framework.permissions import AllowAny +from rest_framework.request import Request +from rest_framework.response import Response +from rest_framework.serializers import ModelSerializer +from rest_framework.viewsets import ModelViewSet + +from authentik.core.api.utils import PassiveSerializer +from authentik.lib.config import CONFIG +from authentik.tenants.models import Tenant + + +class FooterLinkSerializer(PassiveSerializer): + """Links returned in Config API""" + + href = CharField(read_only=True) + name = CharField(read_only=True) + + +class TenantSerializer(ModelSerializer): + """Tenant Serializer""" + + class Meta: + + model = Tenant + fields = [ + "tenant_uuid", + "domain", + "default", + "branding_title", + "branding_logo", + "flow_authentication", + "flow_invalidation", + "flow_recovery", + "flow_enrollment", + "flow_unenrollment", + ] + + +class CurrentTenantSerializer(PassiveSerializer): + """Partial tenant information for styling""" + + branding_title = CharField() + branding_logo = CharField() + ui_footer_links = ListField( + child=FooterLinkSerializer(), + read_only=True, + default=CONFIG.y("authentik.footer_links"), + ) + + +class TenantViewSet(ModelViewSet): + """Tenant Viewset""" + + queryset = Tenant.objects.all() + serializer_class = TenantSerializer + search_fields = [ + "domain", + "branding_title", + ] + ordering = ["domain"] + + @extend_schema( + responses=CurrentTenantSerializer(many=False), + ) + @action(methods=["GET"], detail=False, permission_classes=[AllowAny]) + # pylint: disable=invalid-name, unused-argument + def current(self, request: Request) -> Response: + """Get current tenant""" + tenant: Tenant = request._request.tenant + return Response(CurrentTenantSerializer(tenant).data) diff --git a/authentik/tenants/apps.py b/authentik/tenants/apps.py new file mode 100644 index 000000000..f84933aba --- /dev/null +++ b/authentik/tenants/apps.py @@ -0,0 +1,10 @@ +"""authentik tenant app""" +from django.apps import AppConfig + + +class AuthentikTenantsConfig(AppConfig): + """authentik Tenant app""" + + name = "authentik.tenants" + label = "authentik_tenants" + verbose_name = "authentik Tenants" diff --git a/authentik/tenants/middleware.py b/authentik/tenants/middleware.py new file mode 100644 index 000000000..3739422fe --- /dev/null +++ b/authentik/tenants/middleware.py @@ -0,0 +1,22 @@ +"""Inject tenant into current request""" +from typing import Callable + +from django.http.request import HttpRequest +from django.http.response import HttpResponse + +from authentik.tenants.utils import get_tenant_for_request + + +class TenantMiddleware: + """Add current tenant to http request""" + + get_response: Callable[[HttpRequest], HttpResponse] + + def __init__(self, get_response: Callable[[HttpRequest], HttpResponse]): + self.get_response = get_response + + def __call__(self, request: HttpRequest) -> HttpResponse: + if not hasattr(request, "tenant"): + tenant = get_tenant_for_request(request) + setattr(request, "tenant", tenant) + return self.get_response(request) diff --git a/authentik/tenants/migrations/0001_initial.py b/authentik/tenants/migrations/0001_initial.py new file mode 100644 index 000000000..3f0abc978 --- /dev/null +++ b/authentik/tenants/migrations/0001_initial.py @@ -0,0 +1,95 @@ +# Generated by Django 3.2.3 on 2021-05-29 12:18 + +import uuid + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ("authentik_flows", "0018_oob_flows"), + ] + + operations = [ + migrations.CreateModel( + name="Tenant", + fields=[ + ( + "tenant_uuid", + models.UUIDField( + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + ), + ), + ( + "domain", + models.TextField( + help_text="Domain that activates this tenant. Can be a superset, i.e. `a.b` for `aa.b` and `ba.b`" + ), + ), + ("default", models.BooleanField(default=False)), + ("branding_title", models.TextField(default="authentik")), + ( + "branding_logo", + models.TextField( + default="/static/dist/assets/icons/icon_left_brand.svg" + ), + ), + ( + "flow_authentication", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="tenant_authentication", + to="authentik_flows.flow", + ), + ), + ( + "flow_enrollment", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="tenant_enrollment", + to="authentik_flows.flow", + ), + ), + ( + "flow_invalidation", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="tenant_invalidation", + to="authentik_flows.flow", + ), + ), + ( + "flow_recovery", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="tenant_recovery", + to="authentik_flows.flow", + ), + ), + ( + "flow_unenrollment", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="tenant_unenrollment", + to="authentik_flows.flow", + ), + ), + ], + options={ + "verbose_name": "Tenant", + "verbose_name_plural": "Tenants", + }, + ), + ] diff --git a/authentik/tenants/migrations/__init__.py b/authentik/tenants/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/authentik/tenants/models.py b/authentik/tenants/models.py new file mode 100644 index 000000000..342db514b --- /dev/null +++ b/authentik/tenants/models.py @@ -0,0 +1,51 @@ +"""tenant models""" +from uuid import uuid4 + +from django.db import models +from django.utils.translation import gettext_lazy as _ + +from authentik.flows.models import Flow + + +class Tenant(models.Model): + """Single tenant""" + + tenant_uuid = models.UUIDField(primary_key=True, editable=False, default=uuid4) + domain = models.TextField( + help_text=_( + "Domain that activates this tenant. " + "Can be a superset, i.e. `a.b` for `aa.b` and `ba.b`" + ) + ) + default = models.BooleanField( + default=False, + ) + + branding_title = models.TextField(default="authentik") + branding_logo = models.TextField( + default="/static/dist/assets/icons/icon_left_brand.svg" + ) + + flow_authentication = models.ForeignKey( + Flow, null=True, on_delete=models.SET_NULL, related_name="tenant_authentication" + ) + flow_invalidation = models.ForeignKey( + Flow, null=True, on_delete=models.SET_NULL, related_name="tenant_invalidation" + ) + flow_recovery = models.ForeignKey( + Flow, null=True, on_delete=models.SET_NULL, related_name="tenant_recovery" + ) + flow_enrollment = models.ForeignKey( + Flow, null=True, on_delete=models.SET_NULL, related_name="tenant_enrollment" + ) + flow_unenrollment = models.ForeignKey( + Flow, null=True, on_delete=models.SET_NULL, related_name="tenant_unenrollment" + ) + + def __str__(self) -> str: + return self.domain + + class Meta: + + verbose_name = _("Tenant") + verbose_name_plural = _("Tenants") diff --git a/authentik/tenants/utils.py b/authentik/tenants/utils.py new file mode 100644 index 000000000..bcb901a3b --- /dev/null +++ b/authentik/tenants/utils.py @@ -0,0 +1,17 @@ +"""Tenant utilities""" +from django.db.models import Q +from django.http.request import HttpRequest + +from authentik.tenants.models import Tenant + +_q_default = Q(default=True) + + +def get_tenant_for_request(request: HttpRequest) -> Tenant: + """Get tenant object for current request""" + db_tenants = Tenant.objects.filter( + Q(domain__iendswith=request.get_host()) | _q_default + ) + if not db_tenants.exists(): + return Tenant() + return db_tenants.first() diff --git a/schema.yml b/schema.yml index 09c03aa20..17dad63b6 100644 --- a/schema.yml +++ b/schema.yml @@ -1712,6 +1712,231 @@ paths: $ref: '#/components/schemas/ValidationError' '403': $ref: '#/components/schemas/GenericError' + /api/v2beta/core/tenants/: + get: + operationId: core_tenants_list + description: Tenant Viewset + 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 + tags: + - core + security: + - authentik: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PaginatedTenantList' + description: '' + '400': + $ref: '#/components/schemas/ValidationError' + '403': + $ref: '#/components/schemas/GenericError' + post: + operationId: core_tenants_create + description: Tenant Viewset + tags: + - core + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/TenantRequest' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/TenantRequest' + multipart/form-data: + schema: + $ref: '#/components/schemas/TenantRequest' + required: true + security: + - authentik: [] + - cookieAuth: [] + responses: + '201': + content: + application/json: + schema: + $ref: '#/components/schemas/Tenant' + description: '' + '400': + $ref: '#/components/schemas/ValidationError' + '403': + $ref: '#/components/schemas/GenericError' + /api/v2beta/core/tenants/{tenant_uuid}/: + get: + operationId: core_tenants_retrieve + description: Tenant Viewset + parameters: + - in: path + name: tenant_uuid + schema: + type: string + format: uuid + description: A UUID string identifying this Tenant. + required: true + tags: + - core + security: + - authentik: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/Tenant' + description: '' + '400': + $ref: '#/components/schemas/ValidationError' + '403': + $ref: '#/components/schemas/GenericError' + put: + operationId: core_tenants_update + description: Tenant Viewset + parameters: + - in: path + name: tenant_uuid + schema: + type: string + format: uuid + description: A UUID string identifying this Tenant. + required: true + tags: + - core + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/TenantRequest' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/TenantRequest' + multipart/form-data: + schema: + $ref: '#/components/schemas/TenantRequest' + required: true + security: + - authentik: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/Tenant' + description: '' + '400': + $ref: '#/components/schemas/ValidationError' + '403': + $ref: '#/components/schemas/GenericError' + patch: + operationId: core_tenants_partial_update + description: Tenant Viewset + parameters: + - in: path + name: tenant_uuid + schema: + type: string + format: uuid + description: A UUID string identifying this Tenant. + required: true + tags: + - core + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedTenantRequest' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedTenantRequest' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedTenantRequest' + security: + - authentik: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/Tenant' + description: '' + '400': + $ref: '#/components/schemas/ValidationError' + '403': + $ref: '#/components/schemas/GenericError' + delete: + operationId: core_tenants_destroy + description: Tenant Viewset + parameters: + - in: path + name: tenant_uuid + schema: + type: string + format: uuid + description: A UUID string identifying this Tenant. + required: true + tags: + - core + security: + - authentik: [] + - cookieAuth: [] + responses: + '204': + description: No response body + '400': + $ref: '#/components/schemas/ValidationError' + '403': + $ref: '#/components/schemas/GenericError' + /api/v2beta/core/tenants/current/: + get: + operationId: core_tenants_current_retrieve + description: Get current tenant + tags: + - core + security: + - authentik: [] + - cookieAuth: [] + - {} + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/CurrentTenant' + description: '' + '400': + $ref: '#/components/schemas/ValidationError' + '403': + $ref: '#/components/schemas/GenericError' /api/v2beta/core/tokens/: get: operationId: core_tokens_list @@ -15291,6 +15516,7 @@ components: - authentik.stages.user_login - authentik.stages.user_logout - authentik.stages.user_write + - authentik.tenants - authentik.core - authentik.managed type: string @@ -16151,17 +16377,6 @@ components: type: object description: Serialize authentik Config into DRF Object properties: - branding_logo: - type: string - readOnly: true - branding_title: - type: string - readOnly: true - ui_footer_links: - type: array - items: - $ref: '#/components/schemas/FooterLink' - readOnly: true error_reporting_enabled: type: boolean readOnly: true @@ -16176,13 +16391,10 @@ components: items: $ref: '#/components/schemas/CapabilitiesEnum' required: - - branding_logo - - branding_title - capabilities - error_reporting_enabled - error_reporting_environment - error_reporting_send_pii - - ui_footer_links ConsentChallenge: type: object description: Challenge info for consent screens @@ -16298,6 +16510,28 @@ components: required: - x_cord - y_cord + CurrentTenant: + type: object + description: Partial tenant information for styling + properties: + branding_title: + type: string + branding_logo: + type: string + ui_footer_links: + type: array + items: + $ref: '#/components/schemas/FooterLink' + readOnly: true + default: + - href: https://goauthentik.io/docs/ + name: Documentation + - href: https://goauthentik.io/ + name: authentik Website + required: + - branding_logo + - branding_title + - ui_footer_links DenyStage: type: object description: DenyStage Serializer @@ -20893,6 +21127,41 @@ components: required: - pagination - results + PaginatedTenantList: + 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/Tenant' + required: + - pagination + - results PaginatedTokenList: type: object properties: @@ -22830,6 +23099,40 @@ components: type: string description: The human-readable name of this device. maxLength: 64 + PatchedTenantRequest: + type: object + description: Tenant Serializer + properties: + domain: + type: string + description: Domain that activates this tenant. Can be a superset, i.e. + `a.b` for `aa.b` and `ba.b` + default: + type: boolean + branding_title: + type: string + branding_logo: + type: string + flow_authentication: + type: string + format: uuid + nullable: true + flow_invalidation: + type: string + format: uuid + nullable: true + flow_recovery: + type: string + format: uuid + nullable: true + flow_enrollment: + type: string + format: uuid + nullable: true + flow_unenrollment: + type: string + format: uuid + nullable: true PatchedTokenRequest: type: object description: Token Serializer @@ -24779,6 +25082,83 @@ components: - task_description - task_finish_timestamp - task_name + Tenant: + type: object + description: Tenant Serializer + properties: + tenant_uuid: + type: string + format: uuid + readOnly: true + domain: + type: string + description: Domain that activates this tenant. Can be a superset, i.e. + `a.b` for `aa.b` and `ba.b` + default: + type: boolean + branding_title: + type: string + branding_logo: + type: string + flow_authentication: + type: string + format: uuid + nullable: true + flow_invalidation: + type: string + format: uuid + nullable: true + flow_recovery: + type: string + format: uuid + nullable: true + flow_enrollment: + type: string + format: uuid + nullable: true + flow_unenrollment: + type: string + format: uuid + nullable: true + required: + - domain + - tenant_uuid + TenantRequest: + type: object + description: Tenant Serializer + properties: + domain: + type: string + description: Domain that activates this tenant. Can be a superset, i.e. + `a.b` for `aa.b` and `ba.b` + default: + type: boolean + branding_title: + type: string + branding_logo: + type: string + flow_authentication: + type: string + format: uuid + nullable: true + flow_invalidation: + type: string + format: uuid + nullable: true + flow_recovery: + type: string + format: uuid + nullable: true + flow_enrollment: + type: string + format: uuid + nullable: true + flow_unenrollment: + type: string + format: uuid + nullable: true + required: + - domain Token: type: object description: Token Serializer diff --git a/web/src/api/Config.ts b/web/src/api/Config.ts index 83374bab8..5190778b7 100644 --- a/web/src/api/Config.ts +++ b/web/src/api/Config.ts @@ -1,4 +1,4 @@ -import { Config, Configuration, Middleware, ResponseContext, RootApi } from "authentik-api"; +import { Config, Configuration, CoreApi, CurrentTenant, Middleware, ResponseContext, RootApi, Tenant } from "authentik-api"; import { getCookie } from "../utils"; import { API_DRAWER_MIDDLEWARE } from "../elements/notifications/APIDrawer"; import { MessageMiddleware } from "../elements/messages/Middleware"; @@ -20,6 +20,14 @@ export function config(): Promise { return globalConfigPromise; } +let globalTenantPromise: Promise; +export function tenant(): Promise { + if (!globalTenantPromise) { + globalTenantPromise = new CoreApi(DEFAULT_CONFIG).coreTenantsCurrentRetrieve(); + } + return globalTenantPromise; +} + export const DEFAULT_CONFIG = new Configuration({ basePath: "", headers: { diff --git a/web/src/elements/PageHeader.ts b/web/src/elements/PageHeader.ts index e0aff235a..1ec36d1cb 100644 --- a/web/src/elements/PageHeader.ts +++ b/web/src/elements/PageHeader.ts @@ -5,7 +5,7 @@ import AKGlobal from "../authentik.css"; import PFBase from "@patternfly/patternfly/patternfly-base.css"; import PFButton from "@patternfly/patternfly/components/Button/button.css"; import { EVENT_SIDEBAR_TOGGLE, TITLE_DEFAULT } from "../constants"; -import { config } from "../api/Config"; +import { tenant } from "../api/Config"; @customElement("ak-page-header") export class PageHeader extends LitElement { @@ -18,11 +18,11 @@ export class PageHeader extends LitElement { @property() set header(value: string) { - config().then(config => { + tenant().then(tenant => { if (value !== "") { - document.title = `${value} - ${config.brandingTitle}`; + document.title = `${value} - ${tenant.brandingTitle}`; } else { - document.title = config.brandingTitle || TITLE_DEFAULT; + document.title = tenant.brandingTitle || TITLE_DEFAULT; } }); this._header = value; diff --git a/web/src/elements/sidebar/SidebarBrand.ts b/web/src/elements/sidebar/SidebarBrand.ts index c4a1a8f7d..a9392cf6c 100644 --- a/web/src/elements/sidebar/SidebarBrand.ts +++ b/web/src/elements/sidebar/SidebarBrand.ts @@ -6,29 +6,25 @@ import PFBase from "@patternfly/patternfly/patternfly-base.css"; import AKGlobal from "../../authentik.css"; import { configureSentry } from "../../api/Sentry"; -import { Config } from "authentik-api"; +import { CurrentTenant } from "authentik-api"; import { ifDefined } from "lit-html/directives/if-defined"; import { EVENT_SIDEBAR_TOGGLE } from "../../constants"; +import { tenant } from "../../api/Config"; // If the viewport is wider than MIN_WIDTH, the sidebar // is shown besides the content, and not overlayed. export const MIN_WIDTH = 1200; -export const DefaultConfig: Config = { +export const DefaultTenant: CurrentTenant = { brandingLogo: " /static/dist/assets/icons/icon_left_brand.svg", brandingTitle: "authentik", - - errorReportingEnabled: false, - errorReportingEnvironment: "", - errorReportingSendPii: false, uiFooterLinks: [], - capabilities: [], }; @customElement("ak-sidebar-brand") export class SidebarBrand extends LitElement { @property({attribute: false}) - config: Config = DefaultConfig; + tenant: CurrentTenant = DefaultTenant; static get styles(): CSSResult[] { return [ @@ -68,7 +64,8 @@ export class SidebarBrand extends LitElement { } firstUpdated(): void { - configureSentry(true).then((c) => {this.config = c;}); + configureSentry(true); + tenant().then(tenant => this.tenant = tenant); } render(): TemplateResult { @@ -89,7 +86,7 @@ export class SidebarBrand extends LitElement { ` : html``}
- authentik icon + authentik icon
`; } diff --git a/web/src/flows/FlowExecutor.ts b/web/src/flows/FlowExecutor.ts index 98bdf4fd8..8f580717a 100644 --- a/web/src/flows/FlowExecutor.ts +++ b/web/src/flows/FlowExecutor.ts @@ -26,8 +26,8 @@ import "./stages/password/PasswordStage"; import "./stages/prompt/PromptStage"; import "./sources/plex/PlexLoginInit"; import { StageHost } from "./stages/base"; -import { ChallengeChoices, Config, FlowChallengeRequest, FlowChallengeResponseRequest, FlowsApi, RedirectChallenge, ShellChallenge } from "authentik-api"; -import { config, DEFAULT_CONFIG } from "../api/Config"; +import { ChallengeChoices, CurrentTenant, FlowChallengeRequest, FlowChallengeResponseRequest, FlowsApi, RedirectChallenge, ShellChallenge } from "authentik-api"; +import { DEFAULT_CONFIG, tenant } from "../api/Config"; import { ifDefined } from "lit-html/directives/if-defined"; import { until } from "lit-html/directives/until"; import { PFSize } from "../elements/Spinner"; @@ -46,7 +46,7 @@ export class FlowExecutor extends LitElement implements StageHost { loading = false; @property({ attribute: false }) - config?: Config; + tenant?: CurrentTenant; static get styles(): CSSResult[] { return [PFBase, PFLogin, PFButton, PFTitle, PFList, PFBackgroundImage, AKGlobal].concat(css` @@ -85,11 +85,11 @@ export class FlowExecutor extends LitElement implements StageHost { } private postUpdate(): void { - config().then(config => { + tenant().then(tenant => { if (this.challenge?.title) { - document.title = `${this.challenge.title} - ${config.brandingTitle}`; + document.title = `${this.challenge.title} - ${tenant.brandingTitle}`; } else { - document.title = config.brandingTitle || TITLE_DEFAULT; + document.title = tenant.brandingTitle || TITLE_DEFAULT; } }); } @@ -115,9 +115,8 @@ export class FlowExecutor extends LitElement implements StageHost { } firstUpdated(): void { - configureSentry().then((config) => { - this.config = config; - }); + configureSentry(); + tenant().then(tenant => this.tenant = tenant); this.loading = true; new FlowsApi(DEFAULT_CONFIG).flowsExecutorGet({ flowSlug: this.flowSlug, @@ -255,7 +254,7 @@ export class FlowExecutor extends LitElement implements StageHost {