Merge pull request #940 from goauthentik/tenant

Tenancy
This commit is contained in:
Jens L 2021-05-29 21:22:10 +02:00 committed by GitHub
commit 6232333a52
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
42 changed files with 1476 additions and 124 deletions

View file

@ -3,7 +3,7 @@
{% load static %} {% load static %}
{% block title %} {% block title %}
API Browser - {{ config.authentik.branding.title }} API Browser - {{ tenant.branding_title }}
{% endblock %} {% endblock %}
{% block head %} {% block head %}

View file

@ -14,13 +14,6 @@ from authentik.core.api.utils import PassiveSerializer
from authentik.lib.config import CONFIG 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): class Capabilities(models.TextChoices):
"""Define capabilities which influence which APIs can/should be used""" """Define capabilities which influence which APIs can/should be used"""
@ -30,10 +23,6 @@ class Capabilities(models.TextChoices):
class ConfigSerializer(PassiveSerializer): class ConfigSerializer(PassiveSerializer):
"""Serialize authentik Config into DRF Object""" """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_enabled = BooleanField(read_only=True)
error_reporting_environment = CharField(read_only=True) error_reporting_environment = CharField(read_only=True)
error_reporting_send_pii = BooleanField(read_only=True) error_reporting_send_pii = BooleanField(read_only=True)
@ -59,12 +48,9 @@ class ConfigView(APIView):
"""Retrive public configuration options""" """Retrive public configuration options"""
config = ConfigSerializer( 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_enabled": CONFIG.y("error_reporting.enabled"),
"error_reporting_environment": CONFIG.y("error_reporting.environment"), "error_reporting_environment": CONFIG.y("error_reporting.environment"),
"error_reporting_send_pii": CONFIG.y("error_reporting.send_pii"), "error_reporting_send_pii": CONFIG.y("error_reporting.send_pii"),
"ui_footer_links": CONFIG.y("authentik.footer_links"),
"capabilities": self.get_capabilities(), "capabilities": self.get_capabilities(),
} }
) )

View file

@ -100,6 +100,7 @@ from authentik.stages.user_delete.api import UserDeleteStageViewSet
from authentik.stages.user_login.api import UserLoginStageViewSet from authentik.stages.user_login.api import UserLoginStageViewSet
from authentik.stages.user_logout.api import UserLogoutStageViewSet from authentik.stages.user_logout.api import UserLogoutStageViewSet
from authentik.stages.user_write.api import UserWriteStageViewSet from authentik.stages.user_write.api import UserWriteStageViewSet
from authentik.tenants.api import TenantViewSet
router = routers.DefaultRouter() router = routers.DefaultRouter()
@ -111,6 +112,7 @@ router.register("core/groups", GroupViewSet)
router.register("core/users", UserViewSet) router.register("core/users", UserViewSet)
router.register("core/user_consent", UserConsentViewSet) router.register("core/user_consent", UserConsentViewSet)
router.register("core/tokens", TokenViewSet) router.register("core/tokens", TokenViewSet)
router.register("core/tenants", TenantViewSet)
router.register("outposts/instances", OutpostViewSet) router.register("outposts/instances", OutpostViewSet)
router.register("outposts/service_connections/all", ServiceConnectionViewSet) router.register("outposts/service_connections/all", ServiceConnectionViewSet)

View file

@ -32,7 +32,7 @@ from authentik.core.middleware import (
) )
from authentik.core.models import Token, TokenIntents, User from authentik.core.models import Token, TokenIntents, User
from authentik.events.models import EventAction from authentik.events.models import EventAction
from authentik.flows.models import Flow, FlowDesignation from authentik.tenants.models import Tenant
class UserSerializer(ModelSerializer): class UserSerializer(ModelSerializer):
@ -179,8 +179,9 @@ class UserViewSet(ModelViewSet):
# pylint: disable=invalid-name, unused-argument # pylint: disable=invalid-name, unused-argument
def recovery(self, request: Request, pk: int) -> Response: def recovery(self, request: Request, pk: int) -> Response:
"""Create a temporary link that a user can use to recover their accounts""" """Create a temporary link that a user can use to recover their accounts"""
tenant: Tenant = request._request.tenant
# 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 = Flow.with_policy(request, designation=FlowDesignation.RECOVERY) flow = tenant.flow_recovery
if not flow: if not flow:
raise Http404 raise Http404
user: User = self.get_object() user: User = self.get_object()
@ -191,7 +192,8 @@ class UserViewSet(ModelViewSet):
) )
querystring = urlencode({"token": token.key}) querystring = urlencode({"token": token.key})
link = request.build_absolute_uri( link = request.build_absolute_uri(
reverse_lazy("authentik_flows:default-recovery") + f"?{querystring}" reverse_lazy("authentik_core:if-flow", kwargs={"flow_slug": flow.slug})
+ f"?{querystring}"
) )
return Response({"link": link}) return Response({"link": link})

View file

@ -42,10 +42,14 @@ class RequestIDMiddleware:
if not hasattr(request, "request_id"): if not hasattr(request, "request_id"):
request_id = uuid4().hex request_id = uuid4().hex
setattr(request, "request_id", request_id) setattr(request, "request_id", request_id)
LOCAL.authentik = {"request_id": request_id} LOCAL.authentik = {
"request_id": request_id,
"host": request.get_host(),
}
response = self.get_response(request) response = self.get_response(request)
response[RESPONSE_HEADER_ID] = request.request_id response[RESPONSE_HEADER_ID] = request.request_id
del LOCAL.authentik["request_id"] del LOCAL.authentik["request_id"]
del LOCAL.authentik["host"]
return response return response
@ -54,4 +58,5 @@ def structlog_add_request_id(logger: Logger, method_name: str, event_dict):
"""If threadlocal has authentik defined, add request_id to log""" """If threadlocal has authentik defined, add request_id to log"""
if hasattr(LOCAL, "authentik"): if hasattr(LOCAL, "authentik"):
event_dict["request_id"] = LOCAL.authentik.get("request_id", "") event_dict["request_id"] = LOCAL.authentik.get("request_id", "")
event_dict["host"] = LOCAL.authentik.get("host", "")
return event_dict return event_dict

View file

@ -7,7 +7,7 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<title>{% block title %}{% trans title|default:config.authentik.branding.title %}{% endblock %}</title> <title>{% block title %}{% trans title|default:tenant.branding_title %}{% endblock %}</title>
<link rel="icon" type="image/png" href="{% static 'dist/assets/icons/icon.png' %}?v={{ ak_version }}"> <link rel="icon" type="image/png" href="{% static 'dist/assets/icons/icon.png' %}?v={{ ak_version }}">
<link rel="shortcut icon" type="image/png" href="{% static 'dist/assets/icons/icon.png' %}?v={{ ak_version }}"> <link rel="shortcut icon" type="image/png" href="{% static 'dist/assets/icons/icon.png' %}?v={{ ak_version }}">
<link rel="stylesheet" type="text/css" href="{% static 'dist/patternfly-base.css' %}?v={{ ak_version }}"> <link rel="stylesheet" type="text/css" href="{% static 'dist/patternfly-base.css' %}?v={{ ak_version }}">

View file

@ -26,10 +26,7 @@
<div class="ak-login-container"> <div class="ak-login-container">
<header class="pf-c-login__header"> <header class="pf-c-login__header">
<div class="pf-c-brand ak-brand"> <div class="pf-c-brand ak-brand">
<img src="{{ config.authentik.branding.logo }}" alt="authentik icon" /> <img src="{{ tenant.branding_logo }}" alt="authentik icon" />
{% if config.authentik.branding.title_show %}
<p>{{ config.authentik.branding.title }}</p>
{% endif %}
</div> </div>
</header> </header>
{% block main_container %} {% block main_container %}
@ -49,12 +46,12 @@
<footer class="pf-c-login__footer"> <footer class="pf-c-login__footer">
<p></p> <p></p>
<ul class="pf-c-list pf-m-inline"> <ul class="pf-c-list pf-m-inline">
{% for link in config.authentik.footer_links %} {% for link in footer_links %}
<li> <li>
<a href="{{ link.href }}">{{ link.name }}</a> <a href="{{ link.href }}">{{ link.name }}</a>
</li> </li>
{% endfor %} {% endfor %}
{% if config.authentik.branding.title != "authentik" %} {% if tenant.branding_title != "authentik" %}
<li> <li>
<a href="https://goauthentik.io"> <a href="https://goauthentik.io">
{% trans 'Powered by authentik' %} {% trans 'Powered by authentik' %}

View file

@ -142,11 +142,6 @@ class Flow(SerializerModel, PolicyBindingModel):
LOGGER.debug("with_policy: no flow found", filters=flow_filter) LOGGER.debug("with_policy: no flow found", filters=flow_filter)
return None return None
def related_flow(self, designation: str, request: HttpRequest) -> Optional["Flow"]:
"""Get a related flow with `designation`. Currently this only queries
Flows by `designation`, but will eventually use `self` for related lookups."""
return Flow.with_policy(request, designation=designation)
def __str__(self) -> str: def __str__(self) -> str:
return f"Flow {self.name} ({self.slug})" return f"Flow {self.name} ({self.slug})"

View file

@ -16,21 +16,6 @@ urlpatterns = [
ToDefaultFlow.as_view(designation=FlowDesignation.INVALIDATION), ToDefaultFlow.as_view(designation=FlowDesignation.INVALIDATION),
name="default-invalidation", name="default-invalidation",
), ),
path(
"-/default/recovery/",
ToDefaultFlow.as_view(designation=FlowDesignation.RECOVERY),
name="default-recovery",
),
path(
"-/default/enrollment/",
ToDefaultFlow.as_view(designation=FlowDesignation.ENROLLMENT),
name="default-enrollment",
),
path(
"-/default/unenrollment/",
ToDefaultFlow.as_view(designation=FlowDesignation.UNRENOLLMENT),
name="default-unenrollment",
),
path("-/cancel/", CancelView.as_view(), name="cancel"), path("-/cancel/", CancelView.as_view(), name="cancel"),
path( path(
"-/configure/<uuid:stage_uuid>/", "-/configure/<uuid:stage_uuid>/",

View file

@ -44,6 +44,7 @@ from authentik.flows.planner import (
) )
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.tenants.models import Tenant
LOGGER = get_logger() LOGGER = get_logger()
# Argument used to redirect user after login # Argument used to redirect user after login
@ -366,7 +367,17 @@ class ToDefaultFlow(View):
designation: Optional[FlowDesignation] = None designation: Optional[FlowDesignation] = None
def dispatch(self, request: HttpRequest) -> HttpResponse: def dispatch(self, request: HttpRequest) -> HttpResponse:
flow = Flow.with_policy(request, designation=self.designation) tenant: Tenant = request.tenant
flow = None
# First, attempt to get default flow from tenant
if self.designation == FlowDesignation.AUTHENTICATION:
flow = tenant.flow_authentication
if self.designation == FlowDesignation.INVALIDATION:
flow = tenant.flow_invalidation
# If no flow was set, get the first based on slug and policy
if not flow:
flow = Flow.with_policy(request, designation=self.designation)
# If we still don't have a flow, 404
if not flow: if not flow:
raise Http404 raise Http404
# If user already has a pending plan, clear it so we don't have to later. # If user already has a pending plan, clear it so we don't have to later.

View file

@ -10,9 +10,6 @@ from urllib.parse import urlparse
import yaml import yaml
from django.conf import ImproperlyConfigured from django.conf import ImproperlyConfigured
from django.http import HttpRequest
from authentik import __version__
SEARCH_PATHS = ["authentik/lib/default.yml", "/etc/authentik/config.yml", ""] + glob( SEARCH_PATHS = ["authentik/lib/default.yml", "/etc/authentik/config.yml", ""] + glob(
"/etc/authentik/config.d/*.yml", recursive=True "/etc/authentik/config.d/*.yml", recursive=True
@ -21,11 +18,6 @@ ENV_PREFIX = "AUTHENTIK"
ENVIRONMENT = os.getenv(f"{ENV_PREFIX}_ENV", "local") ENVIRONMENT = os.getenv(f"{ENV_PREFIX}_ENV", "local")
def context_processor(request: HttpRequest) -> dict[str, Any]:
"""Context Processor that injects config object into every template"""
return {"config": CONFIG.raw, "ak_version": __version__}
class ConfigLoader: class ConfigLoader:
"""Search through SEARCH_PATHS and load configuration. Environment variables starting with """Search through SEARCH_PATHS and load configuration. Environment variables starting with
`ENV_PREFIX` are also applied. `ENV_PREFIX` are also applied.

View file

@ -48,9 +48,6 @@ outposts:
authentik: authentik:
avatars: gravatar # gravatar or none avatars: gravatar # gravatar or none
geoip: "" geoip: ""
branding:
title: authentik
logo: /static/dist/assets/icons/icon_left_brand.svg
# Optionally add links to the footer on the login page # Optionally add links to the footer on the login page
footer_links: footer_links:
- name: Documentation - name: Documentation

View file

@ -6,7 +6,7 @@ class AuthentikManagedConfig(AppConfig):
"""authentik Managed app""" """authentik Managed app"""
name = "authentik.managed" name = "authentik.managed"
label = "authentik_Managed" label = "authentik_managed"
verbose_name = "authentik Managed" verbose_name = "authentik Managed"
def ready(self) -> None: def ready(self) -> None:

View file

@ -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.",
),
),
]

View file

@ -4,7 +4,7 @@
{% load i18n %} {% load i18n %}
{% block title %} {% block title %}
{% trans 'Permission denied' %} - {{ config.authentik.branding.title }} {% trans 'Permission denied' %} - {{ tenant.branding_title }}
{% endblock %} {% endblock %}
{% block card_title %} {% block card_title %}

View file

@ -14,7 +14,7 @@
{% endblock %} {% endblock %}
{% block title %} {% block title %}
{% trans 'End session' %} - {{ config.authentik.branding.title }} {% trans 'End session' %} - {{ tenant.branding_title }}
{% endblock %} {% endblock %}
{% block card_title %} {% block card_title %}

View file

@ -127,6 +127,7 @@ INSTALLED_APPS = [
"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",
"rest_framework", "rest_framework",
"django_filters", "django_filters",
"drf_spectacular", "drf_spectacular",
@ -208,6 +209,7 @@ MIDDLEWARE = [
"django.contrib.sessions.middleware.SessionMiddleware", "django.contrib.sessions.middleware.SessionMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware", "django.contrib.auth.middleware.AuthenticationMiddleware",
"authentik.core.middleware.RequestIDMiddleware", "authentik.core.middleware.RequestIDMiddleware",
"authentik.tenants.middleware.TenantMiddleware",
"authentik.events.middleware.AuditMiddleware", "authentik.events.middleware.AuditMiddleware",
"django.middleware.security.SecurityMiddleware", "django.middleware.security.SecurityMiddleware",
"django.middleware.common.CommonMiddleware", "django.middleware.common.CommonMiddleware",
@ -231,7 +233,7 @@ TEMPLATES = [
"django.template.context_processors.request", "django.template.context_processors.request",
"django.contrib.auth.context_processors.auth", "django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages", "django.contrib.messages.context_processors.messages",
"authentik.lib.config.context_processor", "authentik.tenants.utils.context_processor",
], ],
}, },
}, },

View file

@ -0,0 +1,33 @@
# Generated by Django 3.2.3 on 2021-05-29 16:10
import django.contrib.postgres.fields
from django.db import migrations, models
import authentik.stages.authenticator_validate.models
class Migration(migrations.Migration):
dependencies = [
("authentik_stages_authenticator_validate", "0007_auto_20210403_0927"),
]
operations = [
migrations.AlterField(
model_name="authenticatorvalidatestage",
name="device_classes",
field=django.contrib.postgres.fields.ArrayField(
base_field=models.TextField(
choices=[
("static", "Static"),
("totp", "TOTP"),
("webauthn", "WebAuthn"),
("duo", "Duo"),
]
),
default=authentik.stages.authenticator_validate.models.default_device_classes,
help_text="Device classes which can be used to authenticate",
size=None,
),
),
]

View file

76
authentik/tenants/api.py Normal file
View file

@ -0,0 +1,76 @@
"""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_unenrollment",
]
class CurrentTenantSerializer(PassiveSerializer):
"""Partial tenant information for styling"""
matched_domain = CharField(source="domain")
branding_title = CharField()
branding_logo = CharField()
ui_footer_links = ListField(
child=FooterLinkSerializer(),
read_only=True,
default=CONFIG.y("authentik.footer_links"),
)
flow_unenrollment = CharField(source="flow_unenrollment.slug", required=False)
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)

10
authentik/tenants/apps.py Normal file
View file

@ -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"

View file

@ -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)

View file

@ -0,0 +1,86 @@
# Generated by Django 3.2.3 on 2021-05-29 16:10
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_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",
},
),
]

View file

@ -0,0 +1,40 @@
# Generated by Django 3.2.3 on 2021-05-29 16:55
from django.apps.registry import Apps
from django.db import migrations
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
def create_default_tenant(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
Flow = apps.get_model("authentik_flows", "Flow")
Tenant = apps.get_model("authentik_tenants", "Tenant")
db_alias = schema_editor.connection.alias
default_authentication = (
Flow.objects.using(db_alias).filter(slug="default-authentication-flow").first()
)
default_invalidation = (
Flow.objects.using(db_alias).filter(slug="default-invalidation-flow").first()
)
tenant, _ = Tenant.objects.using(db_alias).update_or_create(
domain="authentik-default",
default=True,
defaults={
"flow_authentication": default_authentication,
"flow_invalidation": default_invalidation,
},
)
class Migration(migrations.Migration):
dependencies = [
("authentik_tenants", "0001_initial"),
("authentik_flows", "0008_default_flows"),
]
operations = [
migrations.RunPython(create_default_tenant),
]

View file

View file

@ -0,0 +1,48 @@
"""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_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")

View file

@ -0,0 +1,36 @@
"""Test tenants"""
from django.test import TestCase
from django.urls import reverse
from django.utils.encoding import force_str
from authentik.lib.config import CONFIG
from authentik.tenants.models import Tenant
class TestTenants(TestCase):
"""Test tenants"""
def test_current_tenant(self):
"""Test Current tenant API"""
self.assertJSONEqual(
force_str(self.client.get(reverse("authentik_api:tenant-current")).content),
{
"branding_logo": "/static/dist/assets/icons/icon_left_brand.svg",
"branding_title": "authentik",
"matched_domain": "authentik-default",
"ui_footer_links": CONFIG.y("authentik.footer_links"),
},
)
def test_fallback(self):
"""Test fallback tenant"""
Tenant.objects.all().delete()
self.assertJSONEqual(
force_str(self.client.get(reverse("authentik_api:tenant-current")).content),
{
"branding_logo": "/static/dist/assets/icons/icon_left_brand.svg",
"branding_title": "authentik",
"matched_domain": "fallback",
"ui_footer_links": CONFIG.y("authentik.footer_links"),
},
)

View file

@ -0,0 +1,30 @@
"""Tenant utilities"""
from typing import Any
from django.db.models import Q
from django.http.request import HttpRequest
from authentik import __version__
from authentik.lib.config import CONFIG
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(domain="fallback")
return db_tenants.first()
def context_processor(request: HttpRequest) -> dict[str, Any]:
"""Context Processor that injects tenant object into every template"""
return {
"tenant": request.tenant,
"ak_version": __version__,
"footer_links": CONFIG.y("authentik.footer_links"),
}

View file

@ -1712,6 +1712,231 @@ paths:
$ref: '#/components/schemas/ValidationError' $ref: '#/components/schemas/ValidationError'
'403': '403':
$ref: '#/components/schemas/GenericError' $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/: /api/v2beta/core/tokens/:
get: get:
operationId: core_tokens_list operationId: core_tokens_list
@ -15291,6 +15516,7 @@ components:
- 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.core - authentik.core
- authentik.managed - authentik.managed
type: string type: string
@ -16151,17 +16377,6 @@ components:
type: object type: object
description: Serialize authentik Config into DRF Object description: Serialize authentik Config into DRF Object
properties: 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: error_reporting_enabled:
type: boolean type: boolean
readOnly: true readOnly: true
@ -16176,13 +16391,10 @@ components:
items: items:
$ref: '#/components/schemas/CapabilitiesEnum' $ref: '#/components/schemas/CapabilitiesEnum'
required: required:
- branding_logo
- branding_title
- capabilities - capabilities
- error_reporting_enabled - error_reporting_enabled
- error_reporting_environment - error_reporting_environment
- error_reporting_send_pii - error_reporting_send_pii
- ui_footer_links
ConsentChallenge: ConsentChallenge:
type: object type: object
description: Challenge info for consent screens description: Challenge info for consent screens
@ -16298,6 +16510,33 @@ components:
required: required:
- x_cord - x_cord
- y_cord - y_cord
CurrentTenant:
type: object
description: Partial tenant information for styling
properties:
matched_domain:
type: string
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
flow_unenrollment:
type: string
required:
- branding_logo
- branding_title
- matched_domain
- ui_footer_links
DenyStage: DenyStage:
type: object type: object
description: DenyStage Serializer description: DenyStage Serializer
@ -20893,6 +21132,41 @@ components:
required: required:
- pagination - pagination
- results - 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: PaginatedTokenList:
type: object type: object
properties: properties:
@ -22830,6 +23104,36 @@ components:
type: string type: string
description: The human-readable name of this device. description: The human-readable name of this device.
maxLength: 64 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_unenrollment:
type: string
format: uuid
nullable: true
PatchedTokenRequest: PatchedTokenRequest:
type: object type: object
description: Token Serializer description: Token Serializer
@ -24779,6 +25083,75 @@ components:
- task_description - task_description
- task_finish_timestamp - task_finish_timestamp
- task_name - 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_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_unenrollment:
type: string
format: uuid
nullable: true
required:
- domain
Token: Token:
type: object type: object
description: Token Serializer description: Token Serializer

View file

@ -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 { getCookie } from "../utils";
import { API_DRAWER_MIDDLEWARE } from "../elements/notifications/APIDrawer"; import { API_DRAWER_MIDDLEWARE } from "../elements/notifications/APIDrawer";
import { MessageMiddleware } from "../elements/messages/Middleware"; import { MessageMiddleware } from "../elements/messages/Middleware";
@ -6,7 +6,9 @@ import { MessageMiddleware } from "../elements/messages/Middleware";
export class LoggingMiddleware implements Middleware { export class LoggingMiddleware implements Middleware {
post(context: ResponseContext): Promise<Response | void> { post(context: ResponseContext): Promise<Response | void> {
console.debug(`authentik/api: ${context.response.status} ${context.init.method} ${context.url}`); tenant().then(tenant => {
console.debug(`authentik/api[${tenant.matchedDomain}]: ${context.response.status} ${context.init.method} ${context.url}`);
});
return Promise.resolve(context.response); return Promise.resolve(context.response);
} }
@ -20,6 +22,14 @@ export function config(): Promise<Config> {
return globalConfigPromise; return globalConfigPromise;
} }
let globalTenantPromise: Promise<CurrentTenant>;
export function tenant(): Promise<CurrentTenant> {
if (!globalTenantPromise) {
globalTenantPromise = new CoreApi(DEFAULT_CONFIG).coreTenantsCurrentRetrieve();
}
return globalTenantPromise;
}
export const DEFAULT_CONFIG = new Configuration({ export const DEFAULT_CONFIG = new Configuration({
basePath: "", basePath: "",
headers: { headers: {

View file

@ -11,10 +11,6 @@ export class AppURLManager {
export class FlowURLManager { export class FlowURLManager {
static defaultUnenrollment(): string {
return "/flows/-/default/unenrollment/";
}
static configure(stageUuid: string, rest: string): string { static configure(stageUuid: string, rest: string): string {
return `/flows/-/configure/${stageUuid}/${rest}`; return `/flows/-/configure/${stageUuid}/${rest}`;
} }

View file

@ -5,7 +5,7 @@ import AKGlobal from "../authentik.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css"; import PFBase from "@patternfly/patternfly/patternfly-base.css";
import PFButton from "@patternfly/patternfly/components/Button/button.css"; import PFButton from "@patternfly/patternfly/components/Button/button.css";
import { EVENT_SIDEBAR_TOGGLE, TITLE_DEFAULT } from "../constants"; import { EVENT_SIDEBAR_TOGGLE, TITLE_DEFAULT } from "../constants";
import { config } from "../api/Config"; import { tenant } from "../api/Config";
@customElement("ak-page-header") @customElement("ak-page-header")
export class PageHeader extends LitElement { export class PageHeader extends LitElement {
@ -18,11 +18,11 @@ export class PageHeader extends LitElement {
@property() @property()
set header(value: string) { set header(value: string) {
config().then(config => { tenant().then(tenant => {
if (value !== "") { if (value !== "") {
document.title = `${value} - ${config.brandingTitle}`; document.title = `${value} - ${tenant.brandingTitle}`;
} else { } else {
document.title = config.brandingTitle || TITLE_DEFAULT; document.title = tenant.brandingTitle || TITLE_DEFAULT;
} }
}); });
this._header = value; this._header = value;

View file

@ -6,29 +6,26 @@ import PFBase from "@patternfly/patternfly/patternfly-base.css";
import AKGlobal from "../../authentik.css"; import AKGlobal from "../../authentik.css";
import { configureSentry } from "../../api/Sentry"; import { configureSentry } from "../../api/Sentry";
import { Config } from "authentik-api"; import { CurrentTenant } from "authentik-api";
import { ifDefined } from "lit-html/directives/if-defined"; import { ifDefined } from "lit-html/directives/if-defined";
import { EVENT_SIDEBAR_TOGGLE } from "../../constants"; import { EVENT_SIDEBAR_TOGGLE } from "../../constants";
import { tenant } from "../../api/Config";
// If the viewport is wider than MIN_WIDTH, the sidebar // If the viewport is wider than MIN_WIDTH, the sidebar
// is shown besides the content, and not overlayed. // is shown besides the content, and not overlayed.
export const MIN_WIDTH = 1200; export const MIN_WIDTH = 1200;
export const DefaultConfig: Config = { export const DefaultTenant: CurrentTenant = {
brandingLogo: " /static/dist/assets/icons/icon_left_brand.svg", brandingLogo: " /static/dist/assets/icons/icon_left_brand.svg",
brandingTitle: "authentik", brandingTitle: "authentik",
errorReportingEnabled: false,
errorReportingEnvironment: "",
errorReportingSendPii: false,
uiFooterLinks: [], uiFooterLinks: [],
capabilities: [], matchedDomain: "",
}; };
@customElement("ak-sidebar-brand") @customElement("ak-sidebar-brand")
export class SidebarBrand extends LitElement { export class SidebarBrand extends LitElement {
@property({attribute: false}) @property({attribute: false})
config: Config = DefaultConfig; tenant: CurrentTenant = DefaultTenant;
static get styles(): CSSResult[] { static get styles(): CSSResult[] {
return [ return [
@ -68,7 +65,8 @@ export class SidebarBrand extends LitElement {
} }
firstUpdated(): void { firstUpdated(): void {
configureSentry(true).then((c) => {this.config = c;}); configureSentry(true);
tenant().then(tenant => this.tenant = tenant);
} }
render(): TemplateResult { render(): TemplateResult {
@ -89,7 +87,7 @@ export class SidebarBrand extends LitElement {
` : html``} ` : html``}
<a href="#/" class="pf-c-page__header-brand-link"> <a href="#/" class="pf-c-page__header-brand-link">
<div class="pf-c-brand ak-brand"> <div class="pf-c-brand ak-brand">
<img src="${ifDefined(this.config.brandingLogo)}" alt="authentik icon" loading="lazy" /> <img src="${ifDefined(this.tenant.brandingLogo)}" alt="authentik icon" loading="lazy" />
</div> </div>
</a>`; </a>`;
} }

View file

@ -167,7 +167,7 @@ export abstract class Table<T> extends LitElement {
<tr role="row"> <tr role="row">
<td role="cell" colspan="8"> <td role="cell" colspan="8">
<div class="pf-l-bullseye"> <div class="pf-l-bullseye">
${inner ? inner : html`<ak-empty-state header="${t`No elements found.`}"></ak-empty-state>`} ${inner ? inner : html`<ak-empty-state header="${t`No objects found.`}"></ak-empty-state>`}
</div> </div>
</td> </td>
</tr> </tr>

View file

@ -26,8 +26,8 @@ import "./stages/password/PasswordStage";
import "./stages/prompt/PromptStage"; import "./stages/prompt/PromptStage";
import "./sources/plex/PlexLoginInit"; import "./sources/plex/PlexLoginInit";
import { StageHost } from "./stages/base"; import { StageHost } from "./stages/base";
import { ChallengeChoices, Config, FlowChallengeRequest, FlowChallengeResponseRequest, FlowsApi, RedirectChallenge, ShellChallenge } from "authentik-api"; import { ChallengeChoices, CurrentTenant, FlowChallengeRequest, FlowChallengeResponseRequest, FlowsApi, RedirectChallenge, ShellChallenge } from "authentik-api";
import { config, DEFAULT_CONFIG } from "../api/Config"; import { DEFAULT_CONFIG, tenant } from "../api/Config";
import { ifDefined } from "lit-html/directives/if-defined"; import { ifDefined } from "lit-html/directives/if-defined";
import { until } from "lit-html/directives/until"; import { until } from "lit-html/directives/until";
import { PFSize } from "../elements/Spinner"; import { PFSize } from "../elements/Spinner";
@ -46,7 +46,7 @@ export class FlowExecutor extends LitElement implements StageHost {
loading = false; loading = false;
@property({ attribute: false }) @property({ attribute: false })
config?: Config; tenant?: CurrentTenant;
static get styles(): CSSResult[] { static get styles(): CSSResult[] {
return [PFBase, PFLogin, PFButton, PFTitle, PFList, PFBackgroundImage, AKGlobal].concat(css` return [PFBase, PFLogin, PFButton, PFTitle, PFList, PFBackgroundImage, AKGlobal].concat(css`
@ -85,11 +85,11 @@ export class FlowExecutor extends LitElement implements StageHost {
} }
private postUpdate(): void { private postUpdate(): void {
config().then(config => { tenant().then(tenant => {
if (this.challenge?.title) { if (this.challenge?.title) {
document.title = `${this.challenge.title} - ${config.brandingTitle}`; document.title = `${this.challenge.title} - ${tenant.brandingTitle}`;
} else { } 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 { firstUpdated(): void {
configureSentry().then((config) => { configureSentry();
this.config = config; tenant().then(tenant => this.tenant = tenant);
});
this.loading = true; this.loading = true;
new FlowsApi(DEFAULT_CONFIG).flowsExecutorGet({ new FlowsApi(DEFAULT_CONFIG).flowsExecutorGet({
flowSlug: this.flowSlug, flowSlug: this.flowSlug,
@ -255,7 +254,7 @@ export class FlowExecutor extends LitElement implements StageHost {
<div class="ak-login-container"> <div class="ak-login-container">
<header class="pf-c-login__header"> <header class="pf-c-login__header">
<div class="pf-c-brand ak-brand"> <div class="pf-c-brand ak-brand">
<img src="${ifDefined(this.config?.brandingLogo)}" alt="authentik icon" /> <img src="${ifDefined(this.tenant?.brandingLogo)}" alt="authentik icon" />
</div> </div>
</header> </header>
<div class="pf-c-login__main"> <div class="pf-c-login__main">
@ -264,12 +263,12 @@ export class FlowExecutor extends LitElement implements StageHost {
<footer class="pf-c-login__footer"> <footer class="pf-c-login__footer">
<p></p> <p></p>
<ul class="pf-c-list pf-m-inline"> <ul class="pf-c-list pf-m-inline">
${until(this.config?.uiFooterLinks?.map((link) => { ${until(this.tenant?.uiFooterLinks?.map((link) => {
return html`<li> return html`<li>
<a href="${link.href || ""}">${link.name}</a> <a href="${link.href || ""}">${link.name}</a>
</li>`; </li>`;
}))} }))}
${this.config?.brandingTitle != "authentik" ? html` ${this.tenant?.brandingTitle != "authentik" ? html`
<li><a href="https://goauthentik.io">${t`Powered by authentik`}</a></li>` : html``} <li><a href="https://goauthentik.io">${t`Powered by authentik`}</a></li>` : html``}
</ul> </ul>
</footer> </footer>

View file

@ -40,6 +40,9 @@ export class AdminInterface extends Interface {
<ak-sidebar-item path="/core/providers" .activeWhen=${[`^/core/providers/(?<id>${ID_REGEX})$`]}> <ak-sidebar-item path="/core/providers" .activeWhen=${[`^/core/providers/(?<id>${ID_REGEX})$`]}>
<span slot="label">${t`Providers`}</span> <span slot="label">${t`Providers`}</span>
</ak-sidebar-item> </ak-sidebar-item>
<ak-sidebar-item path="/core/tenants">
<span slot="label">${t`Tenants`}</span>
</ak-sidebar-item>
</ak-sidebar-item> </ak-sidebar-item>
<ak-sidebar-item <ak-sidebar-item
.condition=${superUserCondition}> .condition=${superUserCondition}>

View file

@ -308,6 +308,7 @@ msgstr "Authentication"
#: src/pages/sources/oauth/OAuthSourceForm.ts #: src/pages/sources/oauth/OAuthSourceForm.ts
#: src/pages/sources/plex/PlexSourceForm.ts #: src/pages/sources/plex/PlexSourceForm.ts
#: src/pages/sources/saml/SAMLSourceForm.ts #: src/pages/sources/saml/SAMLSourceForm.ts
#: src/pages/tenants/TenantForm.ts
msgid "Authentication flow" msgid "Authentication flow"
msgstr "Authentication flow" msgstr "Authentication flow"
@ -422,6 +423,14 @@ msgstr "Binding"
msgid "Binding Type" msgid "Binding Type"
msgstr "Binding Type" msgstr "Binding Type"
#: src/pages/tenants/TenantForm.ts
msgid "Branding settings"
msgstr "Branding settings"
#: src/pages/tenants/TenantForm.ts
msgid "Branding shown in page title and several other places."
msgstr "Branding shown in page title and several other places."
#: src/pages/admin-overview/cards/VersionStatusCard.ts #: src/pages/admin-overview/cards/VersionStatusCard.ts
msgid "Build hash: {0}" msgid "Build hash: {0}"
msgstr "Build hash: {0}" msgstr "Build hash: {0}"
@ -522,6 +531,14 @@ msgstr "Changelog"
msgid "Characters which are considered as symbols." msgid "Characters which are considered as symbols."
msgstr "Characters which are considered as symbols." msgstr "Characters which are considered as symbols."
#: src/pages/applications/ApplicationViewPage.ts
msgid "Check"
msgstr "Check"
#: src/pages/applications/ApplicationViewPage.ts
msgid "Check Application access"
msgstr "Check Application access"
#: src/pages/policies/reputation/ReputationPolicyForm.ts #: src/pages/policies/reputation/ReputationPolicyForm.ts
msgid "Check IP" msgid "Check IP"
msgstr "Check IP" msgstr "Check IP"
@ -530,6 +547,10 @@ msgstr "Check IP"
msgid "Check Username" msgid "Check Username"
msgstr "Check Username" msgstr "Check Username"
#: src/pages/applications/ApplicationViewPage.ts
msgid "Check access"
msgstr "Check access"
#: src/flows/stages/authenticator_duo/AuthenticatorDuoStage.ts #: src/flows/stages/authenticator_duo/AuthenticatorDuoStage.ts
msgid "Check status" msgid "Check status"
msgstr "Check status" msgstr "Check status"
@ -664,6 +685,10 @@ msgstr "Configure settings relevant to your user profile."
msgid "Configure the maximum allowed time drift for an asseration." msgid "Configure the maximum allowed time drift for an asseration."
msgstr "Configure the maximum allowed time drift for an asseration." msgstr "Configure the maximum allowed time drift for an asseration."
#: src/pages/tenants/TenantListPage.ts
msgid "Configure visual settings and defaults for different domains."
msgstr "Configure visual settings and defaults for different domains."
#: src/pages/providers/oauth2/OAuth2ProviderForm.ts #: src/pages/providers/oauth2/OAuth2ProviderForm.ts
msgid "Configure what data should be used as unique User Identifier. For most cases, the default should be fine." msgid "Configure what data should be used as unique User Identifier. For most cases, the default should be fine."
msgstr "Configure what data should be used as unique User Identifier. For most cases, the default should be fine." msgstr "Configure what data should be used as unique User Identifier. For most cases, the default should be fine."
@ -800,6 +825,8 @@ msgstr "Copy download URL"
#: src/pages/stages/prompt/PromptListPage.ts #: src/pages/stages/prompt/PromptListPage.ts
#: src/pages/stages/prompt/PromptStageForm.ts #: src/pages/stages/prompt/PromptStageForm.ts
#: src/pages/stages/prompt/PromptStageForm.ts #: src/pages/stages/prompt/PromptStageForm.ts
#: src/pages/tenants/TenantListPage.ts
#: src/pages/tenants/TenantListPage.ts
#: src/pages/user-settings/tokens/UserTokenList.ts #: src/pages/user-settings/tokens/UserTokenList.ts
#: src/pages/user-settings/tokens/UserTokenList.ts #: src/pages/user-settings/tokens/UserTokenList.ts
#: src/pages/users/UserListPage.ts #: src/pages/users/UserListPage.ts
@ -869,6 +896,10 @@ msgstr "Create Stage"
msgid "Create Stage binding" msgid "Create Stage binding"
msgstr "Create Stage binding" msgstr "Create Stage binding"
#: src/pages/tenants/TenantListPage.ts
msgid "Create Tenant"
msgstr "Create Tenant"
#: src/pages/user-settings/tokens/UserTokenList.ts #: src/pages/user-settings/tokens/UserTokenList.ts
msgid "Create Token" msgid "Create Token"
msgstr "Create Token" msgstr "Create Token"
@ -928,6 +959,18 @@ msgstr "Date Time"
msgid "Decides what this Flow is used for. For example, the Authentication flow is redirect to when an un-authenticated user visits authentik." msgid "Decides what this Flow is used for. For example, the Authentication flow is redirect to when an un-authenticated user visits authentik."
msgstr "Decides what this Flow is used for. For example, the Authentication flow is redirect to when an un-authenticated user visits authentik." msgstr "Decides what this Flow is used for. For example, the Authentication flow is redirect to when an un-authenticated user visits authentik."
#: src/pages/tenants/TenantForm.ts
msgid "Default"
msgstr "Default"
#: src/pages/tenants/TenantForm.ts
msgid "Default flows"
msgstr "Default flows"
#: src/pages/tenants/TenantListPage.ts
msgid "Default?"
msgstr "Default?"
#: src/pages/events/TransportListPage.ts #: src/pages/events/TransportListPage.ts
msgid "Define how notifications are sent to users, like Email or Webhook." msgid "Define how notifications are sent to users, like Email or Webhook."
msgstr "Define how notifications are sent to users, like Email or Webhook." msgstr "Define how notifications are sent to users, like Email or Webhook."
@ -948,6 +991,7 @@ msgstr "Define how notifications are sent to users, like Email or Webhook."
#: src/pages/stages/StageListPage.ts #: src/pages/stages/StageListPage.ts
#: src/pages/stages/invitation/InvitationListPage.ts #: src/pages/stages/invitation/InvitationListPage.ts
#: src/pages/stages/prompt/PromptListPage.ts #: src/pages/stages/prompt/PromptListPage.ts
#: src/pages/tenants/TenantListPage.ts
#: src/pages/tokens/TokenListPage.ts #: src/pages/tokens/TokenListPage.ts
#: src/pages/user-settings/settings/UserSettingsAuthenticatorWebAuthn.ts #: src/pages/user-settings/settings/UserSettingsAuthenticatorWebAuthn.ts
#: src/pages/user-settings/tokens/UserTokenList.ts #: src/pages/user-settings/tokens/UserTokenList.ts
@ -1071,6 +1115,11 @@ msgstr "Disconnect"
msgid "Docker URL" msgid "Docker URL"
msgstr "Docker URL" msgstr "Docker URL"
#: src/pages/tenants/TenantForm.ts
#: src/pages/tenants/TenantListPage.ts
msgid "Domain"
msgstr "Domain"
#: src/pages/crypto/CertificateKeyPairListPage.ts #: src/pages/crypto/CertificateKeyPairListPage.ts
#: src/pages/providers/saml/SAMLProviderViewPage.ts #: src/pages/providers/saml/SAMLProviderViewPage.ts
#: src/pages/sources/saml/SAMLSourceViewPage.ts #: src/pages/sources/saml/SAMLSourceViewPage.ts
@ -1133,6 +1182,7 @@ msgstr "Each provider has a different issuer, based on the application slug."
#: src/pages/sources/saml/SAMLSourceViewPage.ts #: src/pages/sources/saml/SAMLSourceViewPage.ts
#: src/pages/stages/StageListPage.ts #: src/pages/stages/StageListPage.ts
#: src/pages/stages/prompt/PromptListPage.ts #: src/pages/stages/prompt/PromptListPage.ts
#: src/pages/tenants/TenantListPage.ts
#: src/pages/user-settings/tokens/UserTokenList.ts #: src/pages/user-settings/tokens/UserTokenList.ts
#: src/pages/users/UserListPage.ts #: src/pages/users/UserListPage.ts
#: src/pages/users/UserViewPage.ts #: src/pages/users/UserViewPage.ts
@ -1477,6 +1527,14 @@ msgstr "Flow used by an authenticated user to configure this Stage. If empty, us
msgid "Flow used for users to authenticate. Currently only identification and password stages are supported." msgid "Flow used for users to authenticate. Currently only identification and password stages are supported."
msgstr "Flow used for users to authenticate. Currently only identification and password stages are supported." msgstr "Flow used for users to authenticate. Currently only identification and password stages are supported."
#: src/pages/tenants/TenantForm.ts
msgid "Flow used to authenticate users. If left empty, the first applicable flow sorted by the slug is used."
msgstr "Flow used to authenticate users. If left empty, the first applicable flow sorted by the slug is used."
#: src/pages/tenants/TenantForm.ts
msgid "Flow used to logout. If left empty, the first applicable flow sorted by the slug is used."
msgstr "Flow used to logout. If left empty, the first applicable flow sorted by the slug is used."
#: src/pages/providers/oauth2/OAuth2ProviderForm.ts #: src/pages/providers/oauth2/OAuth2ProviderForm.ts
#: src/pages/providers/proxy/ProxyProviderForm.ts #: src/pages/providers/proxy/ProxyProviderForm.ts
#: src/pages/providers/saml/SAMLProviderForm.ts #: src/pages/providers/saml/SAMLProviderForm.ts
@ -1625,6 +1683,7 @@ msgstr "Hide service-accounts"
#: src/pages/sources/plex/PlexSourceForm.ts #: src/pages/sources/plex/PlexSourceForm.ts
#: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts #: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts
#: src/pages/stages/identification/IdentificationStageForm.ts #: src/pages/stages/identification/IdentificationStageForm.ts
#: src/pages/stages/identification/IdentificationStageForm.ts
#: src/pages/stages/password/PasswordStageForm.ts #: src/pages/stages/password/PasswordStageForm.ts
#: src/pages/stages/prompt/PromptStageForm.ts #: src/pages/stages/prompt/PromptStageForm.ts
#: src/pages/stages/prompt/PromptStageForm.ts #: src/pages/stages/prompt/PromptStageForm.ts
@ -1643,6 +1702,10 @@ msgstr "ID"
msgid "Icon" msgid "Icon"
msgstr "Icon" msgstr "Icon"
#: src/pages/tenants/TenantForm.ts
msgid "Icon shown in sidebar/header and flow executor."
msgstr "Icon shown in sidebar/header and flow executor."
#: src/pages/flows/FlowListPage.ts #: src/pages/flows/FlowListPage.ts
#: src/pages/system-tasks/SystemTaskListPage.ts #: src/pages/system-tasks/SystemTaskListPage.ts
#: src/pages/tokens/TokenListPage.ts #: src/pages/tokens/TokenListPage.ts
@ -1664,6 +1727,10 @@ msgstr "If enabled, use the local connection. Required Docker socket/Kubernetes
msgid "If left empty, authentik will try to extract the launch URL based on the selected provider." msgid "If left empty, authentik will try to extract the launch URL based on the selected provider."
msgstr "If left empty, authentik will try to extract the launch URL based on the selected provider." msgstr "If left empty, authentik will try to extract the launch URL based on the selected provider."
#: src/pages/tenants/TenantForm.ts
msgid "If set, users are able to unenroll themselves using this flow. If no flow is set, option is not shown."
msgstr "If set, users are able to unenroll themselves using this flow. If no flow is set, option is not shown."
#: src/pages/stages/invitation/InvitationStageForm.ts #: src/pages/stages/invitation/InvitationStageForm.ts
msgid "If this flag is set, this Stage will jump to the next Stage when no Invitation is given. By default this Stage will cancel the Flow when no invitation is given." msgid "If this flag is set, this Stage will jump to the next Stage when no Invitation is given. By default this Stage will cancel the Flow when no invitation is given."
msgstr "If this flag is set, this Stage will jump to the next Stage when no Invitation is given. By default this Stage will cancel the Flow when no invitation is given." msgstr "If this flag is set, this Stage will jump to the next Stage when no Invitation is given. By default this Stage will cancel the Flow when no invitation is given."
@ -1725,6 +1792,10 @@ msgstr "Internal host SSL Validation"
msgid "Invalidation" msgid "Invalidation"
msgstr "Invalidation" msgstr "Invalidation"
#: src/pages/tenants/TenantForm.ts
msgid "Invalidation flow"
msgstr "Invalidation flow"
#: src/interfaces/AdminInterface.ts #: src/interfaces/AdminInterface.ts
#: src/pages/stages/invitation/InvitationListPage.ts #: src/pages/stages/invitation/InvitationListPage.ts
msgid "Invitations" msgid "Invitations"
@ -1866,6 +1937,7 @@ msgid "Loading"
msgstr "Loading" msgstr "Loading"
#: src/elements/Spinner.ts #: src/elements/Spinner.ts
#: src/pages/applications/ApplicationCheckAccessForm.ts
#: src/pages/applications/ApplicationForm.ts #: src/pages/applications/ApplicationForm.ts
#: src/pages/events/RuleForm.ts #: src/pages/events/RuleForm.ts
#: src/pages/events/RuleForm.ts #: src/pages/events/RuleForm.ts
@ -1916,9 +1988,14 @@ msgstr "Loading"
#: src/pages/stages/email/EmailStageForm.ts #: src/pages/stages/email/EmailStageForm.ts
#: src/pages/stages/identification/IdentificationStageForm.ts #: src/pages/stages/identification/IdentificationStageForm.ts
#: src/pages/stages/identification/IdentificationStageForm.ts #: src/pages/stages/identification/IdentificationStageForm.ts
#: src/pages/stages/identification/IdentificationStageForm.ts
#: src/pages/stages/password/PasswordStageForm.ts #: src/pages/stages/password/PasswordStageForm.ts
#: src/pages/stages/prompt/PromptStageForm.ts #: src/pages/stages/prompt/PromptStageForm.ts
#: src/pages/stages/prompt/PromptStageForm.ts #: src/pages/stages/prompt/PromptStageForm.ts
#: src/pages/tenants/TenantForm.ts
#: src/pages/tenants/TenantForm.ts
#: src/pages/tenants/TenantForm.ts
#: src/pages/tenants/TenantForm.ts
msgid "Loading..." msgid "Loading..."
msgstr "Loading..." msgstr "Loading..."
@ -1949,6 +2026,10 @@ msgstr "Logins"
msgid "Logins over the last 24 hours" msgid "Logins over the last 24 hours"
msgstr "Logins over the last 24 hours" msgstr "Logins over the last 24 hours"
#: src/pages/tenants/TenantForm.ts
msgid "Logo"
msgstr "Logo"
#: src/pages/providers/oauth2/OAuth2ProviderViewPage.ts #: src/pages/providers/oauth2/OAuth2ProviderViewPage.ts
msgid "Logout URL" msgid "Logout URL"
msgstr "Logout URL" msgstr "Logout URL"
@ -1986,6 +2067,7 @@ msgstr "Maximum age (in days)"
msgid "Members" msgid "Members"
msgstr "Members" msgstr "Members"
#: src/pages/applications/ApplicationCheckAccessForm.ts
#: src/pages/events/EventInfo.ts #: src/pages/events/EventInfo.ts
#: src/pages/policies/PolicyTestForm.ts #: src/pages/policies/PolicyTestForm.ts
#: src/pages/system-tasks/SystemTaskListPage.ts #: src/pages/system-tasks/SystemTaskListPage.ts
@ -2130,6 +2212,7 @@ msgstr "Need an account?"
msgid "New version available!" msgid "New version available!"
msgstr "New version available!" msgstr "New version available!"
#: src/pages/applications/ApplicationCheckAccessForm.ts
#: src/pages/crypto/CertificateKeyPairListPage.ts #: src/pages/crypto/CertificateKeyPairListPage.ts
#: src/pages/groups/GroupListPage.ts #: src/pages/groups/GroupListPage.ts
#: src/pages/groups/MemberSelectModal.ts #: src/pages/groups/MemberSelectModal.ts
@ -2138,6 +2221,7 @@ msgstr "New version available!"
#: src/pages/policies/PolicyTestForm.ts #: src/pages/policies/PolicyTestForm.ts
#: src/pages/providers/proxy/ProxyProviderViewPage.ts #: src/pages/providers/proxy/ProxyProviderViewPage.ts
#: src/pages/providers/proxy/ProxyProviderViewPage.ts #: src/pages/providers/proxy/ProxyProviderViewPage.ts
#: src/pages/tenants/TenantListPage.ts
#: src/pages/tokens/TokenListPage.ts #: src/pages/tokens/TokenListPage.ts
#: src/pages/user-settings/tokens/UserTokenList.ts #: src/pages/user-settings/tokens/UserTokenList.ts
#: src/pages/users/UserListPage.ts #: src/pages/users/UserListPage.ts
@ -2165,10 +2249,6 @@ msgstr "No Stages bound"
msgid "No additional data available." msgid "No additional data available."
msgstr "No additional data available." msgstr "No additional data available."
#: src/elements/table/Table.ts
msgid "No elements found."
msgstr "No elements found."
#: src/elements/forms/ModalForm.ts #: src/elements/forms/ModalForm.ts
msgid "No form found" msgid "No form found"
msgstr "No form found" msgstr "No form found"
@ -2178,6 +2258,10 @@ msgstr "No form found"
msgid "No matching events could be found." msgid "No matching events could be found."
msgstr "No matching events could be found." msgstr "No matching events could be found."
#: src/elements/table/Table.ts
msgid "No objects found."
msgstr "No objects found."
#: src/pages/policies/BoundPoliciesList.ts #: src/pages/policies/BoundPoliciesList.ts
msgid "No policies are currently bound to this object." msgid "No policies are currently bound to this object."
msgstr "No policies are currently bound to this object." msgstr "No policies are currently bound to this object."
@ -2408,6 +2492,7 @@ msgstr "Parent"
msgid "Pass policy?" msgid "Pass policy?"
msgstr "Pass policy?" msgstr "Pass policy?"
#: src/pages/applications/ApplicationCheckAccessForm.ts
#: src/pages/events/EventInfo.ts #: src/pages/events/EventInfo.ts
#: src/pages/policies/PolicyTestForm.ts #: src/pages/policies/PolicyTestForm.ts
msgid "Passing" msgid "Passing"
@ -2673,9 +2758,14 @@ msgid "Recovery"
msgstr "Recovery" msgstr "Recovery"
#: src/pages/stages/identification/IdentificationStageForm.ts #: src/pages/stages/identification/IdentificationStageForm.ts
#: src/pages/tenants/TenantForm.ts
msgid "Recovery flow" msgid "Recovery flow"
msgstr "Recovery flow" msgstr "Recovery flow"
#: src/pages/tenants/TenantForm.ts
msgid "Recovery flow. If left empty, the first applicable flow sorted by the slug is used."
msgstr "Recovery flow. If left empty, the first applicable flow sorted by the slug is used."
#: src/flows/stages/authenticator_validate/AuthenticatorValidateStage.ts #: src/flows/stages/authenticator_validate/AuthenticatorValidateStage.ts
msgid "Recovery keys" msgid "Recovery keys"
msgstr "Recovery keys" msgstr "Recovery keys"
@ -2887,6 +2977,10 @@ msgstr "Select an identification method."
msgid "Select one of the sources below to login." msgid "Select one of the sources below to login."
msgstr "Select one of the sources below to login." msgstr "Select one of the sources below to login."
#: src/pages/stages/identification/IdentificationStageForm.ts
msgid "Select sources should be shown for users to authenticate with. This only affects web-based sources, not LDAP."
msgstr "Select sources should be shown for users to authenticate with. This only affects web-based sources, not LDAP."
#: src/pages/groups/MemberSelectModal.ts #: src/pages/groups/MemberSelectModal.ts
msgid "Select users to add" msgid "Select users to add"
msgstr "Select users to add" msgstr "Select users to add"
@ -3054,6 +3148,7 @@ msgstr "Source {0}"
#: src/interfaces/AdminInterface.ts #: src/interfaces/AdminInterface.ts
#: src/pages/sources/SourcesListPage.ts #: src/pages/sources/SourcesListPage.ts
#: src/pages/stages/identification/IdentificationStageForm.ts
msgid "Sources" msgid "Sources"
msgstr "Sources" msgstr "Sources"
@ -3288,6 +3383,10 @@ msgstr "Successfully created source."
msgid "Successfully created stage." msgid "Successfully created stage."
msgstr "Successfully created stage." msgstr "Successfully created stage."
#: src/pages/tenants/TenantForm.ts
msgid "Successfully created tenant."
msgstr "Successfully created tenant."
#: src/pages/user-settings/tokens/UserTokenForm.ts #: src/pages/user-settings/tokens/UserTokenForm.ts
msgid "Successfully created token." msgid "Successfully created token."
msgstr "Successfully created token." msgstr "Successfully created token."
@ -3321,6 +3420,7 @@ msgstr "Successfully imported flow."
msgid "Successfully imported provider." msgid "Successfully imported provider."
msgstr "Successfully imported provider." msgstr "Successfully imported provider."
#: src/pages/applications/ApplicationCheckAccessForm.ts
#: src/pages/policies/PolicyTestForm.ts #: src/pages/policies/PolicyTestForm.ts
#: src/pages/property-mappings/PropertyMappingTestForm.ts #: src/pages/property-mappings/PropertyMappingTestForm.ts
msgid "Successfully sent test-request." msgid "Successfully sent test-request."
@ -3427,6 +3527,10 @@ msgstr "Successfully updated source."
msgid "Successfully updated stage." msgid "Successfully updated stage."
msgstr "Successfully updated stage." msgstr "Successfully updated stage."
#: src/pages/tenants/TenantForm.ts
msgid "Successfully updated tenant."
msgstr "Successfully updated tenant."
#: src/pages/user-settings/tokens/UserTokenForm.ts #: src/pages/user-settings/tokens/UserTokenForm.ts
msgid "Successfully updated token." msgid "Successfully updated token."
msgstr "Successfully updated token." msgstr "Successfully updated token."
@ -3516,6 +3620,16 @@ msgstr "Task finished with warnings"
msgid "Template" msgid "Template"
msgstr "Template" msgstr "Template"
#: src/pages/tenants/TenantListPage.ts
msgid "Tenant"
msgstr "Tenant"
#: src/interfaces/AdminInterface.ts
#: src/pages/tenants/TenantListPage.ts
msgid "Tenants"
msgstr "Tenants"
#: src/pages/applications/ApplicationViewPage.ts
#: src/pages/events/TransportListPage.ts #: src/pages/events/TransportListPage.ts
#: src/pages/policies/PolicyListPage.ts #: src/pages/policies/PolicyListPage.ts
#: src/pages/policies/PolicyListPage.ts #: src/pages/policies/PolicyListPage.ts
@ -3609,6 +3723,7 @@ msgid "Timeout"
msgstr "Timeout" msgstr "Timeout"
#: src/pages/flows/FlowForm.ts #: src/pages/flows/FlowForm.ts
#: src/pages/tenants/TenantForm.ts
msgid "Title" msgid "Title"
msgstr "Title" msgstr "Title"
@ -3725,6 +3840,10 @@ msgstr "Unbound policies"
msgid "Unenrollment" msgid "Unenrollment"
msgstr "Unenrollment" msgstr "Unenrollment"
#: src/pages/tenants/TenantForm.ts
msgid "Unenrollment flow"
msgstr "Unenrollment flow"
#: src/pages/outposts/ServiceConnectionListPage.ts #: src/pages/outposts/ServiceConnectionListPage.ts
msgid "Unhealthy" msgid "Unhealthy"
msgstr "Unhealthy" msgstr "Unhealthy"
@ -3774,6 +3893,7 @@ msgstr "Up-to-date!"
#: src/pages/sources/saml/SAMLSourceViewPage.ts #: src/pages/sources/saml/SAMLSourceViewPage.ts
#: src/pages/stages/StageListPage.ts #: src/pages/stages/StageListPage.ts
#: src/pages/stages/prompt/PromptListPage.ts #: src/pages/stages/prompt/PromptListPage.ts
#: src/pages/tenants/TenantListPage.ts
#: src/pages/user-settings/UserDetailsPage.ts #: src/pages/user-settings/UserDetailsPage.ts
#: src/pages/user-settings/settings/UserSettingsAuthenticatorWebAuthn.ts #: src/pages/user-settings/settings/UserSettingsAuthenticatorWebAuthn.ts
#: src/pages/user-settings/settings/UserSettingsAuthenticatorWebAuthn.ts #: src/pages/user-settings/settings/UserSettingsAuthenticatorWebAuthn.ts
@ -3859,6 +3979,10 @@ msgstr "Update SAML Source"
msgid "Update Stage binding" msgid "Update Stage binding"
msgstr "Update Stage binding" msgstr "Update Stage binding"
#: src/pages/tenants/TenantListPage.ts
msgid "Update Tenant"
msgstr "Update Tenant"
#: src/pages/user-settings/tokens/UserTokenList.ts #: src/pages/user-settings/tokens/UserTokenList.ts
msgid "Update Token" msgid "Update Token"
msgstr "Update Token" msgstr "Update Token"
@ -3923,8 +4047,13 @@ msgstr "Use the user's username, but deny enrollment when the username already e
msgid "Use this redirect URL:" msgid "Use this redirect URL:"
msgstr "Use this redirect URL:" msgstr "Use this redirect URL:"
#: src/pages/tenants/TenantForm.ts
msgid "Use this tenant for each domain that doesn't have a dedicated tenant."
msgstr "Use this tenant for each domain that doesn't have a dedicated tenant."
#: src/elements/events/ObjectChangelog.ts #: src/elements/events/ObjectChangelog.ts
#: src/elements/events/UserEvents.ts #: src/elements/events/UserEvents.ts
#: src/pages/applications/ApplicationCheckAccessForm.ts
#: src/pages/events/EventInfo.ts #: src/pages/events/EventInfo.ts
#: src/pages/events/EventListPage.ts #: src/pages/events/EventListPage.ts
#: src/pages/policies/PolicyBindingForm.ts #: src/pages/policies/PolicyBindingForm.ts
@ -4178,6 +4307,7 @@ msgstr ""
msgid "X509 Subject" msgid "X509 Subject"
msgstr "X509 Subject" msgstr "X509 Subject"
#: src/pages/applications/ApplicationCheckAccessForm.ts
#: src/pages/crypto/CertificateKeyPairListPage.ts #: src/pages/crypto/CertificateKeyPairListPage.ts
#: src/pages/groups/GroupListPage.ts #: src/pages/groups/GroupListPage.ts
#: src/pages/groups/MemberSelectModal.ts #: src/pages/groups/MemberSelectModal.ts
@ -4186,6 +4316,7 @@ msgstr "X509 Subject"
#: src/pages/policies/PolicyTestForm.ts #: src/pages/policies/PolicyTestForm.ts
#: src/pages/providers/proxy/ProxyProviderViewPage.ts #: src/pages/providers/proxy/ProxyProviderViewPage.ts
#: src/pages/providers/proxy/ProxyProviderViewPage.ts #: src/pages/providers/proxy/ProxyProviderViewPage.ts
#: src/pages/tenants/TenantListPage.ts
#: src/pages/tokens/TokenListPage.ts #: src/pages/tokens/TokenListPage.ts
#: src/pages/user-settings/tokens/UserTokenList.ts #: src/pages/user-settings/tokens/UserTokenList.ts
#: src/pages/users/UserListPage.ts #: src/pages/users/UserListPage.ts

View file

@ -304,6 +304,7 @@ msgstr ""
#: #:
#: #:
#: #:
#:
msgid "Authentication flow" msgid "Authentication flow"
msgstr "" msgstr ""
@ -418,6 +419,14 @@ msgstr ""
msgid "Binding Type" msgid "Binding Type"
msgstr "" msgstr ""
#:
msgid "Branding settings"
msgstr ""
#:
msgid "Branding shown in page title and several other places."
msgstr ""
#: #:
msgid "Build hash: {0}" msgid "Build hash: {0}"
msgstr "" msgstr ""
@ -518,6 +527,14 @@ msgstr ""
msgid "Characters which are considered as symbols." msgid "Characters which are considered as symbols."
msgstr "" msgstr ""
#:
msgid "Check"
msgstr ""
#:
msgid "Check Application access"
msgstr ""
#: #:
msgid "Check IP" msgid "Check IP"
msgstr "" msgstr ""
@ -526,6 +543,10 @@ msgstr ""
msgid "Check Username" msgid "Check Username"
msgstr "" msgstr ""
#:
msgid "Check access"
msgstr ""
#: #:
msgid "Check status" msgid "Check status"
msgstr "" msgstr ""
@ -658,6 +679,10 @@ msgstr ""
msgid "Configure the maximum allowed time drift for an asseration." msgid "Configure the maximum allowed time drift for an asseration."
msgstr "" msgstr ""
#:
msgid "Configure visual settings and defaults for different domains."
msgstr ""
#: #:
msgid "Configure what data should be used as unique User Identifier. For most cases, the default should be fine." msgid "Configure what data should be used as unique User Identifier. For most cases, the default should be fine."
msgstr "" msgstr ""
@ -798,6 +823,8 @@ msgstr ""
#: #:
#: #:
#: #:
#:
#:
msgid "Create" msgid "Create"
msgstr "" msgstr ""
@ -863,6 +890,10 @@ msgstr ""
msgid "Create Stage binding" msgid "Create Stage binding"
msgstr "" msgstr ""
#:
msgid "Create Tenant"
msgstr ""
#: #:
msgid "Create Token" msgid "Create Token"
msgstr "" msgstr ""
@ -922,6 +953,18 @@ msgstr ""
msgid "Decides what this Flow is used for. For example, the Authentication flow is redirect to when an un-authenticated user visits authentik." msgid "Decides what this Flow is used for. For example, the Authentication flow is redirect to when an un-authenticated user visits authentik."
msgstr "" msgstr ""
#:
msgid "Default"
msgstr ""
#:
msgid "Default flows"
msgstr ""
#:
msgid "Default?"
msgstr ""
#: #:
msgid "Define how notifications are sent to users, like Email or Webhook." msgid "Define how notifications are sent to users, like Email or Webhook."
msgstr "" msgstr ""
@ -946,6 +989,7 @@ msgstr ""
#: #:
#: #:
#: #:
#:
msgid "Delete" msgid "Delete"
msgstr "" msgstr ""
@ -1063,6 +1107,11 @@ msgstr ""
msgid "Docker URL" msgid "Docker URL"
msgstr "" msgstr ""
#:
#:
msgid "Domain"
msgstr ""
#: #:
#: #:
#: #:
@ -1128,6 +1177,7 @@ msgstr ""
#: #:
#: #:
#: #:
#:
msgid "Edit" msgid "Edit"
msgstr "" msgstr ""
@ -1469,6 +1519,14 @@ msgstr ""
msgid "Flow used for users to authenticate. Currently only identification and password stages are supported." msgid "Flow used for users to authenticate. Currently only identification and password stages are supported."
msgstr "" msgstr ""
#:
msgid "Flow used to authenticate users. If left empty, the first applicable flow sorted by the slug is used."
msgstr ""
#:
msgid "Flow used to logout. If left empty, the first applicable flow sorted by the slug is used."
msgstr ""
#: #:
#: #:
#: #:
@ -1620,6 +1678,7 @@ msgstr ""
#: #:
#: #:
#: #:
#:
msgid "Hold control/command to select multiple items." msgid "Hold control/command to select multiple items."
msgstr "" msgstr ""
@ -1635,6 +1694,10 @@ msgstr ""
msgid "Icon" msgid "Icon"
msgstr "" msgstr ""
#:
msgid "Icon shown in sidebar/header and flow executor."
msgstr ""
#: #:
#: #:
#: #:
@ -1656,6 +1719,10 @@ msgstr ""
msgid "If left empty, authentik will try to extract the launch URL based on the selected provider." msgid "If left empty, authentik will try to extract the launch URL based on the selected provider."
msgstr "" msgstr ""
#:
msgid "If set, users are able to unenroll themselves using this flow. If no flow is set, option is not shown."
msgstr ""
#: #:
msgid "If this flag is set, this Stage will jump to the next Stage when no Invitation is given. By default this Stage will cancel the Flow when no invitation is given." msgid "If this flag is set, this Stage will jump to the next Stage when no Invitation is given. By default this Stage will cancel the Flow when no invitation is given."
msgstr "" msgstr ""
@ -1717,6 +1784,10 @@ msgstr ""
msgid "Invalidation" msgid "Invalidation"
msgstr "" msgstr ""
#:
msgid "Invalidation flow"
msgstr ""
#: #:
#: #:
msgid "Invitations" msgid "Invitations"
@ -1911,6 +1982,12 @@ msgstr ""
#: #:
#: #:
#: #:
#:
#:
#:
#:
#:
#:
msgid "Loading..." msgid "Loading..."
msgstr "" msgstr ""
@ -1941,6 +2018,10 @@ msgstr ""
msgid "Logins over the last 24 hours" msgid "Logins over the last 24 hours"
msgstr "" msgstr ""
#:
msgid "Logo"
msgstr ""
#: #:
msgid "Logout URL" msgid "Logout URL"
msgstr "" msgstr ""
@ -1981,6 +2062,7 @@ msgstr ""
#: #:
#: #:
#: #:
#:
msgid "Messages" msgid "Messages"
msgstr "" msgstr ""
@ -2133,6 +2215,8 @@ msgstr ""
#: #:
#: #:
#: #:
#:
#:
msgid "No" msgid "No"
msgstr "" msgstr ""
@ -2157,10 +2241,6 @@ msgstr ""
msgid "No additional data available." msgid "No additional data available."
msgstr "" msgstr ""
#:
msgid "No elements found."
msgstr ""
#: #:
msgid "No form found" msgid "No form found"
msgstr "" msgstr ""
@ -2170,6 +2250,10 @@ msgstr ""
msgid "No matching events could be found." msgid "No matching events could be found."
msgstr "" msgstr ""
#:
msgid "No objects found."
msgstr ""
#: #:
msgid "No policies are currently bound to this object." msgid "No policies are currently bound to this object."
msgstr "" msgstr ""
@ -2400,6 +2484,7 @@ msgstr ""
msgid "Pass policy?" msgid "Pass policy?"
msgstr "" msgstr ""
#:
#: #:
#: #:
msgid "Passing" msgid "Passing"
@ -2664,10 +2749,15 @@ msgstr ""
msgid "Recovery" msgid "Recovery"
msgstr "" msgstr ""
#:
#: #:
msgid "Recovery flow" msgid "Recovery flow"
msgstr "" msgstr ""
#:
msgid "Recovery flow. If left empty, the first applicable flow sorted by the slug is used."
msgstr ""
#: #:
msgid "Recovery keys" msgid "Recovery keys"
msgstr "" msgstr ""
@ -2879,6 +2969,10 @@ msgstr ""
msgid "Select one of the sources below to login." msgid "Select one of the sources below to login."
msgstr "" msgstr ""
#:
msgid "Select sources should be shown for users to authenticate with. This only affects web-based sources, not LDAP."
msgstr ""
#: #:
msgid "Select users to add" msgid "Select users to add"
msgstr "" msgstr ""
@ -3044,6 +3138,7 @@ msgstr ""
msgid "Source {0}" msgid "Source {0}"
msgstr "" msgstr ""
#:
#: #:
#: #:
msgid "Sources" msgid "Sources"
@ -3280,6 +3375,10 @@ msgstr ""
msgid "Successfully created stage." msgid "Successfully created stage."
msgstr "" msgstr ""
#:
msgid "Successfully created tenant."
msgstr ""
#: #:
msgid "Successfully created token." msgid "Successfully created token."
msgstr "" msgstr ""
@ -3313,6 +3412,7 @@ msgstr ""
msgid "Successfully imported provider." msgid "Successfully imported provider."
msgstr "" msgstr ""
#:
#: #:
#: #:
msgid "Successfully sent test-request." msgid "Successfully sent test-request."
@ -3419,6 +3519,10 @@ msgstr ""
msgid "Successfully updated stage." msgid "Successfully updated stage."
msgstr "" msgstr ""
#:
msgid "Successfully updated tenant."
msgstr ""
#: #:
msgid "Successfully updated token." msgid "Successfully updated token."
msgstr "" msgstr ""
@ -3508,6 +3612,16 @@ msgstr ""
msgid "Template" msgid "Template"
msgstr "" msgstr ""
#:
msgid "Tenant"
msgstr ""
#:
#:
msgid "Tenants"
msgstr ""
#:
#: #:
#: #:
#: #:
@ -3596,6 +3710,7 @@ msgstr ""
msgid "Timeout" msgid "Timeout"
msgstr "" msgstr ""
#:
#: #:
msgid "Title" msgid "Title"
msgstr "" msgstr ""
@ -3713,6 +3828,10 @@ msgstr ""
msgid "Unenrollment" msgid "Unenrollment"
msgstr "" msgstr ""
#:
msgid "Unenrollment flow"
msgstr ""
#: #:
msgid "Unhealthy" msgid "Unhealthy"
msgstr "" msgstr ""
@ -3770,6 +3889,7 @@ msgstr ""
#: #:
#: #:
#: #:
#:
msgid "Update" msgid "Update"
msgstr "" msgstr ""
@ -3847,6 +3967,10 @@ msgstr ""
msgid "Update Stage binding" msgid "Update Stage binding"
msgstr "" msgstr ""
#:
msgid "Update Tenant"
msgstr ""
#: #:
msgid "Update Token" msgid "Update Token"
msgstr "" msgstr ""
@ -3911,6 +4035,11 @@ msgstr ""
msgid "Use this redirect URL:" msgid "Use this redirect URL:"
msgstr "" msgstr ""
#:
msgid "Use this tenant for each domain that doesn't have a dedicated tenant."
msgstr ""
#:
#: #:
#: #:
#: #:
@ -4175,6 +4304,8 @@ msgstr ""
#: #:
#: #:
#: #:
#:
#:
msgid "Yes" msgid "Yes"
msgstr "" msgstr ""

View file

@ -0,0 +1,158 @@
import { CoreApi, FlowsApi, FlowsInstancesListDesignationEnum, Tenant } from "authentik-api";
import { t } from "@lingui/macro";
import { customElement } from "lit-element";
import { html, TemplateResult } from "lit-html";
import { DEFAULT_CONFIG } from "../../api/Config";
import "../../elements/forms/HorizontalFormElement";
import { first } from "../../utils";
import { ModelForm } from "../../elements/forms/ModelForm";
import { until } from "lit-html/directives/until";
@customElement("ak-tenant-form")
export class TenantForm extends ModelForm<Tenant, string> {
loadInstance(pk: string): Promise<Tenant> {
return new CoreApi(DEFAULT_CONFIG).coreTenantsRetrieve({
tenantUuid: pk
});
}
getSuccessMessage(): string {
if (this.instance) {
return t`Successfully updated tenant.`;
} else {
return t`Successfully created tenant.`;
}
}
send = (data: Tenant): Promise<Tenant> => {
if (this.instance?.tenantUuid) {
return new CoreApi(DEFAULT_CONFIG).coreTenantsUpdate({
tenantUuid: this.instance.tenantUuid,
tenantRequest: data
});
} else {
return new CoreApi(DEFAULT_CONFIG).coreTenantsCreate({
tenantRequest: data
});
}
};
renderForm(): TemplateResult {
return html`<form class="pf-c-form pf-m-horizontal">
<ak-form-element-horizontal
label=${t`Domain`}
?required=${true}
name="domain">
<input type="text" value="${first(this.instance?.domain, window.location.host)}" class="pf-c-form-control" required>
</ak-form-element-horizontal>
<ak-form-element-horizontal name="default">
<div class="pf-c-check">
<input type="checkbox" class="pf-c-check__input" ?checked=${first(this.instance?._default, false)}>
<label class="pf-c-check__label">
${t`Default`}
</label>
</div>
<p class="pf-c-form__helper-text">${t`Use this tenant for each domain that doesn't have a dedicated tenant.`}</p>
</ak-form-element-horizontal>
<ak-form-group .expanded=${true}>
<span slot="header">
${t`Branding settings`}
</span>
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal
label=${t`Title`}
?required=${true}
name="brandingTitle">
<input type="text" value="${first(this.instance?.brandingTitle, "authentik")}" class="pf-c-form-control" required>
<p class="pf-c-form__helper-text">${t`Branding shown in page title and several other places.`}</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${t`Logo`}
?required=${true}
name="brandingLogo">
<input type="text" value="${first(this.instance?.brandingLogo, "/static/dist/assets/icons/icon_left_brand.svg")}" class="pf-c-form-control" required>
<p class="pf-c-form__helper-text">${t`Icon shown in sidebar/header and flow executor.`}</p>
</ak-form-element-horizontal>
</div>
</ak-form-group>
<ak-form-group>
<span slot="header">
${t`Default flows`}
</span>
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal
label=${t`Authentication flow`}
name="flowAuthentication">
<select class="pf-c-form-control">
<option value="" ?selected=${this.instance?.flowAuthentication === undefined}>---------</option>
${until(new FlowsApi(DEFAULT_CONFIG).flowsInstancesList({
ordering: "pk",
designation: FlowsInstancesListDesignationEnum.Authentication,
}).then(flows => {
return flows.results.map(flow => {
const selected = this.instance?.flowAuthentication === flow.pk;
return html`<option value=${flow.pk} ?selected=${selected}>${flow.name} (${flow.slug})</option>`;
});
}), html`<option>${t`Loading...`}</option>`)}
</select>
<p class="pf-c-form__helper-text">${t`Flow used to authenticate users. If left empty, the first applicable flow sorted by the slug is used.`}</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${t`Invalidation flow`}
name="flowInvalidation">
<select class="pf-c-form-control">
<option value="" ?selected=${this.instance?.flowInvalidation === undefined}>---------</option>
${until(new FlowsApi(DEFAULT_CONFIG).flowsInstancesList({
ordering: "pk",
designation: FlowsInstancesListDesignationEnum.Invalidation,
}).then(flows => {
return flows.results.map(flow => {
const selected = this.instance?.flowInvalidation === flow.pk;
return html`<option value=${flow.pk} ?selected=${selected}>${flow.name} (${flow.slug})</option>`;
});
}), html`<option>${t`Loading...`}</option>`)}
</select>
<p class="pf-c-form__helper-text">${t`Flow used to logout. If left empty, the first applicable flow sorted by the slug is used.`}</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${t`Recovery flow`}
name="flowRecovery">
<select class="pf-c-form-control">
<option value="" ?selected=${this.instance?.flowRecovery === undefined}>---------</option>
${until(new FlowsApi(DEFAULT_CONFIG).flowsInstancesList({
ordering: "pk",
designation: FlowsInstancesListDesignationEnum.Recovery,
}).then(flows => {
return flows.results.map(flow => {
const selected = this.instance?.flowRecovery === flow.pk;
return html`<option value=${flow.pk} ?selected=${selected}>${flow.name} (${flow.slug})</option>`;
});
}), html`<option>${t`Loading...`}</option>`)}
</select>
<p class="pf-c-form__helper-text">${t`Recovery flow. If left empty, the first applicable flow sorted by the slug is used.`}</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${t`Unenrollment flow`}
name="flowUnenrollment">
<select class="pf-c-form-control">
<option value="" ?selected=${this.instance?.flowUnenrollment === undefined}>---------</option>
${until(new FlowsApi(DEFAULT_CONFIG).flowsInstancesList({
ordering: "pk",
designation: FlowsInstancesListDesignationEnum.Recovery,
}).then(flows => {
return flows.results.map(flow => {
const selected = this.instance?.flowUnenrollment === flow.pk;
return html`<option value=${flow.pk} ?selected=${selected}>${flow.name} (${flow.slug})</option>`;
});
}), html`<option>${t`Loading...`}</option>`)}
</select>
<p class="pf-c-form__helper-text">${t`If set, users are able to unenroll themselves using this flow. If no flow is set, option is not shown.`}</p>
</ak-form-element-horizontal>
</div>
</ak-form-group>
</form>`;
}
}

View file

@ -0,0 +1,101 @@
import { t } from "@lingui/macro";
import { customElement, html, property, TemplateResult } from "lit-element";
import { AKResponse } from "../../api/Client";
import { TablePage } from "../../elements/table/TablePage";
import "../../elements/forms/DeleteForm";
import "../../elements/buttons/SpinnerButton";
import { TableColumn } from "../../elements/table/Table";
import { PAGE_SIZE } from "../../constants";
import { CoreApi, Tenant } from "authentik-api";
import { DEFAULT_CONFIG } from "../../api/Config";
import "../../elements/forms/ModalForm";
import "./TenantForm";
@customElement("ak-tenant-list")
export class TenantListPage extends TablePage<Tenant> {
searchEnabled(): boolean {
return true;
}
pageTitle(): string {
return t`Tenants`;
}
pageDescription(): string {
return t`Configure visual settings and defaults for different domains.`;
}
pageIcon(): string {
return "pf-icon pf-icon-tenant";
}
@property()
order = "domain";
apiEndpoint(page: number): Promise<AKResponse<Tenant>> {
return new CoreApi(DEFAULT_CONFIG).coreTenantsList({
ordering: this.order,
page: page,
pageSize: PAGE_SIZE,
search: this.search || "",
});
}
columns(): TableColumn[] {
return [
new TableColumn(t`Domain`, "domain"),
new TableColumn(t`Default?`, "default"),
new TableColumn(""),
];
}
row(item: Tenant): TemplateResult[] {
return [
html`${item.domain}`,
html`${item._default ? t`Yes` : t`No`}`,
html`
<ak-forms-modal>
<span slot="submit">
${t`Update`}
</span>
<span slot="header">
${t`Update Tenant`}
</span>
<ak-tenant-form slot="form" .instancePk=${item.tenantUuid}>
</ak-tenant-form>
<button slot="trigger" class="pf-c-button pf-m-secondary">
${t`Edit`}
</button>
</ak-forms-modal>
<ak-forms-delete
.obj=${item}
objectLabel=${t`Tenant`}
.delete=${() => {
return new CoreApi(DEFAULT_CONFIG).coreTenantsDestroy({
tenantUuid: item.tenantUuid
});
}}>
<button slot="trigger" class="pf-c-button pf-m-danger">
${t`Delete`}
</button>
</ak-forms-delete>`,
];
}
renderToolbar(): TemplateResult {
return html`
<ak-forms-modal>
<span slot="submit">
${t`Create`}
</span>
<span slot="header">
${t`Create Tenant`}
</span>
<ak-tenant-form slot="form">
</ak-tenant-form>
<button slot="trigger" class="pf-c-button pf-m-primary">
${t`Create`}
</button>
</ak-forms-modal>
${super.renderToolbar()}
`;
}
}

View file

@ -8,13 +8,13 @@ import PFForm from "@patternfly/patternfly/components/Form/form.css";
import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css"; import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css";
import { CoreApi, User } from "authentik-api"; import { CoreApi, User } from "authentik-api";
import { me } from "../../api/Users"; import { me } from "../../api/Users";
import { FlowURLManager } from "../../api/legacy";
import { ifDefined } from "lit-html/directives/if-defined"; import { ifDefined } from "lit-html/directives/if-defined";
import { DEFAULT_CONFIG } from "../../api/Config"; import { DEFAULT_CONFIG, tenant } from "../../api/Config";
import "../../elements/forms/FormElement"; import "../../elements/forms/FormElement";
import "../../elements/EmptyState"; import "../../elements/EmptyState";
import "../../elements/forms/Form"; import "../../elements/forms/Form";
import "../../elements/forms/HorizontalFormElement"; import "../../elements/forms/HorizontalFormElement";
import { until } from "lit-html/directives/until";
@customElement("ak-user-details") @customElement("ak-user-details")
export class UserDetailsPage extends LitElement { export class UserDetailsPage extends LitElement {
@ -80,10 +80,15 @@ export class UserDetailsPage extends LitElement {
<button class="pf-c-button pf-m-primary"> <button class="pf-c-button pf-m-primary">
${t`Update`} ${t`Update`}
</button> </button>
<a class="pf-c-button pf-m-danger" ${until(tenant().then(tenant => {
href="${FlowURLManager.defaultUnenrollment()}"> if (tenant.flowUnenrollment) {
${t`Delete account`} return html`<a class="pf-c-button pf-m-danger"
</a> href="/if/flow/${tenant.flowUnenrollment}">
${t`Delete account`}
</a>`;
}
return html``;
}))}
</div> </div>
</div> </div>
</div> </div>

View file

@ -25,10 +25,11 @@ import "./pages/stages/invitation/InvitationListPage";
import "./pages/stages/prompt/PromptListPage"; import "./pages/stages/prompt/PromptListPage";
import "./pages/stages/StageListPage"; import "./pages/stages/StageListPage";
import "./pages/system-tasks/SystemTaskListPage"; import "./pages/system-tasks/SystemTaskListPage";
import "./pages/tenants/TenantListPage";
import "./pages/tokens/TokenListPage"; import "./pages/tokens/TokenListPage";
import "./pages/user-settings/UserSettingsPage";
import "./pages/users/UserListPage"; import "./pages/users/UserListPage";
import "./pages/users/UserViewPage"; import "./pages/users/UserViewPage";
import "./pages/user-settings/UserSettingsPage";
export const ROUTES: Route[] = [ export const ROUTES: Route[] = [
// Prevent infinite Shell loops // Prevent infinite Shell loops
@ -51,6 +52,7 @@ export const ROUTES: Route[] = [
}), }),
new Route(new RegExp("^/core/property-mappings$"), html`<ak-property-mapping-list></ak-property-mapping-list>`), new Route(new RegExp("^/core/property-mappings$"), html`<ak-property-mapping-list></ak-property-mapping-list>`),
new Route(new RegExp("^/core/tokens$"), html`<ak-token-list></ak-token-list>`), new Route(new RegExp("^/core/tokens$"), html`<ak-token-list></ak-token-list>`),
new Route(new RegExp("^/core/tenants$"), html`<ak-tenant-list></ak-tenant-list>`),
new Route(new RegExp("^/policy/policies$"), html`<ak-policy-list></ak-policy-list>`), new Route(new RegExp("^/policy/policies$"), html`<ak-policy-list></ak-policy-list>`),
new Route(new RegExp("^/identity/groups$"), html`<ak-group-list></ak-group-list>`), new Route(new RegExp("^/identity/groups$"), html`<ak-group-list></ak-group-list>`),
new Route(new RegExp("^/identity/users$"), html`<ak-user-list></ak-user-list>`), new Route(new RegExp("^/identity/users$"), html`<ak-user-list></ak-user-list>`),