tenants -> brands, init new tenant model, migrate some config to tenants
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
This commit is contained in:
parent
2814a8e951
commit
77d8877efe
|
@ -37,7 +37,7 @@ class SystemInfoSerializer(PassiveSerializer):
|
||||||
http_host = SerializerMethodField()
|
http_host = SerializerMethodField()
|
||||||
http_is_secure = SerializerMethodField()
|
http_is_secure = SerializerMethodField()
|
||||||
runtime = SerializerMethodField()
|
runtime = SerializerMethodField()
|
||||||
tenant = SerializerMethodField()
|
brand = SerializerMethodField()
|
||||||
server_time = SerializerMethodField()
|
server_time = SerializerMethodField()
|
||||||
embedded_outpost_host = SerializerMethodField()
|
embedded_outpost_host = SerializerMethodField()
|
||||||
|
|
||||||
|
@ -69,9 +69,9 @@ class SystemInfoSerializer(PassiveSerializer):
|
||||||
"uname": " ".join(platform.uname()),
|
"uname": " ".join(platform.uname()),
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_tenant(self, request: Request) -> str:
|
def get_brand(self, request: Request) -> str:
|
||||||
"""Currently active tenant"""
|
"""Currently active brand"""
|
||||||
return str(request._request.tenant)
|
return str(request._request.brand)
|
||||||
|
|
||||||
def get_server_time(self, request: Request) -> datetime:
|
def get_server_time(self, request: Request) -> datetime:
|
||||||
"""Current server time"""
|
"""Current server time"""
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
{% load static %}
|
{% load static %}
|
||||||
|
|
||||||
{% block title %}
|
{% block title %}
|
||||||
API Browser - {{ tenant.branding_title }}
|
API Browser - {{ brand.branding_title }}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block head %}
|
{% block head %}
|
||||||
|
|
|
@ -62,7 +62,7 @@ class ConfigView(APIView):
|
||||||
|
|
||||||
permission_classes = [AllowAny]
|
permission_classes = [AllowAny]
|
||||||
|
|
||||||
def get_capabilities(self) -> list[Capabilities]:
|
def get_capabilities(self, request: Request) -> list[Capabilities]:
|
||||||
"""Get all capabilities this server instance supports"""
|
"""Get all capabilities this server instance supports"""
|
||||||
caps = []
|
caps = []
|
||||||
deb_test = settings.DEBUG or settings.TEST
|
deb_test = settings.DEBUG or settings.TEST
|
||||||
|
@ -70,7 +70,7 @@ class ConfigView(APIView):
|
||||||
caps.append(Capabilities.CAN_SAVE_MEDIA)
|
caps.append(Capabilities.CAN_SAVE_MEDIA)
|
||||||
if GEOIP_READER.enabled:
|
if GEOIP_READER.enabled:
|
||||||
caps.append(Capabilities.CAN_GEO_IP)
|
caps.append(Capabilities.CAN_GEO_IP)
|
||||||
if CONFIG.get_bool("impersonation"):
|
if request.tenant.impersonation:
|
||||||
caps.append(Capabilities.CAN_IMPERSONATE)
|
caps.append(Capabilities.CAN_IMPERSONATE)
|
||||||
if settings.DEBUG: # pragma: no cover
|
if settings.DEBUG: # pragma: no cover
|
||||||
caps.append(Capabilities.CAN_DEBUG)
|
caps.append(Capabilities.CAN_DEBUG)
|
||||||
|
@ -81,7 +81,7 @@ class ConfigView(APIView):
|
||||||
caps.append(result)
|
caps.append(result)
|
||||||
return caps
|
return caps
|
||||||
|
|
||||||
def get_config(self) -> ConfigSerializer:
|
def get_config(self, request: Request) -> ConfigSerializer:
|
||||||
"""Get Config"""
|
"""Get Config"""
|
||||||
return ConfigSerializer(
|
return ConfigSerializer(
|
||||||
{
|
{
|
||||||
|
@ -92,7 +92,7 @@ class ConfigView(APIView):
|
||||||
"send_pii": CONFIG.get("error_reporting.send_pii"),
|
"send_pii": CONFIG.get("error_reporting.send_pii"),
|
||||||
"traces_sample_rate": float(CONFIG.get("error_reporting.sample_rate", 0.4)),
|
"traces_sample_rate": float(CONFIG.get("error_reporting.sample_rate", 0.4)),
|
||||||
},
|
},
|
||||||
"capabilities": self.get_capabilities(),
|
"capabilities": self.get_capabilities(request),
|
||||||
"cache_timeout": CONFIG.get_int("cache.timeout"),
|
"cache_timeout": CONFIG.get_int("cache.timeout"),
|
||||||
"cache_timeout_flows": CONFIG.get_int("cache.timeout_flows"),
|
"cache_timeout_flows": CONFIG.get_int("cache.timeout_flows"),
|
||||||
"cache_timeout_policies": CONFIG.get_int("cache.timeout_policies"),
|
"cache_timeout_policies": CONFIG.get_int("cache.timeout_policies"),
|
||||||
|
@ -103,4 +103,4 @@ class ConfigView(APIView):
|
||||||
@extend_schema(responses={200: ConfigSerializer(many=False)})
|
@extend_schema(responses={200: ConfigSerializer(many=False)})
|
||||||
def get(self, request: Request) -> Response:
|
def get(self, request: Request) -> Response:
|
||||||
"""Retrieve public configuration options"""
|
"""Retrieve public configuration options"""
|
||||||
return Response(self.get_config().data)
|
return Response(self.get_config(request).data)
|
||||||
|
|
|
@ -7,16 +7,16 @@ from django.test import TransactionTestCase
|
||||||
from authentik.blueprints.models import BlueprintInstance
|
from authentik.blueprints.models import BlueprintInstance
|
||||||
from authentik.blueprints.tests import apply_blueprint
|
from authentik.blueprints.tests import apply_blueprint
|
||||||
from authentik.blueprints.v1.importer import Importer
|
from authentik.blueprints.v1.importer import Importer
|
||||||
from authentik.tenants.models import Tenant
|
from authentik.brands.models import Brand
|
||||||
|
|
||||||
|
|
||||||
class TestPackaged(TransactionTestCase):
|
class TestPackaged(TransactionTestCase):
|
||||||
"""Empty class, test methods are added dynamically"""
|
"""Empty class, test methods are added dynamically"""
|
||||||
|
|
||||||
@apply_blueprint("default/default-tenant.yaml")
|
@apply_blueprint("default/default-brand.yaml")
|
||||||
def test_decorator_static(self):
|
def test_decorator_static(self):
|
||||||
"""Test @apply_blueprint decorator"""
|
"""Test @apply_blueprint decorator"""
|
||||||
self.assertTrue(Tenant.objects.filter(domain="authentik-default").exists())
|
self.assertTrue(Brand.objects.filter(domain="authentik-default").exists())
|
||||||
|
|
||||||
|
|
||||||
def blueprint_tester(file_name: Path) -> Callable:
|
def blueprint_tester(file_name: Path) -> Callable:
|
||||||
|
|
|
@ -0,0 +1,142 @@
|
||||||
|
"""Serializer for brands models"""
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from django.db import models
|
||||||
|
from drf_spectacular.utils import extend_schema
|
||||||
|
from rest_framework.decorators import action
|
||||||
|
from rest_framework.exceptions import ValidationError
|
||||||
|
from rest_framework.fields import CharField, ChoiceField, ListField
|
||||||
|
from rest_framework.filters import OrderingFilter, SearchFilter
|
||||||
|
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.api.authorization import SecretKeyFilter
|
||||||
|
from authentik.brands.models import Brand
|
||||||
|
from authentik.core.api.used_by import UsedByMixin
|
||||||
|
from authentik.core.api.utils import PassiveSerializer
|
||||||
|
from authentik.lib.config import CONFIG
|
||||||
|
from authentik.tenants.utils import get_current_tenant
|
||||||
|
|
||||||
|
|
||||||
|
class FooterLinkSerializer(PassiveSerializer):
|
||||||
|
"""Links returned in Config API"""
|
||||||
|
|
||||||
|
href = CharField(read_only=True)
|
||||||
|
name = CharField(read_only=True)
|
||||||
|
|
||||||
|
|
||||||
|
class BrandSerializer(ModelSerializer):
|
||||||
|
"""Brand Serializer"""
|
||||||
|
|
||||||
|
def validate(self, attrs: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
if attrs.get("default", False):
|
||||||
|
brands = Brand.objects.filter(default=True)
|
||||||
|
if self.instance:
|
||||||
|
brands = brands.exclude(pk=self.instance.pk)
|
||||||
|
if brands.exists():
|
||||||
|
raise ValidationError({"default": "Only a single brand can be set as default."})
|
||||||
|
return super().validate(attrs)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Brand
|
||||||
|
fields = [
|
||||||
|
"brand_uuid",
|
||||||
|
"domain",
|
||||||
|
"default",
|
||||||
|
"branding_title",
|
||||||
|
"branding_logo",
|
||||||
|
"branding_favicon",
|
||||||
|
"flow_authentication",
|
||||||
|
"flow_invalidation",
|
||||||
|
"flow_recovery",
|
||||||
|
"flow_unenrollment",
|
||||||
|
"flow_user_settings",
|
||||||
|
"flow_device_code",
|
||||||
|
"event_retention",
|
||||||
|
"web_certificate",
|
||||||
|
"attributes",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class Themes(models.TextChoices):
|
||||||
|
"""Themes"""
|
||||||
|
|
||||||
|
AUTOMATIC = "automatic"
|
||||||
|
LIGHT = "light"
|
||||||
|
DARK = "dark"
|
||||||
|
|
||||||
|
|
||||||
|
class CurrentBrandSerializer(PassiveSerializer):
|
||||||
|
"""Partial brand information for styling"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_default_ui_footer_links():
|
||||||
|
return get_current_tenant().footer_links
|
||||||
|
|
||||||
|
matched_domain = CharField(source="domain")
|
||||||
|
branding_title = CharField()
|
||||||
|
branding_logo = CharField()
|
||||||
|
branding_favicon = CharField()
|
||||||
|
ui_footer_links = ListField(
|
||||||
|
child=FooterLinkSerializer(),
|
||||||
|
read_only=True,
|
||||||
|
default=get_default_ui_footer_links,
|
||||||
|
)
|
||||||
|
ui_theme = ChoiceField(
|
||||||
|
choices=Themes.choices,
|
||||||
|
source="attributes.settings.theme.base",
|
||||||
|
default=Themes.AUTOMATIC,
|
||||||
|
read_only=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
flow_authentication = CharField(source="flow_authentication.slug", required=False)
|
||||||
|
flow_invalidation = CharField(source="flow_invalidation.slug", required=False)
|
||||||
|
flow_recovery = CharField(source="flow_recovery.slug", required=False)
|
||||||
|
flow_unenrollment = CharField(source="flow_unenrollment.slug", required=False)
|
||||||
|
flow_user_settings = CharField(source="flow_user_settings.slug", required=False)
|
||||||
|
flow_device_code = CharField(source="flow_device_code.slug", required=False)
|
||||||
|
|
||||||
|
default_locale = CharField(read_only=True)
|
||||||
|
|
||||||
|
|
||||||
|
class BrandViewSet(UsedByMixin, ModelViewSet):
|
||||||
|
"""Brand Viewset"""
|
||||||
|
|
||||||
|
queryset = Brand.objects.all()
|
||||||
|
serializer_class = BrandSerializer
|
||||||
|
search_fields = [
|
||||||
|
"domain",
|
||||||
|
"branding_title",
|
||||||
|
"web_certificate__name",
|
||||||
|
]
|
||||||
|
filterset_fields = [
|
||||||
|
"brand_uuid",
|
||||||
|
"domain",
|
||||||
|
"default",
|
||||||
|
"branding_title",
|
||||||
|
"branding_logo",
|
||||||
|
"branding_favicon",
|
||||||
|
"flow_authentication",
|
||||||
|
"flow_invalidation",
|
||||||
|
"flow_recovery",
|
||||||
|
"flow_unenrollment",
|
||||||
|
"flow_user_settings",
|
||||||
|
"flow_device_code",
|
||||||
|
"event_retention",
|
||||||
|
"web_certificate",
|
||||||
|
]
|
||||||
|
ordering = ["domain"]
|
||||||
|
|
||||||
|
filter_backends = [SecretKeyFilter, OrderingFilter, SearchFilter]
|
||||||
|
|
||||||
|
@extend_schema(
|
||||||
|
responses=CurrentBrandSerializer(many=False),
|
||||||
|
)
|
||||||
|
@action(methods=["GET"], detail=False, permission_classes=[AllowAny])
|
||||||
|
def current(self, request: Request) -> Response:
|
||||||
|
"""Get current brand"""
|
||||||
|
brand: Brand = request._request.brand
|
||||||
|
return Response(CurrentBrandSerializer(brand).data)
|
|
@ -0,0 +1,10 @@
|
||||||
|
"""authentik brands app"""
|
||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class AuthentikBrandsConfig(AppConfig):
|
||||||
|
"""authentik Brand app"""
|
||||||
|
|
||||||
|
name = "authentik.brands"
|
||||||
|
label = "authentik_brands"
|
||||||
|
verbose_name = "authentik Brands"
|
|
@ -0,0 +1,29 @@
|
||||||
|
"""Inject brand into current request"""
|
||||||
|
from typing import Callable
|
||||||
|
|
||||||
|
from django.http.request import HttpRequest
|
||||||
|
from django.http.response import HttpResponse
|
||||||
|
from django.utils.translation import activate
|
||||||
|
from sentry_sdk.api import set_tag
|
||||||
|
|
||||||
|
from authentik.brands.utils import get_brand_for_request
|
||||||
|
|
||||||
|
|
||||||
|
class BrandMiddleware:
|
||||||
|
"""Add current brand 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, "brand"):
|
||||||
|
brand = get_brand_for_request(request)
|
||||||
|
setattr(request, "brand", brand)
|
||||||
|
set_tag("authentik.brand_uuid", brand.brand_uuid.hex)
|
||||||
|
set_tag("authentik.brand_domain", brand.domain)
|
||||||
|
locale = brand.default_locale
|
||||||
|
if locale != "":
|
||||||
|
activate(locale)
|
||||||
|
return self.get_response(request)
|
|
@ -10,11 +10,11 @@ import authentik.lib.utils.time
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
replaces = [
|
replaces = [
|
||||||
("authentik_tenants", "0001_initial"),
|
("authentik_brands", "0001_initial"),
|
||||||
("authentik_tenants", "0002_default"),
|
("authentik_brands", "0002_default"),
|
||||||
("authentik_tenants", "0003_tenant_branding_favicon"),
|
("authentik_brands", "0003_tenant_branding_favicon"),
|
||||||
("authentik_tenants", "0004_tenant_event_retention"),
|
("authentik_brands", "0004_tenant_event_retention"),
|
||||||
("authentik_tenants", "0005_tenant_web_certificate"),
|
("authentik_brands", "0005_tenant_web_certificate"),
|
||||||
]
|
]
|
||||||
|
|
||||||
initial = True
|
initial = True
|
||||||
|
@ -25,10 +25,10 @@ class Migration(migrations.Migration):
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name="Tenant",
|
name="Brand",
|
||||||
fields=[
|
fields=[
|
||||||
(
|
(
|
||||||
"tenant_uuid",
|
"brand_uuid",
|
||||||
models.UUIDField(
|
models.UUIDField(
|
||||||
default=uuid.uuid4, editable=False, primary_key=True, serialize=False
|
default=uuid.uuid4, editable=False, primary_key=True, serialize=False
|
||||||
),
|
),
|
||||||
|
@ -37,7 +37,7 @@ class Migration(migrations.Migration):
|
||||||
"domain",
|
"domain",
|
||||||
models.TextField(
|
models.TextField(
|
||||||
help_text=(
|
help_text=(
|
||||||
"Domain that activates this tenant. Can be a superset, i.e. `a.b` for"
|
"Domain that activates this brand. Can be a superset, i.e. `a.b` for"
|
||||||
" `aa.b` and `ba.b`"
|
" `aa.b` and `ba.b`"
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
|
@ -53,7 +53,7 @@ class Migration(migrations.Migration):
|
||||||
models.ForeignKey(
|
models.ForeignKey(
|
||||||
null=True,
|
null=True,
|
||||||
on_delete=django.db.models.deletion.SET_NULL,
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
related_name="tenant_authentication",
|
related_name="brand_authentication",
|
||||||
to="authentik_flows.flow",
|
to="authentik_flows.flow",
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -62,7 +62,7 @@ class Migration(migrations.Migration):
|
||||||
models.ForeignKey(
|
models.ForeignKey(
|
||||||
null=True,
|
null=True,
|
||||||
on_delete=django.db.models.deletion.SET_NULL,
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
related_name="tenant_invalidation",
|
related_name="brand_invalidation",
|
||||||
to="authentik_flows.flow",
|
to="authentik_flows.flow",
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -71,7 +71,7 @@ class Migration(migrations.Migration):
|
||||||
models.ForeignKey(
|
models.ForeignKey(
|
||||||
null=True,
|
null=True,
|
||||||
on_delete=django.db.models.deletion.SET_NULL,
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
related_name="tenant_recovery",
|
related_name="brand_recovery",
|
||||||
to="authentik_flows.flow",
|
to="authentik_flows.flow",
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -80,23 +80,23 @@ class Migration(migrations.Migration):
|
||||||
models.ForeignKey(
|
models.ForeignKey(
|
||||||
null=True,
|
null=True,
|
||||||
on_delete=django.db.models.deletion.SET_NULL,
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
related_name="tenant_unenrollment",
|
related_name="brand_unenrollment",
|
||||||
to="authentik_flows.flow",
|
to="authentik_flows.flow",
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
"verbose_name": "Tenant",
|
"verbose_name": "Brand",
|
||||||
"verbose_name_plural": "Tenants",
|
"verbose_name_plural": "Brands",
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name="tenant",
|
model_name="brand",
|
||||||
name="branding_favicon",
|
name="branding_favicon",
|
||||||
field=models.TextField(default="/static/dist/assets/icons/icon.png"),
|
field=models.TextField(default="/static/dist/assets/icons/icon.png"),
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name="tenant",
|
model_name="brand",
|
||||||
name="event_retention",
|
name="event_retention",
|
||||||
field=models.TextField(
|
field=models.TextField(
|
||||||
default="days=365",
|
default="days=365",
|
||||||
|
@ -108,7 +108,7 @@ class Migration(migrations.Migration):
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name="tenant",
|
model_name="brand",
|
||||||
name="web_certificate",
|
name="web_certificate",
|
||||||
field=models.ForeignKey(
|
field=models.ForeignKey(
|
||||||
default=None,
|
default=None,
|
|
@ -8,17 +8,17 @@ class Migration(migrations.Migration):
|
||||||
dependencies = [
|
dependencies = [
|
||||||
("authentik_stages_prompt", "0007_prompt_placeholder_expression"),
|
("authentik_stages_prompt", "0007_prompt_placeholder_expression"),
|
||||||
("authentik_flows", "0021_auto_20211227_2103"),
|
("authentik_flows", "0021_auto_20211227_2103"),
|
||||||
("authentik_tenants", "0001_squashed_0005_tenant_web_certificate"),
|
("authentik_brands", "0001_squashed_0005_tenant_web_certificate"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name="tenant",
|
model_name="brand",
|
||||||
name="flow_user_settings",
|
name="flow_user_settings",
|
||||||
field=models.ForeignKey(
|
field=models.ForeignKey(
|
||||||
null=True,
|
null=True,
|
||||||
on_delete=django.db.models.deletion.SET_NULL,
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
related_name="tenant_user_settings",
|
related_name="brand_user_settings",
|
||||||
to="authentik_flows.flow",
|
to="authentik_flows.flow",
|
||||||
),
|
),
|
||||||
),
|
),
|
|
@ -5,12 +5,12 @@ from django.db import migrations, models
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
dependencies = [
|
dependencies = [
|
||||||
("authentik_tenants", "0002_tenant_flow_user_settings"),
|
("authentik_brands", "0002_tenant_flow_user_settings"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name="tenant",
|
model_name="brand",
|
||||||
name="attributes",
|
name="attributes",
|
||||||
field=models.JSONField(blank=True, default=dict),
|
field=models.JSONField(blank=True, default=dict),
|
||||||
),
|
),
|
|
@ -7,17 +7,17 @@ from django.db import migrations, models
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
dependencies = [
|
dependencies = [
|
||||||
("authentik_flows", "0023_flow_denied_action"),
|
("authentik_flows", "0023_flow_denied_action"),
|
||||||
("authentik_tenants", "0003_tenant_attributes"),
|
("authentik_brands", "0003_tenant_attributes"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name="tenant",
|
model_name="brand",
|
||||||
name="flow_device_code",
|
name="flow_device_code",
|
||||||
field=models.ForeignKey(
|
field=models.ForeignKey(
|
||||||
null=True,
|
null=True,
|
||||||
on_delete=django.db.models.deletion.SET_NULL,
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
related_name="tenant_device_code",
|
related_name="brand_device_code",
|
||||||
to="authentik_flows.flow",
|
to="authentik_flows.flow",
|
||||||
),
|
),
|
||||||
),
|
),
|
|
@ -0,0 +1,119 @@
|
||||||
|
# Generated by Django 4.2.7 on 2023-11-06 19:48
|
||||||
|
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
import authentik.lib.utils.time
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="Brand",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"brand_uuid",
|
||||||
|
models.UUIDField(
|
||||||
|
default=uuid.uuid4, editable=False, primary_key=True, serialize=False
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"domain",
|
||||||
|
models.TextField(
|
||||||
|
help_text="Domain that activates this brand. 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"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"branding_favicon",
|
||||||
|
models.TextField(default="/static/dist/assets/icons/icon.png"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"event_retention",
|
||||||
|
models.TextField(
|
||||||
|
default="days=365",
|
||||||
|
help_text="Events will be deleted after this duration.(Format: weeks=3;days=2;hours=3,seconds=2).",
|
||||||
|
validators=[authentik.lib.utils.time.timedelta_string_validator],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("attributes", models.JSONField(blank=True, default=dict)),
|
||||||
|
(
|
||||||
|
"flow_authentication",
|
||||||
|
models.ForeignKey(
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
|
related_name="brand_authentication",
|
||||||
|
to="authentik_flows.flow",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"flow_device_code",
|
||||||
|
models.ForeignKey(
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
|
related_name="brand_device_code",
|
||||||
|
to="authentik_flows.flow",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"flow_invalidation",
|
||||||
|
models.ForeignKey(
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
|
related_name="brand_invalidation",
|
||||||
|
to="authentik_flows.flow",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"flow_recovery",
|
||||||
|
models.ForeignKey(
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
|
related_name="brand_recovery",
|
||||||
|
to="authentik_flows.flow",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"flow_unenrollment",
|
||||||
|
models.ForeignKey(
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
|
related_name="brand_unenrollment",
|
||||||
|
to="authentik_flows.flow",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"flow_user_settings",
|
||||||
|
models.ForeignKey(
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
|
related_name="brand_user_settings",
|
||||||
|
to="authentik_flows.flow",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"web_certificate",
|
||||||
|
models.ForeignKey(
|
||||||
|
default=None,
|
||||||
|
help_text="Web Certificate used by the authentik Core webserver.",
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.SET_DEFAULT,
|
||||||
|
to="authentik_crypto.certificatekeypair",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"verbose_name": "Brand",
|
||||||
|
"verbose_name_plural": "Brands",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,14 @@
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
"""
|
||||||
|
Noop migration to make sure that data has been migrated from the old tenant system to this before changing this table any further.
|
||||||
|
"""
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("authentik_brands", "0001_initial"),
|
||||||
|
("authentik_tenants", "0005_tenant_to_brand"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = []
|
|
@ -0,0 +1,94 @@
|
||||||
|
"""brand models"""
|
||||||
|
from uuid import uuid4
|
||||||
|
|
||||||
|
from django.db import models
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from rest_framework.serializers import Serializer
|
||||||
|
from structlog.stdlib import get_logger
|
||||||
|
|
||||||
|
from authentik.crypto.models import CertificateKeyPair
|
||||||
|
from authentik.flows.models import Flow
|
||||||
|
from authentik.lib.models import SerializerModel
|
||||||
|
from authentik.lib.utils.time import timedelta_string_validator
|
||||||
|
|
||||||
|
LOGGER = get_logger()
|
||||||
|
|
||||||
|
|
||||||
|
class Brand(SerializerModel):
|
||||||
|
"""Single brand"""
|
||||||
|
|
||||||
|
brand_uuid = models.UUIDField(primary_key=True, editable=False, default=uuid4)
|
||||||
|
domain = models.TextField(
|
||||||
|
help_text=_(
|
||||||
|
"Domain that activates this brand. 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")
|
||||||
|
branding_favicon = models.TextField(default="/static/dist/assets/icons/icon.png")
|
||||||
|
|
||||||
|
flow_authentication = models.ForeignKey(
|
||||||
|
Flow, null=True, on_delete=models.SET_NULL, related_name="brand_authentication"
|
||||||
|
)
|
||||||
|
flow_invalidation = models.ForeignKey(
|
||||||
|
Flow, null=True, on_delete=models.SET_NULL, related_name="brand_invalidation"
|
||||||
|
)
|
||||||
|
flow_recovery = models.ForeignKey(
|
||||||
|
Flow, null=True, on_delete=models.SET_NULL, related_name="brand_recovery"
|
||||||
|
)
|
||||||
|
flow_unenrollment = models.ForeignKey(
|
||||||
|
Flow, null=True, on_delete=models.SET_NULL, related_name="brand_unenrollment"
|
||||||
|
)
|
||||||
|
flow_user_settings = models.ForeignKey(
|
||||||
|
Flow, null=True, on_delete=models.SET_NULL, related_name="brand_user_settings"
|
||||||
|
)
|
||||||
|
flow_device_code = models.ForeignKey(
|
||||||
|
Flow, null=True, on_delete=models.SET_NULL, related_name="brand_device_code"
|
||||||
|
)
|
||||||
|
|
||||||
|
event_retention = models.TextField(
|
||||||
|
default="days=365",
|
||||||
|
validators=[timedelta_string_validator],
|
||||||
|
help_text=_(
|
||||||
|
"Events will be deleted after this duration.(Format: weeks=3;days=2;hours=3,seconds=2)."
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
web_certificate = models.ForeignKey(
|
||||||
|
CertificateKeyPair,
|
||||||
|
null=True,
|
||||||
|
default=None,
|
||||||
|
on_delete=models.SET_DEFAULT,
|
||||||
|
help_text=_("Web Certificate used by the authentik Core webserver."),
|
||||||
|
)
|
||||||
|
attributes = models.JSONField(default=dict, blank=True)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def serializer(self) -> Serializer:
|
||||||
|
from authentik.brands.api import BrandSerializer
|
||||||
|
|
||||||
|
return BrandSerializer
|
||||||
|
|
||||||
|
@property
|
||||||
|
def default_locale(self) -> str:
|
||||||
|
"""Get default locale"""
|
||||||
|
try:
|
||||||
|
return self.attributes.get("settings", {}).get("locale", "")
|
||||||
|
# pylint: disable=broad-except
|
||||||
|
except Exception as exc:
|
||||||
|
LOGGER.warning("Failed to get default locale", exc=exc)
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
if self.default:
|
||||||
|
return "Default brand"
|
||||||
|
return f"Brand {self.domain}"
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _("Brand")
|
||||||
|
verbose_name_plural = _("Brands")
|
|
@ -1,73 +1,73 @@
|
||||||
"""Test tenants"""
|
"""Test brands"""
|
||||||
from django.test.client import RequestFactory
|
from django.test.client import RequestFactory
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from rest_framework.test import APITestCase
|
from rest_framework.test import APITestCase
|
||||||
|
|
||||||
from authentik.core.tests.utils import create_test_admin_user, create_test_tenant
|
from authentik.brands.api import Themes
|
||||||
|
from authentik.brands.models import Brand
|
||||||
|
from authentik.core.tests.utils import create_test_admin_user, create_test_brand
|
||||||
from authentik.events.models import Event, EventAction
|
from authentik.events.models import Event, EventAction
|
||||||
from authentik.lib.config import CONFIG
|
from authentik.lib.config import CONFIG
|
||||||
from authentik.lib.utils.time import timedelta_from_string
|
from authentik.lib.utils.time import timedelta_from_string
|
||||||
from authentik.tenants.api import Themes
|
|
||||||
from authentik.tenants.models import Tenant
|
|
||||||
|
|
||||||
|
|
||||||
class TestTenants(APITestCase):
|
class TestBrands(APITestCase):
|
||||||
"""Test tenants"""
|
"""Test brands"""
|
||||||
|
|
||||||
def test_current_tenant(self):
|
def test_current_brand(self):
|
||||||
"""Test Current tenant API"""
|
"""Test Current brand API"""
|
||||||
tenant = create_test_tenant()
|
brand = create_test_brand()
|
||||||
self.assertJSONEqual(
|
self.assertJSONEqual(
|
||||||
self.client.get(reverse("authentik_api:tenant-current")).content.decode(),
|
self.client.get(reverse("authentik_api:brand-current")).content.decode(),
|
||||||
{
|
{
|
||||||
"branding_logo": "/static/dist/assets/icons/icon_left_brand.svg",
|
"branding_logo": "/static/dist/assets/icons/icon_left_brand.svg",
|
||||||
"branding_favicon": "/static/dist/assets/icons/icon.png",
|
"branding_favicon": "/static/dist/assets/icons/icon.png",
|
||||||
"branding_title": "authentik",
|
"branding_title": "authentik",
|
||||||
"matched_domain": tenant.domain,
|
"matched_domain": brand.domain,
|
||||||
"ui_footer_links": CONFIG.get("footer_links"),
|
"ui_footer_links": [],
|
||||||
"ui_theme": Themes.AUTOMATIC,
|
"ui_theme": Themes.AUTOMATIC,
|
||||||
"default_locale": "",
|
"default_locale": "",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_tenant_subdomain(self):
|
def test_brand_subdomain(self):
|
||||||
"""Test Current tenant API"""
|
"""Test Current brand API"""
|
||||||
Tenant.objects.all().delete()
|
Brand.objects.all().delete()
|
||||||
Tenant.objects.create(domain="bar.baz", branding_title="custom")
|
Brand.objects.create(domain="bar.baz", branding_title="custom")
|
||||||
self.assertJSONEqual(
|
self.assertJSONEqual(
|
||||||
self.client.get(
|
self.client.get(
|
||||||
reverse("authentik_api:tenant-current"), HTTP_HOST="foo.bar.baz"
|
reverse("authentik_api:brand-current"), HTTP_HOST="foo.bar.baz"
|
||||||
).content.decode(),
|
).content.decode(),
|
||||||
{
|
{
|
||||||
"branding_logo": "/static/dist/assets/icons/icon_left_brand.svg",
|
"branding_logo": "/static/dist/assets/icons/icon_left_brand.svg",
|
||||||
"branding_favicon": "/static/dist/assets/icons/icon.png",
|
"branding_favicon": "/static/dist/assets/icons/icon.png",
|
||||||
"branding_title": "custom",
|
"branding_title": "custom",
|
||||||
"matched_domain": "bar.baz",
|
"matched_domain": "bar.baz",
|
||||||
"ui_footer_links": CONFIG.get("footer_links"),
|
"ui_footer_links": [],
|
||||||
"ui_theme": Themes.AUTOMATIC,
|
"ui_theme": Themes.AUTOMATIC,
|
||||||
"default_locale": "",
|
"default_locale": "",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_fallback(self):
|
def test_fallback(self):
|
||||||
"""Test fallback tenant"""
|
"""Test fallback brand"""
|
||||||
Tenant.objects.all().delete()
|
Brand.objects.all().delete()
|
||||||
self.assertJSONEqual(
|
self.assertJSONEqual(
|
||||||
self.client.get(reverse("authentik_api:tenant-current")).content.decode(),
|
self.client.get(reverse("authentik_api:brand-current")).content.decode(),
|
||||||
{
|
{
|
||||||
"branding_logo": "/static/dist/assets/icons/icon_left_brand.svg",
|
"branding_logo": "/static/dist/assets/icons/icon_left_brand.svg",
|
||||||
"branding_favicon": "/static/dist/assets/icons/icon.png",
|
"branding_favicon": "/static/dist/assets/icons/icon.png",
|
||||||
"branding_title": "authentik",
|
"branding_title": "authentik",
|
||||||
"matched_domain": "fallback",
|
"matched_domain": "fallback",
|
||||||
"ui_footer_links": CONFIG.get("footer_links"),
|
"ui_footer_links": [],
|
||||||
"ui_theme": Themes.AUTOMATIC,
|
"ui_theme": Themes.AUTOMATIC,
|
||||||
"default_locale": "",
|
"default_locale": "",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_event_retention(self):
|
def test_event_retention(self):
|
||||||
"""Test tenant's event retention"""
|
"""Test brand's event retention"""
|
||||||
tenant = Tenant.objects.create(
|
brand = Brand.objects.create(
|
||||||
domain="foo",
|
domain="foo",
|
||||||
default=True,
|
default=True,
|
||||||
branding_title="custom",
|
branding_title="custom",
|
||||||
|
@ -75,7 +75,7 @@ class TestTenants(APITestCase):
|
||||||
)
|
)
|
||||||
factory = RequestFactory()
|
factory = RequestFactory()
|
||||||
request = factory.get("/")
|
request = factory.get("/")
|
||||||
request.tenant = tenant
|
request.brand = brand
|
||||||
event = Event.new(action=EventAction.SYSTEM_EXCEPTION, message="test").from_http(request)
|
event = Event.new(action=EventAction.SYSTEM_EXCEPTION, message="test").from_http(request)
|
||||||
self.assertEqual(event.expires.day, (event.created + timedelta_from_string("weeks=3")).day)
|
self.assertEqual(event.expires.day, (event.created + timedelta_from_string("weeks=3")).day)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
|
@ -87,8 +87,8 @@ class TestTenants(APITestCase):
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_create_default_multiple(self):
|
def test_create_default_multiple(self):
|
||||||
"""Test attempted creation of multiple default tenants"""
|
"""Test attempted creation of multiple default brands"""
|
||||||
Tenant.objects.create(
|
Brand.objects.create(
|
||||||
domain="foo",
|
domain="foo",
|
||||||
default=True,
|
default=True,
|
||||||
branding_title="custom",
|
branding_title="custom",
|
||||||
|
@ -97,6 +97,6 @@ class TestTenants(APITestCase):
|
||||||
user = create_test_admin_user()
|
user = create_test_admin_user()
|
||||||
self.client.force_login(user)
|
self.client.force_login(user)
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
reverse("authentik_api:tenant-list"), data={"domain": "bar", "default": True}
|
reverse("authentik_api:brand-list"), data={"domain": "bar", "default": True}
|
||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, 400)
|
self.assertEqual(response.status_code, 400)
|
|
@ -0,0 +1,6 @@
|
||||||
|
"""API URLs"""
|
||||||
|
from authentik.brands.api import BrandViewSet
|
||||||
|
|
||||||
|
api_urlpatterns = [
|
||||||
|
("core/brands", BrandViewSet),
|
||||||
|
]
|
|
@ -0,0 +1,43 @@
|
||||||
|
"""Brand utilities"""
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from django.db.models import F, Q
|
||||||
|
from django.db.models import Value as V
|
||||||
|
from django.http.request import HttpRequest
|
||||||
|
from sentry_sdk.hub import Hub
|
||||||
|
|
||||||
|
from authentik import get_full_version
|
||||||
|
from authentik.brands.models import Brand
|
||||||
|
from authentik.lib.config import CONFIG
|
||||||
|
from authentik.tenants.utils import get_current_tenant
|
||||||
|
|
||||||
|
_q_default = Q(default=True)
|
||||||
|
DEFAULT_BRAND = Brand(domain="fallback")
|
||||||
|
|
||||||
|
|
||||||
|
def get_brand_for_request(request: HttpRequest) -> Brand:
|
||||||
|
"""Get brand object for current request"""
|
||||||
|
db_brands = (
|
||||||
|
Brand.objects.annotate(host_domain=V(request.get_host()))
|
||||||
|
.filter(Q(host_domain__iendswith=F("domain")) | _q_default)
|
||||||
|
.order_by("default")
|
||||||
|
)
|
||||||
|
brands = list(db_brands.all())
|
||||||
|
if len(brands) < 1:
|
||||||
|
return DEFAULT_BRAND
|
||||||
|
return brands[0]
|
||||||
|
|
||||||
|
|
||||||
|
def context_processor(request: HttpRequest) -> dict[str, Any]:
|
||||||
|
"""Context Processor that injects brand object into every template"""
|
||||||
|
brand = getattr(request, "brand", DEFAULT_BRAND)
|
||||||
|
trace = ""
|
||||||
|
span = Hub.current.scope.span
|
||||||
|
if span:
|
||||||
|
trace = span.to_traceparent()
|
||||||
|
return {
|
||||||
|
"brand": brand,
|
||||||
|
"footer_links": get_current_tenant().footer_links,
|
||||||
|
"sentry_trace": trace,
|
||||||
|
"version": get_full_version(),
|
||||||
|
}
|
|
@ -56,6 +56,7 @@ from structlog.stdlib import get_logger
|
||||||
from authentik.admin.api.metrics import CoordinateSerializer
|
from authentik.admin.api.metrics import CoordinateSerializer
|
||||||
from authentik.api.decorators import permission_required
|
from authentik.api.decorators import permission_required
|
||||||
from authentik.blueprints.v1.importer import SERIALIZER_CONTEXT_BLUEPRINT
|
from authentik.blueprints.v1.importer import SERIALIZER_CONTEXT_BLUEPRINT
|
||||||
|
from authentik.brands.models import Brand
|
||||||
from authentik.core.api.used_by import UsedByMixin
|
from authentik.core.api.used_by import UsedByMixin
|
||||||
from authentik.core.api.utils import LinkSerializer, PassiveSerializer, is_dict
|
from authentik.core.api.utils import LinkSerializer, PassiveSerializer, is_dict
|
||||||
from authentik.core.middleware import (
|
from authentik.core.middleware import (
|
||||||
|
@ -81,7 +82,6 @@ from authentik.lib.config import CONFIG
|
||||||
from authentik.stages.email.models import EmailStage
|
from authentik.stages.email.models import EmailStage
|
||||||
from authentik.stages.email.tasks import send_mails
|
from authentik.stages.email.tasks import send_mails
|
||||||
from authentik.stages.email.utils import TemplateEmailMessage
|
from authentik.stages.email.utils import TemplateEmailMessage
|
||||||
from authentik.tenants.models import Tenant
|
|
||||||
|
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
|
|
||||||
|
@ -227,7 +227,7 @@ class UserSelfSerializer(ModelSerializer):
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_settings(self, user: User) -> dict[str, Any]:
|
def get_settings(self, user: User) -> dict[str, Any]:
|
||||||
"""Get user settings with tenant and group settings applied"""
|
"""Get user settings with brand and group settings applied"""
|
||||||
return user.group_attributes(self._context["request"]).get("settings", {})
|
return user.group_attributes(self._context["request"]).get("settings", {})
|
||||||
|
|
||||||
def get_system_permissions(self, user: User) -> list[str]:
|
def get_system_permissions(self, user: User) -> list[str]:
|
||||||
|
@ -388,11 +388,11 @@ class UserViewSet(UsedByMixin, ModelViewSet):
|
||||||
return User.objects.all().exclude(pk=get_anonymous_user().pk)
|
return User.objects.all().exclude(pk=get_anonymous_user().pk)
|
||||||
|
|
||||||
def _create_recovery_link(self) -> tuple[Optional[str], Optional[Token]]:
|
def _create_recovery_link(self) -> tuple[Optional[str], Optional[Token]]:
|
||||||
"""Create a recovery link (when the current tenant has a recovery flow set),
|
"""Create a recovery link (when the current brand has a recovery flow set),
|
||||||
that can either be shown to an admin or sent to the user directly"""
|
that can either be shown to an admin or sent to the user directly"""
|
||||||
tenant: Tenant = self.request._request.tenant
|
brand: Brand = self.request._request.brand
|
||||||
# Check that there is a recovery flow, if not return an error
|
# Check that there is a recovery flow, if not return an error
|
||||||
flow = tenant.flow_recovery
|
flow = brand.flow_recovery
|
||||||
if not flow:
|
if not flow:
|
||||||
LOGGER.debug("No recovery flow set")
|
LOGGER.debug("No recovery flow set")
|
||||||
return None, None
|
return None, None
|
||||||
|
@ -624,7 +624,7 @@ class UserViewSet(UsedByMixin, ModelViewSet):
|
||||||
@action(detail=True, methods=["POST"])
|
@action(detail=True, methods=["POST"])
|
||||||
def impersonate(self, request: Request, pk: int) -> Response:
|
def impersonate(self, request: Request, pk: int) -> Response:
|
||||||
"""Impersonate a user"""
|
"""Impersonate a user"""
|
||||||
if not CONFIG.get_bool("impersonation"):
|
if not request.tenant.impersonation:
|
||||||
LOGGER.debug("User attempted to impersonate", user=request.user)
|
LOGGER.debug("User attempted to impersonate", user=request.user)
|
||||||
return Response(status=401)
|
return Response(status=401)
|
||||||
if not request.user.has_perm("impersonate"):
|
if not request.user.has_perm("impersonate"):
|
||||||
|
|
|
@ -202,8 +202,8 @@ class User(SerializerModel, GuardianUserMixin, AbstractUser):
|
||||||
"""Get a dictionary containing the attributes from all groups the user belongs to,
|
"""Get a dictionary containing the attributes from all groups the user belongs to,
|
||||||
including the users attributes"""
|
including the users attributes"""
|
||||||
final_attributes = {}
|
final_attributes = {}
|
||||||
if request and hasattr(request, "tenant"):
|
if request and hasattr(request, "brand"):
|
||||||
always_merger.merge(final_attributes, request.tenant.attributes)
|
always_merger.merge(final_attributes, request.brand.attributes)
|
||||||
for group in self.all_groups().order_by("name"):
|
for group in self.all_groups().order_by("name"):
|
||||||
always_merger.merge(final_attributes, group.attributes)
|
always_merger.merge(final_attributes, group.attributes)
|
||||||
always_merger.merge(final_attributes, self.attributes)
|
always_merger.merge(final_attributes, self.attributes)
|
||||||
|
@ -262,7 +262,7 @@ class User(SerializerModel, GuardianUserMixin, AbstractUser):
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
LOGGER.warning("Failed to get default locale", exc=exc)
|
LOGGER.warning("Failed to get default locale", exc=exc)
|
||||||
if request:
|
if request:
|
||||||
return request.tenant.locale
|
return request.brand.locale
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
window.authentik = {
|
window.authentik = {
|
||||||
locale: "{{ LANGUAGE_CODE }}",
|
locale: "{{ LANGUAGE_CODE }}",
|
||||||
config: JSON.parse('{{ config_json|escapejs }}'),
|
config: JSON.parse('{{ config_json|escapejs }}'),
|
||||||
tenant: JSON.parse('{{ tenant_json|escapejs }}'),
|
brand: JSON.parse('{{ brand_json|escapejs }}'),
|
||||||
versionFamily: "{{ version_family }}",
|
versionFamily: "{{ version_family }}",
|
||||||
versionSubdomain: "{{ version_subdomain }}",
|
versionSubdomain: "{{ version_subdomain }}",
|
||||||
build: "{{ build }}",
|
build: "{{ build }}",
|
||||||
|
|
|
@ -7,9 +7,9 @@
|
||||||
<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:tenant.branding_title %}{% endblock %}</title>
|
<title>{% block title %}{% trans title|default:brand.branding_title %}{% endblock %}</title>
|
||||||
<link rel="icon" href="{{ tenant.branding_favicon }}">
|
<link rel="icon" href="{{ brand.branding_favicon }}">
|
||||||
<link rel="shortcut icon" href="{{ tenant.branding_favicon }}">
|
<link rel="shortcut icon" href="{{ brand.branding_favicon }}">
|
||||||
{% block head_before %}
|
{% block head_before %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
<link rel="stylesheet" type="text/css" href="{% static 'dist/authentik.css' %}">
|
<link rel="stylesheet" type="text/css" href="{% static 'dist/authentik.css' %}">
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
{% block title %}
|
{% block title %}
|
||||||
{% trans 'End session' %} - {{ tenant.branding_title }}
|
{% trans 'End session' %} - {{ brand.branding_title }}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block card_title %}
|
{% block card_title %}
|
||||||
|
@ -16,7 +16,7 @@ You've logged out of {{ application }}.
|
||||||
{% block card %}
|
{% block card %}
|
||||||
<form method="POST" class="pf-c-form">
|
<form method="POST" class="pf-c-form">
|
||||||
<p>
|
<p>
|
||||||
{% blocktrans with application=application.name branding_title=tenant.branding_title %}
|
{% blocktrans with application=application.name branding_title=brand.branding_title %}
|
||||||
You've logged out of {{ application }}. You can go back to the overview to launch another application, or log out of your {{ branding_title }} account.
|
You've logged out of {{ application }}. You can go back to the overview to launch another application, or log out of your {{ branding_title }} account.
|
||||||
{% endblocktrans %}
|
{% endblocktrans %}
|
||||||
</p>
|
</p>
|
||||||
|
@ -26,7 +26,7 @@ You've logged out of {{ application }}.
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a id="logout" href="{% url 'authentik_flows:default-invalidation' %}" class="pf-c-button pf-m-secondary">
|
<a id="logout" href="{% url 'authentik_flows:default-invalidation' %}" class="pf-c-button pf-m-secondary">
|
||||||
{% blocktrans with branding_title=tenant.branding_title %}
|
{% blocktrans with branding_title=brand.branding_title %}
|
||||||
Log out of {{ branding_title }}
|
Log out of {{ branding_title }}
|
||||||
{% endblocktrans %}
|
{% endblocktrans %}
|
||||||
</a>
|
</a>
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
{% block title %}
|
{% block title %}
|
||||||
{{ tenant.branding_title }}
|
{{ brand.branding_title }}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block card_title %}
|
{% block card_title %}
|
||||||
|
|
|
@ -60,7 +60,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="{{ tenant.branding_logo }}" alt="authentik Logo" />
|
<img src="{{ brand.branding_logo }}" alt="authentik Logo" />
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
{% block main_container %}
|
{% block main_container %}
|
||||||
|
|
|
@ -3,10 +3,10 @@ from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
|
from authentik.brands.models import Brand
|
||||||
from authentik.core.models import Application
|
from authentik.core.models import Application
|
||||||
from authentik.core.tests.utils import create_test_admin_user, create_test_flow, create_test_tenant
|
from authentik.core.tests.utils import create_test_admin_user, create_test_brand, create_test_flow
|
||||||
from authentik.flows.tests import FlowTestCase
|
from authentik.flows.tests import FlowTestCase
|
||||||
from authentik.tenants.models import Tenant
|
|
||||||
|
|
||||||
|
|
||||||
class TestApplicationsViews(FlowTestCase):
|
class TestApplicationsViews(FlowTestCase):
|
||||||
|
@ -21,9 +21,9 @@ class TestApplicationsViews(FlowTestCase):
|
||||||
def test_check_redirect(self):
|
def test_check_redirect(self):
|
||||||
"""Test redirect"""
|
"""Test redirect"""
|
||||||
empty_flow = create_test_flow()
|
empty_flow = create_test_flow()
|
||||||
tenant: Tenant = create_test_tenant()
|
brand: Brand = create_test_brand()
|
||||||
tenant.flow_authentication = empty_flow
|
brand.flow_authentication = empty_flow
|
||||||
tenant.save()
|
brand.save()
|
||||||
response = self.client.get(
|
response = self.client.get(
|
||||||
reverse(
|
reverse(
|
||||||
"authentik_core:application-launch",
|
"authentik_core:application-launch",
|
||||||
|
@ -45,9 +45,9 @@ class TestApplicationsViews(FlowTestCase):
|
||||||
"""Test redirect"""
|
"""Test redirect"""
|
||||||
self.client.force_login(self.user)
|
self.client.force_login(self.user)
|
||||||
empty_flow = create_test_flow()
|
empty_flow = create_test_flow()
|
||||||
tenant: Tenant = create_test_tenant()
|
brand: Brand = create_test_brand()
|
||||||
tenant.flow_authentication = empty_flow
|
brand.flow_authentication = empty_flow
|
||||||
tenant.save()
|
brand.save()
|
||||||
response = self.client.get(
|
response = self.client.get(
|
||||||
reverse(
|
reverse(
|
||||||
"authentik_core:application-launch",
|
"authentik_core:application-launch",
|
||||||
|
|
|
@ -7,6 +7,7 @@ from django.core.cache import cache
|
||||||
from django.urls.base import reverse
|
from django.urls.base import reverse
|
||||||
from rest_framework.test import APITestCase
|
from rest_framework.test import APITestCase
|
||||||
|
|
||||||
|
from authentik.brands.models import Brand
|
||||||
from authentik.core.models import (
|
from authentik.core.models import (
|
||||||
USER_ATTRIBUTE_TOKEN_EXPIRING,
|
USER_ATTRIBUTE_TOKEN_EXPIRING,
|
||||||
AuthenticatedSession,
|
AuthenticatedSession,
|
||||||
|
@ -14,11 +15,10 @@ from authentik.core.models import (
|
||||||
User,
|
User,
|
||||||
UserTypes,
|
UserTypes,
|
||||||
)
|
)
|
||||||
from authentik.core.tests.utils import create_test_admin_user, create_test_flow, create_test_tenant
|
from authentik.core.tests.utils import create_test_admin_user, create_test_brand, create_test_flow
|
||||||
from authentik.flows.models import FlowDesignation
|
from authentik.flows.models import FlowDesignation
|
||||||
from authentik.lib.generators import generate_id, generate_key
|
from authentik.lib.generators import generate_id, generate_key
|
||||||
from authentik.stages.email.models import EmailStage
|
from authentik.stages.email.models import EmailStage
|
||||||
from authentik.tenants.models import Tenant
|
|
||||||
|
|
||||||
|
|
||||||
class TestUsersAPI(APITestCase):
|
class TestUsersAPI(APITestCase):
|
||||||
|
@ -80,9 +80,9 @@ class TestUsersAPI(APITestCase):
|
||||||
def test_recovery(self):
|
def test_recovery(self):
|
||||||
"""Test user recovery link (no recovery flow set)"""
|
"""Test user recovery link (no recovery flow set)"""
|
||||||
flow = create_test_flow(FlowDesignation.RECOVERY)
|
flow = create_test_flow(FlowDesignation.RECOVERY)
|
||||||
tenant: Tenant = create_test_tenant()
|
brand: Brand = create_test_brand()
|
||||||
tenant.flow_recovery = flow
|
brand.flow_recovery = flow
|
||||||
tenant.save()
|
brand.save()
|
||||||
self.client.force_login(self.admin)
|
self.client.force_login(self.admin)
|
||||||
response = self.client.get(
|
response = self.client.get(
|
||||||
reverse("authentik_api:user-recovery", kwargs={"pk": self.user.pk})
|
reverse("authentik_api:user-recovery", kwargs={"pk": self.user.pk})
|
||||||
|
@ -108,9 +108,9 @@ class TestUsersAPI(APITestCase):
|
||||||
self.user.email = "foo@bar.baz"
|
self.user.email = "foo@bar.baz"
|
||||||
self.user.save()
|
self.user.save()
|
||||||
flow = create_test_flow(designation=FlowDesignation.RECOVERY)
|
flow = create_test_flow(designation=FlowDesignation.RECOVERY)
|
||||||
tenant: Tenant = create_test_tenant()
|
brand: Brand = create_test_brand()
|
||||||
tenant.flow_recovery = flow
|
brand.flow_recovery = flow
|
||||||
tenant.save()
|
brand.save()
|
||||||
self.client.force_login(self.admin)
|
self.client.force_login(self.admin)
|
||||||
response = self.client.get(
|
response = self.client.get(
|
||||||
reverse("authentik_api:user-recovery-email", kwargs={"pk": self.user.pk})
|
reverse("authentik_api:user-recovery-email", kwargs={"pk": self.user.pk})
|
||||||
|
@ -122,9 +122,9 @@ class TestUsersAPI(APITestCase):
|
||||||
self.user.email = "foo@bar.baz"
|
self.user.email = "foo@bar.baz"
|
||||||
self.user.save()
|
self.user.save()
|
||||||
flow = create_test_flow(FlowDesignation.RECOVERY)
|
flow = create_test_flow(FlowDesignation.RECOVERY)
|
||||||
tenant: Tenant = create_test_tenant()
|
brand: Brand = create_test_brand()
|
||||||
tenant.flow_recovery = flow
|
brand.flow_recovery = flow
|
||||||
tenant.save()
|
brand.save()
|
||||||
|
|
||||||
stage = EmailStage.objects.create(name="email")
|
stage = EmailStage.objects.create(name="email")
|
||||||
|
|
||||||
|
|
|
@ -3,12 +3,12 @@ from typing import Optional
|
||||||
|
|
||||||
from django.utils.text import slugify
|
from django.utils.text import slugify
|
||||||
|
|
||||||
|
from authentik.brands.models import Brand
|
||||||
from authentik.core.models import Group, User
|
from authentik.core.models import Group, User
|
||||||
from authentik.crypto.builder import CertificateBuilder
|
from authentik.crypto.builder import CertificateBuilder
|
||||||
from authentik.crypto.models import CertificateKeyPair
|
from authentik.crypto.models import CertificateKeyPair
|
||||||
from authentik.flows.models import Flow, FlowDesignation
|
from authentik.flows.models import Flow, FlowDesignation
|
||||||
from authentik.lib.generators import generate_id
|
from authentik.lib.generators import generate_id
|
||||||
from authentik.tenants.models import Tenant
|
|
||||||
|
|
||||||
|
|
||||||
def create_test_flow(
|
def create_test_flow(
|
||||||
|
@ -43,12 +43,12 @@ def create_test_admin_user(name: Optional[str] = None, **kwargs) -> User:
|
||||||
return user
|
return user
|
||||||
|
|
||||||
|
|
||||||
def create_test_tenant(**kwargs) -> Tenant:
|
def create_test_brand(**kwargs) -> Brand:
|
||||||
"""Generate a test tenant, removing all other tenants to make sure this one
|
"""Generate a test brand, removing all other brands to make sure this one
|
||||||
matches."""
|
matches."""
|
||||||
uid = generate_id(20)
|
uid = generate_id(20)
|
||||||
Tenant.objects.all().delete()
|
Brand.objects.all().delete()
|
||||||
return Tenant.objects.create(domain=uid, default=True, **kwargs)
|
return Brand.objects.create(domain=uid, default=True, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
def create_test_cert(use_ec_private_key=False) -> CertificateKeyPair:
|
def create_test_cert(use_ec_private_key=False) -> CertificateKeyPair:
|
||||||
|
|
|
@ -9,8 +9,8 @@ from rest_framework.request import Request
|
||||||
from authentik import get_build_hash
|
from authentik import get_build_hash
|
||||||
from authentik.admin.tasks import LOCAL_VERSION
|
from authentik.admin.tasks import LOCAL_VERSION
|
||||||
from authentik.api.v3.config import ConfigView
|
from authentik.api.v3.config import ConfigView
|
||||||
|
from authentik.brands.api import CurrentBrandSerializer
|
||||||
from authentik.flows.models import Flow
|
from authentik.flows.models import Flow
|
||||||
from authentik.tenants.api import CurrentTenantSerializer
|
|
||||||
|
|
||||||
|
|
||||||
class InterfaceView(TemplateView):
|
class InterfaceView(TemplateView):
|
||||||
|
@ -18,7 +18,7 @@ class InterfaceView(TemplateView):
|
||||||
|
|
||||||
def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
|
def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
|
||||||
kwargs["config_json"] = dumps(ConfigView(request=Request(self.request)).get_config().data)
|
kwargs["config_json"] = dumps(ConfigView(request=Request(self.request)).get_config().data)
|
||||||
kwargs["tenant_json"] = dumps(CurrentTenantSerializer(self.request.tenant).data)
|
kwargs["brand_json"] = dumps(CurrentBrandSerializer(self.request.brand).data)
|
||||||
kwargs["version_family"] = f"{LOCAL_VERSION.major}.{LOCAL_VERSION.minor}"
|
kwargs["version_family"] = f"{LOCAL_VERSION.major}.{LOCAL_VERSION.minor}"
|
||||||
kwargs["version_subdomain"] = f"version-{LOCAL_VERSION.major}-{LOCAL_VERSION.minor}"
|
kwargs["version_subdomain"] = f"version-{LOCAL_VERSION.major}-{LOCAL_VERSION.minor}"
|
||||||
kwargs["build"] = get_build_hash()
|
kwargs["build"] = get_build_hash()
|
||||||
|
|
|
@ -35,7 +35,7 @@ class EventSerializer(ModelSerializer):
|
||||||
"client_ip",
|
"client_ip",
|
||||||
"created",
|
"created",
|
||||||
"expires",
|
"expires",
|
||||||
"tenant",
|
"brand",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -76,10 +76,10 @@ class EventsFilter(django_filters.FilterSet):
|
||||||
field_name="action",
|
field_name="action",
|
||||||
lookup_expr="icontains",
|
lookup_expr="icontains",
|
||||||
)
|
)
|
||||||
tenant_name = django_filters.CharFilter(
|
brand_name = django_filters.CharFilter(
|
||||||
field_name="tenant",
|
field_name="brand",
|
||||||
lookup_expr="name",
|
lookup_expr="name",
|
||||||
label="Tenant name",
|
label="Brand name",
|
||||||
)
|
)
|
||||||
|
|
||||||
def filter_context_model_pk(self, queryset, name, value):
|
def filter_context_model_pk(self, queryset, name, value):
|
||||||
|
|
|
@ -305,7 +305,7 @@ class Migration(migrations.Migration):
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name="event",
|
model_name="event",
|
||||||
name="tenant",
|
name="tenant",
|
||||||
field=models.JSONField(blank=True, default=authentik.events.models.default_tenant),
|
field=models.JSONField(blank=True, default=authentik.events.models.default_brand),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name="event",
|
model_name="event",
|
||||||
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
# Generated by Django 4.2.7 on 2023-11-06 18:58
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("authentik_events", "0002_alter_notificationtransport_mode"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name="event",
|
||||||
|
old_name="tenant",
|
||||||
|
new_name="brand",
|
||||||
|
),
|
||||||
|
]
|
|
@ -21,6 +21,8 @@ from requests import RequestException
|
||||||
from structlog.stdlib import get_logger
|
from structlog.stdlib import get_logger
|
||||||
|
|
||||||
from authentik import get_full_version
|
from authentik import get_full_version
|
||||||
|
from authentik.brands.models import Brand
|
||||||
|
from authentik.brands.utils import DEFAULT_BRAND
|
||||||
from authentik.core.middleware import (
|
from authentik.core.middleware import (
|
||||||
SESSION_KEY_IMPERSONATE_ORIGINAL_USER,
|
SESSION_KEY_IMPERSONATE_ORIGINAL_USER,
|
||||||
SESSION_KEY_IMPERSONATE_USER,
|
SESSION_KEY_IMPERSONATE_USER,
|
||||||
|
@ -40,8 +42,6 @@ from authentik.lib.utils.http import get_client_ip, get_http_session
|
||||||
from authentik.lib.utils.time import timedelta_from_string
|
from authentik.lib.utils.time import timedelta_from_string
|
||||||
from authentik.policies.models import PolicyBindingModel
|
from authentik.policies.models import PolicyBindingModel
|
||||||
from authentik.stages.email.utils import TemplateEmailMessage
|
from authentik.stages.email.utils import TemplateEmailMessage
|
||||||
from authentik.tenants.models import Tenant
|
|
||||||
from authentik.tenants.utils import DEFAULT_TENANT
|
|
||||||
|
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
@ -50,13 +50,13 @@ if TYPE_CHECKING:
|
||||||
|
|
||||||
def default_event_duration():
|
def default_event_duration():
|
||||||
"""Default duration an Event is saved.
|
"""Default duration an Event is saved.
|
||||||
This is used as a fallback when no tenant is available"""
|
This is used as a fallback when no brand is available"""
|
||||||
return now() + timedelta(days=365)
|
return now() + timedelta(days=365)
|
||||||
|
|
||||||
|
|
||||||
def default_tenant():
|
def default_brand():
|
||||||
"""Get a default value for tenant"""
|
"""Get a default value for brand"""
|
||||||
return sanitize_dict(model_to_dict(DEFAULT_TENANT))
|
return sanitize_dict(model_to_dict(DEFAULT_BRAND))
|
||||||
|
|
||||||
|
|
||||||
class NotificationTransportError(SentryIgnoredException):
|
class NotificationTransportError(SentryIgnoredException):
|
||||||
|
@ -170,7 +170,7 @@ class Event(SerializerModel, ExpiringModel):
|
||||||
context = models.JSONField(default=dict, blank=True)
|
context = models.JSONField(default=dict, blank=True)
|
||||||
client_ip = models.GenericIPAddressField(null=True)
|
client_ip = models.GenericIPAddressField(null=True)
|
||||||
created = models.DateTimeField(auto_now_add=True)
|
created = models.DateTimeField(auto_now_add=True)
|
||||||
tenant = models.JSONField(default=default_tenant, blank=True)
|
brand = models.JSONField(default=default_brand, blank=True)
|
||||||
|
|
||||||
# Shadow the expires attribute from ExpiringModel to override the default duration
|
# Shadow the expires attribute from ExpiringModel to override the default duration
|
||||||
expires = models.DateTimeField(default=default_event_duration)
|
expires = models.DateTimeField(default=default_event_duration)
|
||||||
|
@ -223,13 +223,13 @@ class Event(SerializerModel, ExpiringModel):
|
||||||
if QS_QUERY in self.context["http_request"]["args"]:
|
if QS_QUERY in self.context["http_request"]["args"]:
|
||||||
wrapped = self.context["http_request"]["args"][QS_QUERY]
|
wrapped = self.context["http_request"]["args"][QS_QUERY]
|
||||||
self.context["http_request"]["args"] = cleanse_dict(QueryDict(wrapped))
|
self.context["http_request"]["args"] = cleanse_dict(QueryDict(wrapped))
|
||||||
if hasattr(request, "tenant"):
|
if hasattr(request, "brand"):
|
||||||
tenant: Tenant = request.tenant
|
brand: Brand = request.brand
|
||||||
# Because self.created only gets set on save, we can't use it's value here
|
# Because self.created only gets set on save, we can't use it's value here
|
||||||
# hence we set self.created to now and then use it
|
# hence we set self.created to now and then use it
|
||||||
self.created = now()
|
self.created = now()
|
||||||
self.expires = self.created + timedelta_from_string(tenant.event_retention)
|
self.expires = self.created + timedelta_from_string(brand.event_retention)
|
||||||
self.tenant = sanitize_dict(model_to_dict(tenant))
|
self.brand = sanitize_dict(model_to_dict(brand))
|
||||||
if hasattr(request, "user"):
|
if hasattr(request, "user"):
|
||||||
original_user = None
|
original_user = None
|
||||||
if hasattr(request, "session"):
|
if hasattr(request, "session"):
|
||||||
|
|
|
@ -18,6 +18,7 @@ from authentik.stages.invitation.models import Invitation
|
||||||
from authentik.stages.invitation.signals import invitation_used
|
from authentik.stages.invitation.signals import invitation_used
|
||||||
from authentik.stages.password.stage import PLAN_CONTEXT_METHOD, PLAN_CONTEXT_METHOD_ARGS
|
from authentik.stages.password.stage import PLAN_CONTEXT_METHOD, PLAN_CONTEXT_METHOD_ARGS
|
||||||
from authentik.stages.user_write.signals import user_write
|
from authentik.stages.user_write.signals import user_write
|
||||||
|
from authentik.tenants.utils import get_current_tenant
|
||||||
|
|
||||||
SESSION_LOGIN_EVENT = "login_event"
|
SESSION_LOGIN_EVENT = "login_event"
|
||||||
|
|
||||||
|
@ -93,5 +94,5 @@ def event_post_save_notification(sender, instance: Event, **_):
|
||||||
@receiver(pre_delete, sender=User)
|
@receiver(pre_delete, sender=User)
|
||||||
def event_user_pre_delete_cleanup(sender, instance: User, **_):
|
def event_user_pre_delete_cleanup(sender, instance: User, **_):
|
||||||
"""If gdpr_compliance is enabled, remove all the user's events"""
|
"""If gdpr_compliance is enabled, remove all the user's events"""
|
||||||
if CONFIG.get_bool("gdpr_compliance", True):
|
if get_current_tenant().avatars:
|
||||||
gdpr_cleanup.delay(instance.pk)
|
gdpr_cleanup.delay(instance.pk)
|
||||||
|
|
|
@ -6,12 +6,12 @@ from django.test import RequestFactory, TestCase
|
||||||
from django.views.debug import SafeExceptionReporterFilter
|
from django.views.debug import SafeExceptionReporterFilter
|
||||||
from guardian.shortcuts import get_anonymous_user
|
from guardian.shortcuts import get_anonymous_user
|
||||||
|
|
||||||
|
from authentik.brands.models import Brand
|
||||||
from authentik.core.models import Group
|
from authentik.core.models import Group
|
||||||
from authentik.events.models import Event
|
from authentik.events.models import Event
|
||||||
from authentik.flows.views.executor import QS_QUERY
|
from authentik.flows.views.executor import QS_QUERY
|
||||||
from authentik.lib.generators import generate_id
|
from authentik.lib.generators import generate_id
|
||||||
from authentik.policies.dummy.models import DummyPolicy
|
from authentik.policies.dummy.models import DummyPolicy
|
||||||
from authentik.tenants.models import Tenant
|
|
||||||
|
|
||||||
|
|
||||||
class TestEvents(TestCase):
|
class TestEvents(TestCase):
|
||||||
|
@ -87,19 +87,19 @@ class TestEvents(TestCase):
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_from_http_tenant(self):
|
def test_from_http_brand(self):
|
||||||
"""Test from_http tenant"""
|
"""Test from_http brand"""
|
||||||
# Test tenant
|
# Test brand
|
||||||
request = self.factory.get("/")
|
request = self.factory.get("/")
|
||||||
tenant = Tenant(domain="test-tenant")
|
brand = Brand(domain="test-brand")
|
||||||
setattr(request, "tenant", tenant)
|
setattr(request, "brand", brand)
|
||||||
event = Event.new("unittest").from_http(request)
|
event = Event.new("unittest").from_http(request)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
event.tenant,
|
event.brand,
|
||||||
{
|
{
|
||||||
"app": "authentik_tenants",
|
"app": "authentik_brands",
|
||||||
"model_name": "tenant",
|
"model_name": "brand",
|
||||||
"name": "Tenant test-tenant",
|
"name": "Brand test-brand",
|
||||||
"pk": tenant.pk.hex,
|
"pk": brand.pk.hex,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
|
@ -22,6 +22,7 @@ from sentry_sdk.api import set_tag
|
||||||
from sentry_sdk.hub import Hub
|
from sentry_sdk.hub import Hub
|
||||||
from structlog.stdlib import BoundLogger, get_logger
|
from structlog.stdlib import BoundLogger, get_logger
|
||||||
|
|
||||||
|
from authentik.brands.models import Brand
|
||||||
from authentik.core.models import Application
|
from authentik.core.models import Application
|
||||||
from authentik.events.models import Event, EventAction, cleanse_dict
|
from authentik.events.models import Event, EventAction, cleanse_dict
|
||||||
from authentik.flows.apps import HIST_FLOW_EXECUTION_STAGE_TIME
|
from authentik.flows.apps import HIST_FLOW_EXECUTION_STAGE_TIME
|
||||||
|
@ -60,7 +61,6 @@ from authentik.lib.utils.errors import exception_to_string
|
||||||
from authentik.lib.utils.reflection import all_subclasses, class_to_path
|
from authentik.lib.utils.reflection import all_subclasses, class_to_path
|
||||||
from authentik.lib.utils.urls import is_url_absolute, redirect_with_qs
|
from authentik.lib.utils.urls import is_url_absolute, redirect_with_qs
|
||||||
from authentik.policies.engine import PolicyEngine
|
from authentik.policies.engine import PolicyEngine
|
||||||
from authentik.tenants.models import Tenant
|
|
||||||
|
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
# Argument used to redirect user after login
|
# Argument used to redirect user after login
|
||||||
|
@ -490,11 +490,11 @@ class ToDefaultFlow(View):
|
||||||
|
|
||||||
def get_flow(self) -> Flow:
|
def get_flow(self) -> Flow:
|
||||||
"""Get a flow for the selected designation"""
|
"""Get a flow for the selected designation"""
|
||||||
tenant: Tenant = self.request.tenant
|
brand: Brand = self.request.brand
|
||||||
flow = None
|
flow = None
|
||||||
# First, attempt to get default flow from tenant
|
# First, attempt to get default flow from brand
|
||||||
if self.designation == FlowDesignation.AUTHENTICATION:
|
if self.designation == FlowDesignation.AUTHENTICATION:
|
||||||
flow = tenant.flow_authentication
|
flow = brand.flow_authentication
|
||||||
# Check if we have a default flow from application
|
# Check if we have a default flow from application
|
||||||
application: Optional[Application] = self.request.session.get(
|
application: Optional[Application] = self.request.session.get(
|
||||||
SESSION_KEY_APPLICATION_PRE
|
SESSION_KEY_APPLICATION_PRE
|
||||||
|
@ -502,7 +502,7 @@ class ToDefaultFlow(View):
|
||||||
if application and application.provider and application.provider.authentication_flow:
|
if application and application.provider and application.provider.authentication_flow:
|
||||||
flow = application.provider.authentication_flow
|
flow = application.provider.authentication_flow
|
||||||
elif self.designation == FlowDesignation.INVALIDATION:
|
elif self.designation == FlowDesignation.INVALIDATION:
|
||||||
flow = tenant.flow_invalidation
|
flow = brand.flow_invalidation
|
||||||
if flow:
|
if flow:
|
||||||
return flow
|
return flow
|
||||||
# If no flow was set, get the first based on slug and policy
|
# If no flow was set, get the first based on slug and policy
|
||||||
|
|
|
@ -13,6 +13,7 @@ from requests.exceptions import RequestException
|
||||||
|
|
||||||
from authentik.lib.config import CONFIG, get_path_from_dict
|
from authentik.lib.config import CONFIG, get_path_from_dict
|
||||||
from authentik.lib.utils.http import get_http_session
|
from authentik.lib.utils.http import get_http_session
|
||||||
|
from authentik.tenants.utils import get_current_tenant
|
||||||
|
|
||||||
GRAVATAR_URL = "https://secure.gravatar.com"
|
GRAVATAR_URL = "https://secure.gravatar.com"
|
||||||
DEFAULT_AVATAR = static("dist/assets/images/user_default.png")
|
DEFAULT_AVATAR = static("dist/assets/images/user_default.png")
|
||||||
|
@ -183,7 +184,7 @@ def get_avatar(user: "User") -> str:
|
||||||
"initials": avatar_mode_generated,
|
"initials": avatar_mode_generated,
|
||||||
"gravatar": avatar_mode_gravatar,
|
"gravatar": avatar_mode_gravatar,
|
||||||
}
|
}
|
||||||
modes: str = CONFIG.get("avatars", "none")
|
modes: str = get_current_tenant().avatars
|
||||||
for mode in modes.split(","):
|
for mode in modes.split(","):
|
||||||
avatar = None
|
avatar = None
|
||||||
if mode in mode_map:
|
if mode in mode_map:
|
||||||
|
|
|
@ -105,19 +105,12 @@ reputation:
|
||||||
cookie_domain: null
|
cookie_domain: null
|
||||||
disable_update_check: false
|
disable_update_check: false
|
||||||
disable_startup_analytics: false
|
disable_startup_analytics: false
|
||||||
avatars: env://AUTHENTIK_AUTHENTIK__AVATARS?gravatar,initials
|
|
||||||
geoip: "/geoip/GeoLite2-City.mmdb"
|
geoip: "/geoip/GeoLite2-City.mmdb"
|
||||||
|
|
||||||
footer_links: []
|
|
||||||
|
|
||||||
default_user_change_name: true
|
|
||||||
default_user_change_email: false
|
|
||||||
default_user_change_username: false
|
|
||||||
|
|
||||||
gdpr_compliance: true
|
|
||||||
cert_discovery_dir: /certs
|
cert_discovery_dir: /certs
|
||||||
default_token_length: 60
|
default_token_length: 60
|
||||||
impersonation: true
|
|
||||||
|
tenant_management_key: ""
|
||||||
|
|
||||||
blueprints_dir: /blueprints
|
blueprints_dir: /blueprints
|
||||||
|
|
||||||
|
|
|
@ -19,6 +19,7 @@ from structlog.stdlib import get_logger
|
||||||
|
|
||||||
from authentik import __version__, get_build_hash
|
from authentik import __version__, get_build_hash
|
||||||
from authentik.blueprints.models import ManagedModel
|
from authentik.blueprints.models import ManagedModel
|
||||||
|
from authentik.brands.models import Brand
|
||||||
from authentik.core.models import (
|
from authentik.core.models import (
|
||||||
USER_PATH_SYSTEM_PREFIX,
|
USER_PATH_SYSTEM_PREFIX,
|
||||||
Provider,
|
Provider,
|
||||||
|
@ -34,7 +35,6 @@ from authentik.lib.models import InheritanceForeignKey, SerializerModel
|
||||||
from authentik.lib.sentry import SentryIgnoredException
|
from authentik.lib.sentry import SentryIgnoredException
|
||||||
from authentik.lib.utils.errors import exception_to_string
|
from authentik.lib.utils.errors import exception_to_string
|
||||||
from authentik.outposts.controllers.k8s.utils import get_namespace
|
from authentik.outposts.controllers.k8s.utils import get_namespace
|
||||||
from authentik.tenants.models import Tenant
|
|
||||||
|
|
||||||
OUR_VERSION = parse(__version__)
|
OUR_VERSION = parse(__version__)
|
||||||
OUTPOST_HELLO_INTERVAL = 10
|
OUTPOST_HELLO_INTERVAL = 10
|
||||||
|
@ -407,9 +407,9 @@ class Outpost(SerializerModel, ManagedModel):
|
||||||
else:
|
else:
|
||||||
objects.append(provider)
|
objects.append(provider)
|
||||||
if self.managed:
|
if self.managed:
|
||||||
for tenant in Tenant.objects.filter(web_certificate__isnull=False):
|
for brand in Brand.objects.filter(web_certificate__isnull=False):
|
||||||
objects.append(tenant)
|
objects.append(brand)
|
||||||
objects.append(tenant.web_certificate)
|
objects.append(brand.web_certificate)
|
||||||
return objects
|
return objects
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
|
|
|
@ -5,12 +5,12 @@ from django.db.models.signals import m2m_changed, post_save, pre_delete, pre_sav
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from structlog.stdlib import get_logger
|
from structlog.stdlib import get_logger
|
||||||
|
|
||||||
|
from authentik.brands.models import Brand
|
||||||
from authentik.core.models import Provider
|
from authentik.core.models import Provider
|
||||||
from authentik.crypto.models import CertificateKeyPair
|
from authentik.crypto.models import CertificateKeyPair
|
||||||
from authentik.lib.utils.reflection import class_to_path
|
from authentik.lib.utils.reflection import class_to_path
|
||||||
from authentik.outposts.models import Outpost, OutpostServiceConnection
|
from authentik.outposts.models import Outpost, OutpostServiceConnection
|
||||||
from authentik.outposts.tasks import CACHE_KEY_OUTPOST_DOWN, outpost_controller, outpost_post_save
|
from authentik.outposts.tasks import CACHE_KEY_OUTPOST_DOWN, outpost_controller, outpost_post_save
|
||||||
from authentik.tenants.models import Tenant
|
|
||||||
|
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
UPDATE_TRIGGERING_MODELS = (
|
UPDATE_TRIGGERING_MODELS = (
|
||||||
|
@ -18,7 +18,7 @@ UPDATE_TRIGGERING_MODELS = (
|
||||||
OutpostServiceConnection,
|
OutpostServiceConnection,
|
||||||
Provider,
|
Provider,
|
||||||
CertificateKeyPair,
|
CertificateKeyPair,
|
||||||
Tenant,
|
Brand,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -161,7 +161,7 @@ class Migration(migrations.Migration):
|
||||||
("authentik.stages.user_login", "authentik Stages.User Login"),
|
("authentik.stages.user_login", "authentik Stages.User Login"),
|
||||||
("authentik.stages.user_logout", "authentik Stages.User Logout"),
|
("authentik.stages.user_logout", "authentik Stages.User Logout"),
|
||||||
("authentik.stages.user_write", "authentik Stages.User Write"),
|
("authentik.stages.user_write", "authentik Stages.User Write"),
|
||||||
("authentik.tenants", "authentik Tenants"),
|
("authentik.brands", "authentik Brands"),
|
||||||
("authentik.core", "authentik Core"),
|
("authentik.core", "authentik Core"),
|
||||||
("authentik.blueprints", "authentik Blueprints"),
|
("authentik.blueprints", "authentik Blueprints"),
|
||||||
],
|
],
|
||||||
|
|
|
@ -67,7 +67,7 @@ class Migration(migrations.Migration):
|
||||||
("authentik.stages.user_login", "authentik Stages.User Login"),
|
("authentik.stages.user_login", "authentik Stages.User Login"),
|
||||||
("authentik.stages.user_logout", "authentik Stages.User Logout"),
|
("authentik.stages.user_logout", "authentik Stages.User Logout"),
|
||||||
("authentik.stages.user_write", "authentik Stages.User Write"),
|
("authentik.stages.user_write", "authentik Stages.User Write"),
|
||||||
("authentik.tenants", "authentik Tenants"),
|
("authentik.brands", "authentik Brands"),
|
||||||
("authentik.blueprints", "authentik Blueprints"),
|
("authentik.blueprints", "authentik Blueprints"),
|
||||||
("authentik.core", "authentik Core"),
|
("authentik.core", "authentik Core"),
|
||||||
],
|
],
|
||||||
|
|
|
@ -143,7 +143,7 @@ class PasswordPolicy(Policy):
|
||||||
user_inputs.append(request.user.name)
|
user_inputs.append(request.user.name)
|
||||||
user_inputs.append(request.user.email)
|
user_inputs.append(request.user.email)
|
||||||
if request.http_request:
|
if request.http_request:
|
||||||
user_inputs.append(request.http_request.tenant.branding_title)
|
user_inputs.append(request.http_request.brand.branding_title)
|
||||||
# Only calculate result for the first 100 characters, as with over 100 char
|
# Only calculate result for the first 100 characters, as with over 100 char
|
||||||
# long passwords we can be reasonably sure that they'll surpass the score anyways
|
# long passwords we can be reasonably sure that they'll surpass the score anyways
|
||||||
# See https://github.com/dropbox/zxcvbn#runtime-latency
|
# See https://github.com/dropbox/zxcvbn#runtime-latency
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
{% block title %}
|
{% block title %}
|
||||||
{% trans 'Permission denied' %} - {{ tenant.branding_title }}
|
{% trans 'Permission denied' %} - {{ brand.branding_title }}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block card_title %}
|
{% block card_title %}
|
||||||
|
|
|
@ -4,7 +4,7 @@ from urllib.parse import urlencode
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
from authentik.core.models import Application
|
from authentik.core.models import Application
|
||||||
from authentik.core.tests.utils import create_test_admin_user, create_test_flow, create_test_tenant
|
from authentik.core.tests.utils import create_test_admin_user, create_test_brand, create_test_flow
|
||||||
from authentik.lib.generators import generate_id
|
from authentik.lib.generators import generate_id
|
||||||
from authentik.providers.oauth2.models import DeviceToken, OAuth2Provider
|
from authentik.providers.oauth2.models import DeviceToken, OAuth2Provider
|
||||||
from authentik.providers.oauth2.tests.utils import OAuthTestCase
|
from authentik.providers.oauth2.tests.utils import OAuthTestCase
|
||||||
|
@ -28,9 +28,9 @@ class TesOAuth2DeviceInit(OAuthTestCase):
|
||||||
self.user = create_test_admin_user()
|
self.user = create_test_admin_user()
|
||||||
self.client.force_login(self.user)
|
self.client.force_login(self.user)
|
||||||
self.device_flow = create_test_flow()
|
self.device_flow = create_test_flow()
|
||||||
self.tenant = create_test_tenant()
|
self.brand = create_test_brand()
|
||||||
self.tenant.flow_device_code = self.device_flow
|
self.brand.flow_device_code = self.device_flow
|
||||||
self.tenant.save()
|
self.brand.save()
|
||||||
|
|
||||||
def test_device_init(self):
|
def test_device_init(self):
|
||||||
"""Test device init"""
|
"""Test device init"""
|
||||||
|
@ -48,8 +48,8 @@ class TesOAuth2DeviceInit(OAuthTestCase):
|
||||||
|
|
||||||
def test_no_flow(self):
|
def test_no_flow(self):
|
||||||
"""Test no flow"""
|
"""Test no flow"""
|
||||||
self.tenant.flow_device_code = None
|
self.brand.flow_device_code = None
|
||||||
self.tenant.save()
|
self.brand.save()
|
||||||
res = self.client.get(reverse("authentik_providers_oauth2_root:device-login"))
|
res = self.client.get(reverse("authentik_providers_oauth2_root:device-login"))
|
||||||
self.assertEqual(res.status_code, 404)
|
self.assertEqual(res.status_code, 404)
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,7 @@ from rest_framework.exceptions import ErrorDetail
|
||||||
from rest_framework.fields import CharField, IntegerField
|
from rest_framework.fields import CharField, IntegerField
|
||||||
from structlog.stdlib import get_logger
|
from structlog.stdlib import get_logger
|
||||||
|
|
||||||
|
from authentik.brands.models import Brand
|
||||||
from authentik.core.models import Application
|
from authentik.core.models import Application
|
||||||
from authentik.flows.challenge import Challenge, ChallengeResponse, ChallengeTypes
|
from authentik.flows.challenge import Challenge, ChallengeResponse, ChallengeTypes
|
||||||
from authentik.flows.exceptions import FlowNonApplicableException
|
from authentik.flows.exceptions import FlowNonApplicableException
|
||||||
|
@ -26,7 +27,6 @@ from authentik.stages.consent.stage import (
|
||||||
PLAN_CONTEXT_CONSENT_HEADER,
|
PLAN_CONTEXT_CONSENT_HEADER,
|
||||||
PLAN_CONTEXT_CONSENT_PERMISSIONS,
|
PLAN_CONTEXT_CONSENT_PERMISSIONS,
|
||||||
)
|
)
|
||||||
from authentik.tenants.models import Tenant
|
|
||||||
|
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
QS_KEY_CODE = "code" # nosec
|
QS_KEY_CODE = "code" # nosec
|
||||||
|
@ -88,10 +88,10 @@ class DeviceEntryView(View):
|
||||||
"""View used to initiate the device-code flow, url entered by endusers"""
|
"""View used to initiate the device-code flow, url entered by endusers"""
|
||||||
|
|
||||||
def dispatch(self, request: HttpRequest) -> HttpResponse:
|
def dispatch(self, request: HttpRequest) -> HttpResponse:
|
||||||
tenant: Tenant = request.tenant
|
brand: Brand = request.brand
|
||||||
device_flow = tenant.flow_device_code
|
device_flow = brand.flow_device_code
|
||||||
if not device_flow:
|
if not device_flow:
|
||||||
LOGGER.info("Tenant has no device code flow configured", tenant=tenant)
|
LOGGER.info("Brand has no device code flow configured", brand=brand)
|
||||||
return HttpResponse(status=404)
|
return HttpResponse(status=404)
|
||||||
if QS_KEY_CODE in request.GET:
|
if QS_KEY_CODE in request.GET:
|
||||||
validation = validate_code(request.GET[QS_KEY_CODE], request)
|
validation = validate_code(request.GET[QS_KEY_CODE], request)
|
||||||
|
|
|
@ -97,7 +97,7 @@ class GitHubUserTeamsView(View):
|
||||||
"created_at": "",
|
"created_at": "",
|
||||||
"updated_at": "",
|
"updated_at": "",
|
||||||
"organization": {
|
"organization": {
|
||||||
"login": slugify(request.tenant.branding_title),
|
"login": slugify(request.brand.branding_title),
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"node_id": "",
|
"node_id": "",
|
||||||
"url": "",
|
"url": "",
|
||||||
|
@ -109,7 +109,7 @@ class GitHubUserTeamsView(View):
|
||||||
"public_members_url": "",
|
"public_members_url": "",
|
||||||
"avatar_url": "",
|
"avatar_url": "",
|
||||||
"description": "",
|
"description": "",
|
||||||
"name": request.tenant.branding_title,
|
"name": request.brand.branding_title,
|
||||||
"company": "",
|
"company": "",
|
||||||
"blog": "",
|
"blog": "",
|
||||||
"location": "",
|
"location": "",
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
"""authentik database backend"""
|
"""authentik database backend"""
|
||||||
from django_prometheus.db.backends.postgresql.base import DatabaseWrapper as BaseDatabaseWrapper
|
from django_tenants.postgresql_backend.base import DatabaseWrapper as BaseDatabaseWrapper
|
||||||
|
|
||||||
from authentik.lib.config import CONFIG
|
from authentik.lib.config import CONFIG
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
"""root settings for authentik"""
|
"""root settings for authentik"""
|
||||||
import importlib
|
import importlib
|
||||||
import os
|
import os
|
||||||
|
from collections import OrderedDict
|
||||||
from hashlib import sha512
|
from hashlib import sha512
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from urllib.parse import quote_plus
|
from urllib.parse import quote_plus
|
||||||
|
@ -49,14 +50,24 @@ AUTHENTICATION_BACKENDS = [
|
||||||
DEFAULT_AUTO_FIELD = "django.db.models.AutoField"
|
DEFAULT_AUTO_FIELD = "django.db.models.AutoField"
|
||||||
|
|
||||||
# Application definition
|
# Application definition
|
||||||
INSTALLED_APPS = [
|
SHARED_APPS = [
|
||||||
|
"django_tenants",
|
||||||
|
"authentik.tenants",
|
||||||
"daphne",
|
"daphne",
|
||||||
"django.contrib.auth",
|
|
||||||
"django.contrib.contenttypes",
|
"django.contrib.contenttypes",
|
||||||
"django.contrib.sessions",
|
|
||||||
"django.contrib.messages",
|
"django.contrib.messages",
|
||||||
"django.contrib.staticfiles",
|
"django.contrib.staticfiles",
|
||||||
"django.contrib.humanize",
|
"django.contrib.humanize",
|
||||||
|
"rest_framework",
|
||||||
|
"django_filters",
|
||||||
|
"drf_spectacular",
|
||||||
|
"django_prometheus",
|
||||||
|
"channels",
|
||||||
|
]
|
||||||
|
TENANT_APPS = [
|
||||||
|
"django.contrib.auth",
|
||||||
|
"django.contrib.sessions",
|
||||||
|
"authentik.tenants",
|
||||||
"authentik.admin",
|
"authentik.admin",
|
||||||
"authentik.api",
|
"authentik.api",
|
||||||
"authentik.crypto",
|
"authentik.crypto",
|
||||||
|
@ -102,16 +113,14 @@ 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",
|
"authentik.brands",
|
||||||
"authentik.blueprints",
|
"authentik.blueprints",
|
||||||
"rest_framework",
|
|
||||||
"django_filters",
|
|
||||||
"drf_spectacular",
|
|
||||||
"guardian",
|
"guardian",
|
||||||
"django_prometheus",
|
|
||||||
"channels",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
TENANT_MODEL = "authentik_tenants.Tenant"
|
||||||
|
TENANT_DOMAIN_MODEL = "authentik_tenants.Domain"
|
||||||
|
|
||||||
GUARDIAN_MONKEY_PATCH = False
|
GUARDIAN_MONKEY_PATCH = False
|
||||||
|
|
||||||
SPECTACULAR_SETTINGS = {
|
SPECTACULAR_SETTINGS = {
|
||||||
|
@ -214,12 +223,14 @@ SESSION_EXPIRE_AT_BROWSER_CLOSE = True
|
||||||
MESSAGE_STORAGE = "authentik.root.messages.storage.ChannelsStorage"
|
MESSAGE_STORAGE = "authentik.root.messages.storage.ChannelsStorage"
|
||||||
|
|
||||||
MIDDLEWARE = [
|
MIDDLEWARE = [
|
||||||
|
"django_tenants.middleware.default.DefaultTenantMiddleware",
|
||||||
"authentik.root.middleware.LoggingMiddleware",
|
"authentik.root.middleware.LoggingMiddleware",
|
||||||
"django_prometheus.middleware.PrometheusBeforeMiddleware",
|
"django_prometheus.middleware.PrometheusBeforeMiddleware",
|
||||||
|
"authentik.brands.middleware.TenantMiddleware",
|
||||||
"authentik.root.middleware.SessionMiddleware",
|
"authentik.root.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.brands.middleware.BrandMiddleware",
|
||||||
"authentik.events.middleware.AuditMiddleware",
|
"authentik.events.middleware.AuditMiddleware",
|
||||||
"django.middleware.security.SecurityMiddleware",
|
"django.middleware.security.SecurityMiddleware",
|
||||||
"django.middleware.common.CommonMiddleware",
|
"django.middleware.common.CommonMiddleware",
|
||||||
|
@ -243,7 +254,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.tenants.utils.context_processor",
|
"authentik.brands.utils.context_processor",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -265,6 +276,7 @@ CHANNEL_LAYERS = {
|
||||||
# Database
|
# Database
|
||||||
# https://docs.djangoproject.com/en/2.1/ref/settings/#databases
|
# https://docs.djangoproject.com/en/2.1/ref/settings/#databases
|
||||||
|
|
||||||
|
ORIGINAL_BACKEND = "django_prometheus.db.backends.postgresql"
|
||||||
DATABASES = {
|
DATABASES = {
|
||||||
"default": {
|
"default": {
|
||||||
"ENGINE": "authentik.root.db",
|
"ENGINE": "authentik.root.db",
|
||||||
|
@ -289,6 +301,8 @@ if CONFIG.get_bool("postgresql.use_pgbouncer", False):
|
||||||
# https://docs.djangoproject.com/en/4.0/ref/databases/#persistent-connections
|
# https://docs.djangoproject.com/en/4.0/ref/databases/#persistent-connections
|
||||||
DATABASES["default"]["CONN_MAX_AGE"] = None # persistent
|
DATABASES["default"]["CONN_MAX_AGE"] = None # persistent
|
||||||
|
|
||||||
|
DATABASE_ROUTERS = ("django_tenants.routers.TenantSyncRouter",)
|
||||||
|
|
||||||
# Email
|
# Email
|
||||||
# These values should never actually be used, emails are only sent from email stages, which
|
# These values should never actually be used, emails are only sent from email stages, which
|
||||||
# loads the config directly from CONFIG
|
# loads the config directly from CONFIG
|
||||||
|
@ -378,6 +392,8 @@ LOGGING = get_logger_config()
|
||||||
|
|
||||||
|
|
||||||
_DISALLOWED_ITEMS = [
|
_DISALLOWED_ITEMS = [
|
||||||
|
"SHARED_APPS",
|
||||||
|
"TENANT_APPS",
|
||||||
"INSTALLED_APPS",
|
"INSTALLED_APPS",
|
||||||
"MIDDLEWARE",
|
"MIDDLEWARE",
|
||||||
"AUTHENTICATION_BACKENDS",
|
"AUTHENTICATION_BACKENDS",
|
||||||
|
@ -389,7 +405,8 @@ def _update_settings(app_path: str):
|
||||||
try:
|
try:
|
||||||
settings_module = importlib.import_module(app_path)
|
settings_module = importlib.import_module(app_path)
|
||||||
CONFIG.log("debug", "Loaded app settings", path=app_path)
|
CONFIG.log("debug", "Loaded app settings", path=app_path)
|
||||||
INSTALLED_APPS.extend(getattr(settings_module, "INSTALLED_APPS", []))
|
SHARED_APPS.extend(getattr(settings_module, "SHARED_APPS", []))
|
||||||
|
TENANT_APPS.extend(getattr(settings_module, "TENANT_APPS", []))
|
||||||
MIDDLEWARE.extend(getattr(settings_module, "MIDDLEWARE", []))
|
MIDDLEWARE.extend(getattr(settings_module, "MIDDLEWARE", []))
|
||||||
AUTHENTICATION_BACKENDS.extend(getattr(settings_module, "AUTHENTICATION_BACKENDS", []))
|
AUTHENTICATION_BACKENDS.extend(getattr(settings_module, "AUTHENTICATION_BACKENDS", []))
|
||||||
CELERY["beat_schedule"].update(getattr(settings_module, "CELERY_BEAT_SCHEDULE", {}))
|
CELERY["beat_schedule"].update(getattr(settings_module, "CELERY_BEAT_SCHEDULE", {}))
|
||||||
|
@ -401,7 +418,7 @@ def _update_settings(app_path: str):
|
||||||
|
|
||||||
|
|
||||||
# Load subapps's settings
|
# Load subapps's settings
|
||||||
for _app in INSTALLED_APPS:
|
for _app in set(SHARED_APPS + TENANT_APPS):
|
||||||
if not _app.startswith("authentik"):
|
if not _app.startswith("authentik"):
|
||||||
continue
|
continue
|
||||||
_update_settings(f"{_app}.settings")
|
_update_settings(f"{_app}.settings")
|
||||||
|
@ -410,14 +427,14 @@ _update_settings("data.user_settings")
|
||||||
if DEBUG:
|
if DEBUG:
|
||||||
CELERY["task_always_eager"] = True
|
CELERY["task_always_eager"] = True
|
||||||
os.environ[ENV_GIT_HASH_KEY] = "dev"
|
os.environ[ENV_GIT_HASH_KEY] = "dev"
|
||||||
INSTALLED_APPS.append("silk")
|
SHARED_APPS.append("silk")
|
||||||
SILKY_PYTHON_PROFILER = True
|
SILKY_PYTHON_PROFILER = True
|
||||||
MIDDLEWARE = ["silk.middleware.SilkyMiddleware"] + MIDDLEWARE
|
MIDDLEWARE = ["silk.middleware.SilkyMiddleware"] + MIDDLEWARE
|
||||||
REST_FRAMEWORK["DEFAULT_RENDERER_CLASSES"].append(
|
REST_FRAMEWORK["DEFAULT_RENDERER_CLASSES"].append(
|
||||||
"rest_framework.renderers.BrowsableAPIRenderer"
|
"rest_framework.renderers.BrowsableAPIRenderer"
|
||||||
)
|
)
|
||||||
|
|
||||||
INSTALLED_APPS.append("authentik.core")
|
TENANT_APPS.append("authentik.core")
|
||||||
|
|
||||||
CONFIG.log("info", "Booting authentik", version=__version__)
|
CONFIG.log("info", "Booting authentik", version=__version__)
|
||||||
|
|
||||||
|
@ -425,7 +442,10 @@ CONFIG.log("info", "Booting authentik", version=__version__)
|
||||||
try:
|
try:
|
||||||
importlib.import_module("authentik.enterprise.apps")
|
importlib.import_module("authentik.enterprise.apps")
|
||||||
CONFIG.log("info", "Enabled authentik enterprise")
|
CONFIG.log("info", "Enabled authentik enterprise")
|
||||||
INSTALLED_APPS.append("authentik.enterprise")
|
TENANT_APPS.append("authentik.enterprise")
|
||||||
_update_settings("authentik.enterprise.settings")
|
_update_settings("authentik.enterprise.settings")
|
||||||
except ImportError:
|
except ImportError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
SHARED_APPS = list(OrderedDict.fromkeys(SHARED_APPS + TENANT_APPS))
|
||||||
|
INSTALLED_APPS = list(OrderedDict.fromkeys(SHARED_APPS + TENANT_APPS))
|
||||||
|
|
|
@ -56,7 +56,7 @@ class AuthenticatorTOTPStageView(ChallengeStageView):
|
||||||
data={
|
data={
|
||||||
"type": ChallengeTypes.NATIVE.value,
|
"type": ChallengeTypes.NATIVE.value,
|
||||||
"config_url": device.config_url.replace(
|
"config_url": device.config_url.replace(
|
||||||
OTP_TOTP_ISSUER, quote(self.request.tenant.branding_title)
|
OTP_TOTP_ISSUER, quote(self.request.brand.branding_title)
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
|
@ -201,7 +201,7 @@ def validate_challenge_duo(device_pk: int, stage_view: StageView, user: User) ->
|
||||||
type=__(
|
type=__(
|
||||||
"%(brand_name)s Login request"
|
"%(brand_name)s Login request"
|
||||||
% {
|
% {
|
||||||
"brand_name": stage_view.request.tenant.branding_title,
|
"brand_name": stage_view.request.brand.branding_title,
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
display_username=user.username,
|
display_username=user.username,
|
||||||
|
|
|
@ -6,6 +6,7 @@ from django.test.client import RequestFactory
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from rest_framework.exceptions import ValidationError
|
from rest_framework.exceptions import ValidationError
|
||||||
|
|
||||||
|
from authentik.brands.utils import get_brand_for_request
|
||||||
from authentik.core.tests.utils import create_test_admin_user, create_test_flow
|
from authentik.core.tests.utils import create_test_admin_user, create_test_flow
|
||||||
from authentik.events.models import Event, EventAction
|
from authentik.events.models import Event, EventAction
|
||||||
from authentik.flows.models import FlowDesignation, FlowStageBinding
|
from authentik.flows.models import FlowDesignation, FlowStageBinding
|
||||||
|
@ -19,7 +20,6 @@ from authentik.stages.authenticator_duo.models import AuthenticatorDuoStage, Duo
|
||||||
from authentik.stages.authenticator_validate.challenge import validate_challenge_duo
|
from authentik.stages.authenticator_validate.challenge import validate_challenge_duo
|
||||||
from authentik.stages.authenticator_validate.models import AuthenticatorValidateStage, DeviceClasses
|
from authentik.stages.authenticator_validate.models import AuthenticatorValidateStage, DeviceClasses
|
||||||
from authentik.stages.user_login.models import UserLoginStage
|
from authentik.stages.user_login.models import UserLoginStage
|
||||||
from authentik.tenants.utils import get_tenant_for_request
|
|
||||||
|
|
||||||
|
|
||||||
class AuthenticatorValidateStageDuoTests(FlowTestCase):
|
class AuthenticatorValidateStageDuoTests(FlowTestCase):
|
||||||
|
@ -36,7 +36,7 @@ class AuthenticatorValidateStageDuoTests(FlowTestCase):
|
||||||
middleware = SessionMiddleware(dummy_get_response)
|
middleware = SessionMiddleware(dummy_get_response)
|
||||||
middleware.process_request(request)
|
middleware.process_request(request)
|
||||||
request.session.save()
|
request.session.save()
|
||||||
setattr(request, "tenant", get_tenant_for_request(request))
|
setattr(request, "brand", get_brand_for_request(request))
|
||||||
|
|
||||||
stage = AuthenticatorDuoStage.objects.create(
|
stage = AuthenticatorDuoStage.objects.create(
|
||||||
name=generate_id(),
|
name=generate_id(),
|
||||||
|
|
|
@ -89,7 +89,7 @@ class AuthenticatorWebAuthnStageView(ChallengeStageView):
|
||||||
|
|
||||||
registration_options: PublicKeyCredentialCreationOptions = generate_registration_options(
|
registration_options: PublicKeyCredentialCreationOptions = generate_registration_options(
|
||||||
rp_id=get_rp_id(self.request),
|
rp_id=get_rp_id(self.request),
|
||||||
rp_name=self.request.tenant.branding_title,
|
rp_name=self.request.brand.branding_title,
|
||||||
user_id=user.uid,
|
user_id=user.uid,
|
||||||
user_name=user.username,
|
user_name=user.username,
|
||||||
user_display_name=user.name,
|
user_display_name=user.name,
|
||||||
|
|
|
@ -1,137 +1,114 @@
|
||||||
"""Serializer for tenant models"""
|
"""Serializer for tenants models"""
|
||||||
from typing import Any
|
from hmac import compare_digest
|
||||||
|
|
||||||
from django.db import models
|
from django_tenants.utils import get_tenant
|
||||||
from drf_spectacular.utils import extend_schema
|
from rest_framework import permissions
|
||||||
from rest_framework.decorators import action
|
from rest_framework.authentication import get_authorization_header
|
||||||
from rest_framework.exceptions import ValidationError
|
from rest_framework.fields import ReadOnlyField
|
||||||
from rest_framework.fields import CharField, ChoiceField, ListField
|
|
||||||
from rest_framework.filters import OrderingFilter, SearchFilter
|
from rest_framework.filters import OrderingFilter, SearchFilter
|
||||||
from rest_framework.permissions import AllowAny
|
from rest_framework.generics import RetrieveUpdateAPIView
|
||||||
|
from rest_framework.permissions import IsAdminUser
|
||||||
from rest_framework.request import Request
|
from rest_framework.request import Request
|
||||||
from rest_framework.response import Response
|
|
||||||
from rest_framework.serializers import ModelSerializer
|
from rest_framework.serializers import ModelSerializer
|
||||||
|
from rest_framework.views import View
|
||||||
from rest_framework.viewsets import ModelViewSet
|
from rest_framework.viewsets import ModelViewSet
|
||||||
|
|
||||||
from authentik.api.authorization import SecretKeyFilter
|
from authentik.api.authentication import validate_auth
|
||||||
from authentik.core.api.used_by import UsedByMixin
|
|
||||||
from authentik.core.api.utils import PassiveSerializer
|
|
||||||
from authentik.lib.config import CONFIG
|
from authentik.lib.config import CONFIG
|
||||||
from authentik.tenants.models import Tenant
|
from authentik.tenants.models import Domain, Tenant
|
||||||
|
|
||||||
|
|
||||||
class FooterLinkSerializer(PassiveSerializer):
|
class TenantManagementKeyPermission(permissions.BasePermission):
|
||||||
"""Links returned in Config API"""
|
def has_permission(self, request: Request, view: View) -> bool:
|
||||||
|
token = validate_auth(get_authorization_header(request))
|
||||||
href = CharField(read_only=True)
|
tenant_management_key = CONFIG.get("tenant_management_key")
|
||||||
name = CharField(read_only=True)
|
if compare_digest("", tenant_management_key):
|
||||||
|
return False
|
||||||
|
return compare_digest(token, tenant_management_key)
|
||||||
|
|
||||||
|
|
||||||
class TenantSerializer(ModelSerializer):
|
class TenantSerializer(ModelSerializer):
|
||||||
"""Tenant Serializer"""
|
"""Tenant Serializer"""
|
||||||
|
|
||||||
def validate(self, attrs: dict[str, Any]) -> dict[str, Any]:
|
|
||||||
if attrs.get("default", False):
|
|
||||||
tenants = Tenant.objects.filter(default=True)
|
|
||||||
if self.instance:
|
|
||||||
tenants = tenants.exclude(pk=self.instance.pk)
|
|
||||||
if tenants.exists():
|
|
||||||
raise ValidationError({"default": "Only a single Tenant can be set as default."})
|
|
||||||
return super().validate(attrs)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Tenant
|
model = Tenant
|
||||||
fields = [
|
fields = [
|
||||||
"tenant_uuid",
|
"tenant_uuid",
|
||||||
"domain",
|
"schema_name",
|
||||||
"default",
|
"name",
|
||||||
"branding_title",
|
|
||||||
"branding_logo",
|
|
||||||
"branding_favicon",
|
|
||||||
"flow_authentication",
|
|
||||||
"flow_invalidation",
|
|
||||||
"flow_recovery",
|
|
||||||
"flow_unenrollment",
|
|
||||||
"flow_user_settings",
|
|
||||||
"flow_device_code",
|
|
||||||
"event_retention",
|
|
||||||
"web_certificate",
|
|
||||||
"attributes",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class Themes(models.TextChoices):
|
class TenantViewSet(ModelViewSet):
|
||||||
"""Themes"""
|
|
||||||
|
|
||||||
AUTOMATIC = "automatic"
|
|
||||||
LIGHT = "light"
|
|
||||||
DARK = "dark"
|
|
||||||
|
|
||||||
|
|
||||||
class CurrentTenantSerializer(PassiveSerializer):
|
|
||||||
"""Partial tenant information for styling"""
|
|
||||||
|
|
||||||
matched_domain = CharField(source="domain")
|
|
||||||
branding_title = CharField()
|
|
||||||
branding_logo = CharField()
|
|
||||||
branding_favicon = CharField()
|
|
||||||
ui_footer_links = ListField(
|
|
||||||
child=FooterLinkSerializer(),
|
|
||||||
read_only=True,
|
|
||||||
default=CONFIG.get("footer_links", []),
|
|
||||||
)
|
|
||||||
ui_theme = ChoiceField(
|
|
||||||
choices=Themes.choices,
|
|
||||||
source="attributes.settings.theme.base",
|
|
||||||
default=Themes.AUTOMATIC,
|
|
||||||
read_only=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
flow_authentication = CharField(source="flow_authentication.slug", required=False)
|
|
||||||
flow_invalidation = CharField(source="flow_invalidation.slug", required=False)
|
|
||||||
flow_recovery = CharField(source="flow_recovery.slug", required=False)
|
|
||||||
flow_unenrollment = CharField(source="flow_unenrollment.slug", required=False)
|
|
||||||
flow_user_settings = CharField(source="flow_user_settings.slug", required=False)
|
|
||||||
flow_device_code = CharField(source="flow_device_code.slug", required=False)
|
|
||||||
|
|
||||||
default_locale = CharField(read_only=True)
|
|
||||||
|
|
||||||
|
|
||||||
class TenantViewSet(UsedByMixin, ModelViewSet):
|
|
||||||
"""Tenant Viewset"""
|
"""Tenant Viewset"""
|
||||||
|
|
||||||
queryset = Tenant.objects.all()
|
queryset = Tenant.objects.all()
|
||||||
serializer_class = TenantSerializer
|
serializer_class = TenantSerializer
|
||||||
search_fields = [
|
search_fields = [
|
||||||
"domain",
|
"name",
|
||||||
"branding_title",
|
"schema_name",
|
||||||
"web_certificate__name",
|
"domains__domain",
|
||||||
]
|
]
|
||||||
filterset_fields = [
|
ordering = ["schema_name"]
|
||||||
"tenant_uuid",
|
permission_classes = [TenantManagementKeyPermission]
|
||||||
|
filter_backends = [OrderingFilter, SearchFilter]
|
||||||
|
|
||||||
|
|
||||||
|
class DomainSerializer(ModelSerializer):
|
||||||
|
"""Domain Serializer"""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Domain
|
||||||
|
fields = "__all__"
|
||||||
|
|
||||||
|
|
||||||
|
class DomainViewSet(ModelViewSet):
|
||||||
|
"""Domain ViewSet"""
|
||||||
|
|
||||||
|
queryset = Domain.objects.all()
|
||||||
|
serializer_class = DomainSerializer
|
||||||
|
search_fields = [
|
||||||
"domain",
|
"domain",
|
||||||
"default",
|
"tenant__name",
|
||||||
"branding_title",
|
"tenant__schema_name",
|
||||||
"branding_logo",
|
|
||||||
"branding_favicon",
|
|
||||||
"flow_authentication",
|
|
||||||
"flow_invalidation",
|
|
||||||
"flow_recovery",
|
|
||||||
"flow_unenrollment",
|
|
||||||
"flow_user_settings",
|
|
||||||
"flow_device_code",
|
|
||||||
"event_retention",
|
|
||||||
"web_certificate",
|
|
||||||
]
|
]
|
||||||
ordering = ["domain"]
|
ordering = ["domain"]
|
||||||
|
permission_classes = [TenantManagementKeyPermission]
|
||||||
|
filter_backends = [OrderingFilter, SearchFilter]
|
||||||
|
|
||||||
filter_backends = [SecretKeyFilter, OrderingFilter, SearchFilter]
|
|
||||||
|
|
||||||
@extend_schema(
|
class SettingsSerializer(ModelSerializer):
|
||||||
responses=CurrentTenantSerializer(many=False),
|
"""Settings Serializer"""
|
||||||
)
|
|
||||||
@action(methods=["GET"], detail=False, permission_classes=[AllowAny])
|
name = ReadOnlyField()
|
||||||
def current(self, request: Request) -> Response:
|
domains = DomainSerializer(read_only=True, many=True)
|
||||||
"""Get current tenant"""
|
|
||||||
tenant: Tenant = request._request.tenant
|
class Meta:
|
||||||
return Response(CurrentTenantSerializer(tenant).data)
|
model = Tenant
|
||||||
|
fields = [
|
||||||
|
"tenant_uuid",
|
||||||
|
"name",
|
||||||
|
"domains",
|
||||||
|
"avatars",
|
||||||
|
"default_user_change_name",
|
||||||
|
"default_user_change_email",
|
||||||
|
"default_user_change_username",
|
||||||
|
"gdpr_compliance",
|
||||||
|
"impersonation",
|
||||||
|
"footer_links",
|
||||||
|
"reputation_expiry",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class SettingsView(RetrieveUpdateAPIView):
|
||||||
|
"""Settings view"""
|
||||||
|
|
||||||
|
queryset = Tenant.objects.all()
|
||||||
|
serializer_class = SettingsSerializer
|
||||||
|
permission_classes = [IsAdminUser]
|
||||||
|
filter_backends = []
|
||||||
|
|
||||||
|
def get_object(self):
|
||||||
|
obj = get_tenant(self.request)
|
||||||
|
self.check_object_permissions(obj)
|
||||||
|
return obj
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
"""authentik tenant app"""
|
"""authentik tenants app"""
|
||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
class AuthentikTenantsConfig(AppConfig):
|
class AuthentikTenantsConfig(AppConfig):
|
||||||
"""authentik Tenant app"""
|
"""authentik tenants app"""
|
||||||
|
|
||||||
name = "authentik.tenants"
|
name = "authentik.tenants"
|
||||||
label = "authentik_tenants"
|
label = "authentik_tenants"
|
||||||
|
|
|
@ -1,15 +1,13 @@
|
||||||
"""Inject tenant into current request"""
|
"""Inject tenant into current request"""
|
||||||
from typing import Callable
|
from typing import Callable
|
||||||
|
|
||||||
from django.http.request import HttpRequest
|
from django.http import HttpRequest, HttpResponse
|
||||||
from django.http.response import HttpResponse
|
|
||||||
from django.utils.translation import activate
|
|
||||||
from sentry_sdk.api import set_tag
|
from sentry_sdk.api import set_tag
|
||||||
|
|
||||||
from authentik.tenants.utils import get_tenant_for_request
|
from authentik.tenants.utils import get_tenant_for_request
|
||||||
|
|
||||||
|
|
||||||
class TenantMiddleware:
|
class CurrentTenantMiddleware:
|
||||||
"""Add current tenant to http request"""
|
"""Add current tenant to http request"""
|
||||||
|
|
||||||
get_response: Callable[[HttpRequest], HttpResponse]
|
get_response: Callable[[HttpRequest], HttpResponse]
|
||||||
|
@ -22,8 +20,5 @@ class TenantMiddleware:
|
||||||
tenant = get_tenant_for_request(request)
|
tenant = get_tenant_for_request(request)
|
||||||
setattr(request, "tenant", tenant)
|
setattr(request, "tenant", tenant)
|
||||||
set_tag("authentik.tenant_uuid", tenant.tenant_uuid.hex)
|
set_tag("authentik.tenant_uuid", tenant.tenant_uuid.hex)
|
||||||
set_tag("authentik.tenant_domain", tenant.domain)
|
set_tag("authentik.tenant_domain_regex", tenant.domain_regex)
|
||||||
locale = tenant.default_locale
|
|
||||||
if locale != "":
|
|
||||||
activate(locale)
|
|
||||||
return self.get_response(request)
|
return self.get_response(request)
|
||||||
|
|
|
@ -0,0 +1,144 @@
|
||||||
|
# Generated by Django 4.2.7 on 2023-11-15 10:53
|
||||||
|
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
import django_tenants.postgresql_backend.base
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
from authentik.lib.config import CONFIG
|
||||||
|
|
||||||
|
|
||||||
|
def create_default_tenant(apps, schema_editor):
|
||||||
|
db_alias = schema_editor.connection.alias
|
||||||
|
|
||||||
|
Tenant = apps.get_model("authentik_tenants", "Tenant")
|
||||||
|
tenant = Tenant.objects.using(db_alias).create(
|
||||||
|
schema_name="public",
|
||||||
|
name="Default",
|
||||||
|
avatars=CONFIG.get("avatars", "gravatar,initials"),
|
||||||
|
default_user_change_name=CONFIG.get_bool("default_user_change_name", True),
|
||||||
|
default_user_change_email=CONFIG.get_bool("default_user_change_email", False),
|
||||||
|
default_user_change_username=CONFIG.get_bool("default_user_change_username", False),
|
||||||
|
gdpr_compliance=CONFIG.get_bool("gdpr_compliance", True),
|
||||||
|
impersonation=CONFIG.get_bool("impersonation", True),
|
||||||
|
footer_links=CONFIG.get("footer_links", default=[]),
|
||||||
|
)
|
||||||
|
|
||||||
|
Domain = apps.get_model("authentik_tenants", "Domain")
|
||||||
|
domain = Domain.objects.using(db_alias).create(domain="*", tenant=tenant, is_primary=True)
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = []
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="Tenant",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"schema_name",
|
||||||
|
models.CharField(
|
||||||
|
db_index=True,
|
||||||
|
max_length=63,
|
||||||
|
unique=True,
|
||||||
|
validators=[django_tenants.postgresql_backend.base._check_schema_name],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"tenant_uuid",
|
||||||
|
models.UUIDField(
|
||||||
|
default=uuid.uuid4, editable=False, primary_key=True, serialize=False
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("name", models.TextField()),
|
||||||
|
(
|
||||||
|
"avatars",
|
||||||
|
models.TextField(
|
||||||
|
default="gravatar,initials",
|
||||||
|
help_text="Configure how authentik should show avatars for users.",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"default_user_change_name",
|
||||||
|
models.BooleanField(
|
||||||
|
default=True, help_text="Enable the ability for users to change their name."
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"default_user_change_email",
|
||||||
|
models.BooleanField(
|
||||||
|
default=False,
|
||||||
|
help_text="Enable the ability for users to change their email address.",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"default_user_change_username",
|
||||||
|
models.BooleanField(
|
||||||
|
default=False,
|
||||||
|
help_text="Enable the ability for users to change their username.",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"gdpr_compliance",
|
||||||
|
models.BooleanField(
|
||||||
|
default=True,
|
||||||
|
help_text="When enabled, all the events caused by a user will be deleted upon the user's deletion.",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"impersonation",
|
||||||
|
models.BooleanField(
|
||||||
|
default=True, help_text="Globally enable/disable impersonation."
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"footer_links",
|
||||||
|
models.JSONField(
|
||||||
|
blank=True,
|
||||||
|
default=list,
|
||||||
|
help_text="The option configures the footer links on the flow executor pages.",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"reputation_expiry",
|
||||||
|
models.PositiveBigIntegerField(
|
||||||
|
default=86400,
|
||||||
|
help_text="Configure how long reputation scores should be saved for in seconds.",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"verbose_name": "Tenant",
|
||||||
|
"verbose_name_plural": "Tenants",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="Domain",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.AutoField(
|
||||||
|
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("domain", models.CharField(db_index=True, max_length=253, unique=True)),
|
||||||
|
("is_primary", models.BooleanField(db_index=True, default=True)),
|
||||||
|
(
|
||||||
|
"tenant",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="domains",
|
||||||
|
to="authentik_tenants.tenant",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"verbose_name": "Domain",
|
||||||
|
"verbose_name_plural": "Domains",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.RunPython(code=create_default_tenant, reverse_code=migrations.RunPython.noop),
|
||||||
|
]
|
|
@ -1,72 +1,60 @@
|
||||||
"""tenant models"""
|
"""Tenant models"""
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
from django.db.models.deletion import ProtectedError
|
||||||
|
from django.db.models.signals import pre_delete
|
||||||
|
from django.dispatch import receiver
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from django_tenants.models import DomainMixin, TenantMixin
|
||||||
from rest_framework.serializers import Serializer
|
from rest_framework.serializers import Serializer
|
||||||
from structlog.stdlib import get_logger
|
from structlog.stdlib import get_logger
|
||||||
|
|
||||||
from authentik.crypto.models import CertificateKeyPair
|
|
||||||
from authentik.flows.models import Flow
|
|
||||||
from authentik.lib.models import SerializerModel
|
from authentik.lib.models import SerializerModel
|
||||||
from authentik.lib.utils.time import timedelta_string_validator
|
|
||||||
|
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
|
|
||||||
|
|
||||||
class Tenant(SerializerModel):
|
class Tenant(TenantMixin, SerializerModel):
|
||||||
"""Single tenant"""
|
"""Tenant"""
|
||||||
|
|
||||||
tenant_uuid = models.UUIDField(primary_key=True, editable=False, default=uuid4)
|
tenant_uuid = models.UUIDField(primary_key=True, editable=False, default=uuid4)
|
||||||
domain = models.TextField(
|
name = models.TextField()
|
||||||
|
|
||||||
|
auto_create_schema = True
|
||||||
|
auto_drop_schema = True
|
||||||
|
|
||||||
|
avatars = models.TextField(
|
||||||
|
help_text=_("Configure how authentik should show avatars for users."),
|
||||||
|
default="gravatar,initials",
|
||||||
|
)
|
||||||
|
default_user_change_name = models.BooleanField(
|
||||||
|
help_text=_("Enable the ability for users to change their name."), default=True
|
||||||
|
)
|
||||||
|
default_user_change_email = models.BooleanField(
|
||||||
|
help_text=_("Enable the ability for users to change their email address."), default=False
|
||||||
|
)
|
||||||
|
default_user_change_username = models.BooleanField(
|
||||||
|
help_text=_("Enable the ability for users to change their username."), default=False
|
||||||
|
)
|
||||||
|
gdpr_compliance = models.BooleanField(
|
||||||
help_text=_(
|
help_text=_(
|
||||||
"Domain that activates this tenant. Can be a superset, i.e. `a.b` for `aa.b` and `ba.b`"
|
"When enabled, all the events caused by a user will be deleted upon the user's deletion."
|
||||||
)
|
|
||||||
)
|
|
||||||
default = models.BooleanField(
|
|
||||||
default=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
branding_title = models.TextField(default="authentik")
|
|
||||||
|
|
||||||
branding_logo = models.TextField(default="/static/dist/assets/icons/icon_left_brand.svg")
|
|
||||||
branding_favicon = models.TextField(default="/static/dist/assets/icons/icon.png")
|
|
||||||
|
|
||||||
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"
|
|
||||||
)
|
|
||||||
flow_user_settings = models.ForeignKey(
|
|
||||||
Flow, null=True, on_delete=models.SET_NULL, related_name="tenant_user_settings"
|
|
||||||
)
|
|
||||||
flow_device_code = models.ForeignKey(
|
|
||||||
Flow, null=True, on_delete=models.SET_NULL, related_name="tenant_device_code"
|
|
||||||
)
|
|
||||||
|
|
||||||
event_retention = models.TextField(
|
|
||||||
default="days=365",
|
|
||||||
validators=[timedelta_string_validator],
|
|
||||||
help_text=_(
|
|
||||||
"Events will be deleted after this duration.(Format: weeks=3;days=2;hours=3,seconds=2)."
|
|
||||||
),
|
),
|
||||||
|
default=True,
|
||||||
)
|
)
|
||||||
|
impersonation = models.BooleanField(
|
||||||
web_certificate = models.ForeignKey(
|
help_text=_("Globally enable/disable impersonation."), default=True
|
||||||
CertificateKeyPair,
|
)
|
||||||
null=True,
|
footer_links = models.JSONField(
|
||||||
default=None,
|
help_text=_("The option configures the footer links on the flow executor pages."),
|
||||||
on_delete=models.SET_DEFAULT,
|
default=list,
|
||||||
help_text=_("Web Certificate used by the authentik Core webserver."),
|
blank=True,
|
||||||
|
)
|
||||||
|
reputation_expiry = models.PositiveBigIntegerField(
|
||||||
|
help_text=_("Configure how long reputation scores should be saved for in seconds."),
|
||||||
|
default=86400,
|
||||||
)
|
)
|
||||||
attributes = models.JSONField(default=dict, blank=True)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def serializer(self) -> Serializer:
|
def serializer(self) -> Serializer:
|
||||||
|
@ -74,21 +62,24 @@ class Tenant(SerializerModel):
|
||||||
|
|
||||||
return TenantSerializer
|
return TenantSerializer
|
||||||
|
|
||||||
@property
|
|
||||||
def default_locale(self) -> str:
|
|
||||||
"""Get default locale"""
|
|
||||||
try:
|
|
||||||
return self.attributes.get("settings", {}).get("locale", "")
|
|
||||||
# pylint: disable=broad-except
|
|
||||||
except Exception as exc:
|
|
||||||
LOGGER.warning("Failed to get default locale", exc=exc)
|
|
||||||
return ""
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
if self.default:
|
return f"Tenant {self.domain_regex}"
|
||||||
return "Default tenant"
|
|
||||||
return f"Tenant {self.domain}"
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _("Tenant")
|
verbose_name = _("Tenant")
|
||||||
verbose_name_plural = _("Tenants")
|
verbose_name_plural = _("Tenants")
|
||||||
|
|
||||||
|
|
||||||
|
class Domain(DomainMixin, SerializerModel):
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return f"Domain {self.domain}"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def serializer(self) -> Serializer:
|
||||||
|
from authentik.tenants.api import DomainSerializer
|
||||||
|
|
||||||
|
return DomainSerializer
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _("Domain")
|
||||||
|
verbose_name_plural = _("Domains")
|
||||||
|
|
|
@ -1,6 +1,12 @@
|
||||||
"""API URLs"""
|
"""API URLs"""
|
||||||
from authentik.tenants.api import TenantViewSet
|
from django.urls import path
|
||||||
|
|
||||||
|
from authentik.tenants.api import SettingsView, TenantViewSet
|
||||||
|
|
||||||
api_urlpatterns = [
|
api_urlpatterns = [
|
||||||
("core/tenants", TenantViewSet),
|
path("admin/settings/", SettingsView.as_view(), name="tenant_settings"),
|
||||||
|
(
|
||||||
|
"tenants",
|
||||||
|
TenantViewSet,
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -1,42 +1,8 @@
|
||||||
"""Tenant utilities"""
|
from django.db import connection
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from django.db.models import F, Q
|
|
||||||
from django.db.models import Value as V
|
|
||||||
from django.http.request import HttpRequest
|
|
||||||
from sentry_sdk.hub import Hub
|
|
||||||
|
|
||||||
from authentik import get_full_version
|
|
||||||
from authentik.lib.config import CONFIG
|
|
||||||
from authentik.tenants.models import Tenant
|
from authentik.tenants.models import Tenant
|
||||||
|
|
||||||
_q_default = Q(default=True)
|
|
||||||
DEFAULT_TENANT = Tenant(domain="fallback")
|
|
||||||
|
|
||||||
|
def get_current_tenant() -> Tenant:
|
||||||
def get_tenant_for_request(request: HttpRequest) -> Tenant:
|
"""Get tenant for current request"""
|
||||||
"""Get tenant object for current request"""
|
return connection.tenant
|
||||||
db_tenants = (
|
|
||||||
Tenant.objects.annotate(host_domain=V(request.get_host()))
|
|
||||||
.filter(Q(host_domain__iendswith=F("domain")) | _q_default)
|
|
||||||
.order_by("default")
|
|
||||||
)
|
|
||||||
tenants = list(db_tenants.all())
|
|
||||||
if len(tenants) < 1:
|
|
||||||
return DEFAULT_TENANT
|
|
||||||
return tenants[0]
|
|
||||||
|
|
||||||
|
|
||||||
def context_processor(request: HttpRequest) -> dict[str, Any]:
|
|
||||||
"""Context Processor that injects tenant object into every template"""
|
|
||||||
tenant = getattr(request, "tenant", DEFAULT_TENANT)
|
|
||||||
trace = ""
|
|
||||||
span = Hub.current.scope.span
|
|
||||||
if span:
|
|
||||||
trace = span.to_traceparent()
|
|
||||||
return {
|
|
||||||
"tenant": tenant,
|
|
||||||
"footer_links": CONFIG.get("footer_links"),
|
|
||||||
"sentry_trace": trace,
|
|
||||||
"version": get_full_version(),
|
|
||||||
}
|
|
||||||
|
|
|
@ -0,0 +1,31 @@
|
||||||
|
metadata:
|
||||||
|
name: Default - Brand
|
||||||
|
version: 1
|
||||||
|
entries:
|
||||||
|
- model: authentik_blueprints.metaapplyblueprint
|
||||||
|
attrs:
|
||||||
|
identifiers:
|
||||||
|
name: Default - Authentication flow
|
||||||
|
required: false
|
||||||
|
- model: authentik_blueprints.metaapplyblueprint
|
||||||
|
attrs:
|
||||||
|
identifiers:
|
||||||
|
name: Default - Invalidation flow
|
||||||
|
required: false
|
||||||
|
- model: authentik_blueprints.metaapplyblueprint
|
||||||
|
attrs:
|
||||||
|
identifiers:
|
||||||
|
name: Default - User settings flow
|
||||||
|
required: false
|
||||||
|
- attrs:
|
||||||
|
flow_authentication:
|
||||||
|
!Find [authentik_flows.flow, [slug, default-authentication-flow]]
|
||||||
|
flow_invalidation:
|
||||||
|
!Find [authentik_flows.flow, [slug, default-invalidation-flow]]
|
||||||
|
flow_user_settings:
|
||||||
|
!Find [authentik_flows.flow, [slug, default-user-settings-flow]]
|
||||||
|
identifiers:
|
||||||
|
domain: authentik-default
|
||||||
|
default: True
|
||||||
|
state: created
|
||||||
|
model: authentik_brands.brand
|
|
@ -1,28 +0,0 @@
|
||||||
metadata:
|
|
||||||
name: Default - Tenant
|
|
||||||
version: 1
|
|
||||||
entries:
|
|
||||||
- model: authentik_blueprints.metaapplyblueprint
|
|
||||||
attrs:
|
|
||||||
identifiers:
|
|
||||||
name: Default - Authentication flow
|
|
||||||
required: false
|
|
||||||
- model: authentik_blueprints.metaapplyblueprint
|
|
||||||
attrs:
|
|
||||||
identifiers:
|
|
||||||
name: Default - Invalidation flow
|
|
||||||
required: false
|
|
||||||
- model: authentik_blueprints.metaapplyblueprint
|
|
||||||
attrs:
|
|
||||||
identifiers:
|
|
||||||
name: Default - User settings flow
|
|
||||||
required: false
|
|
||||||
- attrs:
|
|
||||||
flow_authentication: !Find [authentik_flows.flow, [slug, default-authentication-flow]]
|
|
||||||
flow_invalidation: !Find [authentik_flows.flow, [slug, default-invalidation-flow]]
|
|
||||||
flow_user_settings: !Find [authentik_flows.flow, [slug, default-user-settings-flow]]
|
|
||||||
identifiers:
|
|
||||||
domain: authentik-default
|
|
||||||
default: True
|
|
||||||
state: created
|
|
||||||
model: authentik_tenants.tenant
|
|
|
@ -94,21 +94,21 @@ entries:
|
||||||
prompt_data = request.context.get("prompt_data")
|
prompt_data = request.context.get("prompt_data")
|
||||||
|
|
||||||
if not request.user.group_attributes(request.http_request).get(
|
if not request.user.group_attributes(request.http_request).get(
|
||||||
USER_ATTRIBUTE_CHANGE_EMAIL, CONFIG.get_bool("default_user_change_email", True)
|
USER_ATTRIBUTE_CHANGE_EMAIL, request.tenant.default_user_change_email
|
||||||
):
|
):
|
||||||
if prompt_data.get("email") != request.user.email:
|
if prompt_data.get("email") != request.user.email:
|
||||||
ak_message("Not allowed to change email address.")
|
ak_message("Not allowed to change email address.")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if not request.user.group_attributes(request.http_request).get(
|
if not request.user.group_attributes(request.http_request).get(
|
||||||
USER_ATTRIBUTE_CHANGE_NAME, CONFIG.get_bool("default_user_change_name", True)
|
USER_ATTRIBUTE_CHANGE_NAME, request.tenant.default_user_change_name
|
||||||
):
|
):
|
||||||
if prompt_data.get("name") != request.user.name:
|
if prompt_data.get("name") != request.user.name:
|
||||||
ak_message("Not allowed to change name.")
|
ak_message("Not allowed to change name.")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if not request.user.group_attributes(request.http_request).get(
|
if not request.user.group_attributes(request.http_request).get(
|
||||||
USER_ATTRIBUTE_CHANGE_USERNAME, CONFIG.get_bool("default_user_change_username", True)
|
USER_ATTRIBUTE_CHANGE_USERNAME, request.tenant.default_user_change_username
|
||||||
):
|
):
|
||||||
if prompt_data.get("username") != request.user.username:
|
if prompt_data.get("username") != request.user.username:
|
||||||
ak_message("Not allowed to change username.")
|
ak_message("Not allowed to change username.")
|
||||||
|
|
|
@ -41,6 +41,80 @@
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": {
|
"items": {
|
||||||
"oneOf": [
|
"oneOf": [
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"model",
|
||||||
|
"identifiers"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"model": {
|
||||||
|
"const": "authentik_tenants.tenant"
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"state": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"absent",
|
||||||
|
"present",
|
||||||
|
"created",
|
||||||
|
"must_created"
|
||||||
|
],
|
||||||
|
"default": "present"
|
||||||
|
},
|
||||||
|
"conditions": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"attrs": {
|
||||||
|
"$ref": "#/$defs/model_authentik_tenants.tenant"
|
||||||
|
},
|
||||||
|
"identifiers": {
|
||||||
|
"$ref": "#/$defs/model_authentik_tenants.tenant"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"model",
|
||||||
|
"identifiers"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"model": {
|
||||||
|
"const": "authentik_tenants.domain"
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"state": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"absent",
|
||||||
|
"present",
|
||||||
|
"created",
|
||||||
|
"must_created"
|
||||||
|
],
|
||||||
|
"default": "present"
|
||||||
|
},
|
||||||
|
"conditions": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"attrs": {
|
||||||
|
"$ref": "#/$defs/model_authentik_tenants.domain"
|
||||||
|
},
|
||||||
|
"identifiers": {
|
||||||
|
"$ref": "#/$defs/model_authentik_tenants.domain"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
|
@ -2528,7 +2602,7 @@
|
||||||
],
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
"model": {
|
"model": {
|
||||||
"const": "authentik_tenants.tenant"
|
"const": "authentik_brands.brand"
|
||||||
},
|
},
|
||||||
"id": {
|
"id": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
@ -2550,10 +2624,10 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"attrs": {
|
"attrs": {
|
||||||
"$ref": "#/$defs/model_authentik_tenants.tenant"
|
"$ref": "#/$defs/model_authentik_brands.brand"
|
||||||
},
|
},
|
||||||
"identifiers": {
|
"identifiers": {
|
||||||
"$ref": "#/$defs/model_authentik_tenants.tenant"
|
"$ref": "#/$defs/model_authentik_brands.brand"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -2821,6 +2895,43 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"$defs": {
|
"$defs": {
|
||||||
|
"model_authentik_tenants.tenant": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"schema_name": {
|
||||||
|
"type": "string",
|
||||||
|
"maxLength": 63,
|
||||||
|
"minLength": 1,
|
||||||
|
"title": "Schema name"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1,
|
||||||
|
"title": "Name"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": []
|
||||||
|
},
|
||||||
|
"model_authentik_tenants.domain": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"domain": {
|
||||||
|
"type": "string",
|
||||||
|
"maxLength": 253,
|
||||||
|
"minLength": 1,
|
||||||
|
"title": "Domain"
|
||||||
|
},
|
||||||
|
"is_primary": {
|
||||||
|
"type": "boolean",
|
||||||
|
"title": "Is primary"
|
||||||
|
},
|
||||||
|
"tenant": {
|
||||||
|
"type": "integer",
|
||||||
|
"title": "Tenant"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": []
|
||||||
|
},
|
||||||
"model_authentik_crypto.certificatekeypair": {
|
"model_authentik_crypto.certificatekeypair": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
@ -2907,10 +3018,10 @@
|
||||||
"format": "date-time",
|
"format": "date-time",
|
||||||
"title": "Expires"
|
"title": "Expires"
|
||||||
},
|
},
|
||||||
"tenant": {
|
"brand": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"additionalProperties": true,
|
"additionalProperties": true,
|
||||||
"title": "Tenant"
|
"title": "Brand"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": []
|
"required": []
|
||||||
|
@ -3016,10 +3127,10 @@
|
||||||
"format": "date-time",
|
"format": "date-time",
|
||||||
"title": "Expires"
|
"title": "Expires"
|
||||||
},
|
},
|
||||||
"tenant": {
|
"brand": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"additionalProperties": true,
|
"additionalProperties": true,
|
||||||
"title": "Tenant"
|
"title": "Brand"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
|
@ -3427,6 +3538,7 @@
|
||||||
],
|
],
|
||||||
"enum": [
|
"enum": [
|
||||||
null,
|
null,
|
||||||
|
"authentik.tenants",
|
||||||
"authentik.admin",
|
"authentik.admin",
|
||||||
"authentik.api",
|
"authentik.api",
|
||||||
"authentik.crypto",
|
"authentik.crypto",
|
||||||
|
@ -3472,7 +3584,7 @@
|
||||||
"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.brands",
|
||||||
"authentik.blueprints",
|
"authentik.blueprints",
|
||||||
"authentik.core",
|
"authentik.core",
|
||||||
"authentik.enterprise"
|
"authentik.enterprise"
|
||||||
|
@ -3487,6 +3599,8 @@
|
||||||
],
|
],
|
||||||
"enum": [
|
"enum": [
|
||||||
null,
|
null,
|
||||||
|
"authentik_tenants.tenant",
|
||||||
|
"authentik_tenants.domain",
|
||||||
"authentik_crypto.certificatekeypair",
|
"authentik_crypto.certificatekeypair",
|
||||||
"authentik_events.event",
|
"authentik_events.event",
|
||||||
"authentik_events.notificationtransport",
|
"authentik_events.notificationtransport",
|
||||||
|
@ -3554,7 +3668,7 @@
|
||||||
"authentik_stages_user_login.userloginstage",
|
"authentik_stages_user_login.userloginstage",
|
||||||
"authentik_stages_user_logout.userlogoutstage",
|
"authentik_stages_user_logout.userlogoutstage",
|
||||||
"authentik_stages_user_write.userwritestage",
|
"authentik_stages_user_write.userwritestage",
|
||||||
"authentik_tenants.tenant",
|
"authentik_brands.brand",
|
||||||
"authentik_blueprints.blueprintinstance",
|
"authentik_blueprints.blueprintinstance",
|
||||||
"authentik_core.group",
|
"authentik_core.group",
|
||||||
"authentik_core.user",
|
"authentik_core.user",
|
||||||
|
@ -8390,14 +8504,14 @@
|
||||||
},
|
},
|
||||||
"required": []
|
"required": []
|
||||||
},
|
},
|
||||||
"model_authentik_tenants.tenant": {
|
"model_authentik_brands.brand": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"domain": {
|
"domain": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"minLength": 1,
|
"minLength": 1,
|
||||||
"title": "Domain",
|
"title": "Domain",
|
||||||
"description": "Domain that activates this tenant. Can be a superset, i.e. `a.b` for `aa.b` and `ba.b`"
|
"description": "Domain that activates this brand. Can be a superset, i.e. `a.b` for `aa.b` and `ba.b`"
|
||||||
},
|
},
|
||||||
"default": {
|
"default": {
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
|
|
|
@ -19,7 +19,7 @@ import (
|
||||||
sentryutils "goauthentik.io/internal/utils/sentry"
|
sentryutils "goauthentik.io/internal/utils/sentry"
|
||||||
webutils "goauthentik.io/internal/utils/web"
|
webutils "goauthentik.io/internal/utils/web"
|
||||||
"goauthentik.io/internal/web"
|
"goauthentik.io/internal/web"
|
||||||
"goauthentik.io/internal/web/tenant_tls"
|
"goauthentik.io/internal/web/brand_tls"
|
||||||
)
|
)
|
||||||
|
|
||||||
var rootCmd = &cobra.Command{
|
var rootCmd = &cobra.Command{
|
||||||
|
@ -95,11 +95,11 @@ func attemptProxyStart(ws *web.WebServer, u *url.URL) {
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
// Init tenant_tls here too since it requires an API Client,
|
// Init brand_tls here too since it requires an API Client,
|
||||||
// so we just reuse the same one as the outpost uses
|
// so we just reuse the same one as the outpost uses
|
||||||
tw := tenant_tls.NewWatcher(ac.Client)
|
tw := brand_tls.NewWatcher(ac.Client)
|
||||||
go tw.Start()
|
go tw.Start()
|
||||||
ws.TenantTLS = tw
|
ws.BrandTLS = tw
|
||||||
ac.AddRefreshHandler(func() {
|
ac.AddRefreshHandler(func() {
|
||||||
tw.Check()
|
tw.Check()
|
||||||
})
|
})
|
||||||
|
|
|
@ -30,9 +30,9 @@ func (ls *LDAPServer) getCurrentProvider(pk int32) *ProviderInstance {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ls *LDAPServer) getInvalidationFlow() string {
|
func (ls *LDAPServer) getInvalidationFlow() string {
|
||||||
req, _, err := ls.ac.Client.CoreApi.CoreTenantsCurrentRetrieve(context.Background()).Execute()
|
req, _, err := ls.ac.Client.CoreApi.CoreBrandsCurrentRetrieve(context.Background()).Execute()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ls.log.WithError(err).Warning("failed to fetch tenant config")
|
ls.log.WithError(err).Warning("failed to fetch brand config")
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
flow := req.GetFlowInvalidation()
|
flow := req.GetFlowInvalidation()
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
package tenant_tls
|
package brand_tls
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
|
@ -6,6 +6,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
"goauthentik.io/api/v3"
|
"goauthentik.io/api/v3"
|
||||||
"goauthentik.io/internal/crypto"
|
"goauthentik.io/internal/crypto"
|
||||||
"goauthentik.io/internal/outpost/ak"
|
"goauthentik.io/internal/outpost/ak"
|
||||||
|
@ -16,12 +17,12 @@ type Watcher struct {
|
||||||
log *log.Entry
|
log *log.Entry
|
||||||
cs *ak.CryptoStore
|
cs *ak.CryptoStore
|
||||||
fallback *tls.Certificate
|
fallback *tls.Certificate
|
||||||
tenants []api.Tenant
|
brands []api.Brand
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewWatcher(client *api.APIClient) *Watcher {
|
func NewWatcher(client *api.APIClient) *Watcher {
|
||||||
cs := ak.NewCryptoStore(client.CryptoApi)
|
cs := ak.NewCryptoStore(client.CryptoApi)
|
||||||
l := log.WithField("logger", "authentik.router.tenant_tls")
|
l := log.WithField("logger", "authentik.router.brand_tls")
|
||||||
cert, err := crypto.GenerateSelfSignedCert()
|
cert, err := crypto.GenerateSelfSignedCert()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.WithError(err).Error("failed to generate default cert")
|
l.WithError(err).Error("failed to generate default cert")
|
||||||
|
@ -37,20 +38,20 @@ func NewWatcher(client *api.APIClient) *Watcher {
|
||||||
|
|
||||||
func (w *Watcher) Start() {
|
func (w *Watcher) Start() {
|
||||||
ticker := time.NewTicker(time.Minute * 3)
|
ticker := time.NewTicker(time.Minute * 3)
|
||||||
w.log.Info("Starting Tenant TLS Checker")
|
w.log.Info("Starting Brand TLS Checker")
|
||||||
for ; true; <-ticker.C {
|
for ; true; <-ticker.C {
|
||||||
w.Check()
|
w.Check()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *Watcher) Check() {
|
func (w *Watcher) Check() {
|
||||||
w.log.Info("updating tenant certificates")
|
w.log.Info("updating brand certificates")
|
||||||
tenants, _, err := w.client.CoreApi.CoreTenantsListExecute(api.ApiCoreTenantsListRequest{})
|
brands, _, err := w.client.CoreApi.CoreBrandsListExecute(api.ApiCoreBrandsListRequest{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
w.log.WithError(err).Warning("failed to get tenants")
|
w.log.WithError(err).Warning("failed to get brands")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
for _, t := range tenants.Results {
|
for _, t := range brands.Results {
|
||||||
if kp := t.WebCertificate.Get(); kp != nil {
|
if kp := t.WebCertificate.Get(); kp != nil {
|
||||||
err := w.cs.AddKeypair(*kp)
|
err := w.cs.AddKeypair(*kp)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -58,12 +59,12 @@ func (w *Watcher) Check() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
w.tenants = tenants.Results
|
w.brands = brands.Results
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *Watcher) GetCertificate(ch *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
func (w *Watcher) GetCertificate(ch *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||||
var bestSelection *api.Tenant
|
var bestSelection *api.Brand
|
||||||
for _, t := range w.tenants {
|
for _, t := range w.brands {
|
||||||
if t.WebCertificate.Get() == nil {
|
if t.WebCertificate.Get() == nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"net"
|
"net"
|
||||||
|
|
||||||
"github.com/pires/go-proxyproto"
|
"github.com/pires/go-proxyproto"
|
||||||
|
|
||||||
"goauthentik.io/internal/config"
|
"goauthentik.io/internal/config"
|
||||||
"goauthentik.io/internal/crypto"
|
"goauthentik.io/internal/crypto"
|
||||||
"goauthentik.io/internal/utils"
|
"goauthentik.io/internal/utils"
|
||||||
|
@ -26,8 +27,8 @@ func (ws *WebServer) GetCertificate() func(ch *tls.ClientHelloInfo) (*tls.Certif
|
||||||
return appCert, nil
|
return appCert, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ws.TenantTLS != nil {
|
if ws.BrandTLS != nil {
|
||||||
return ws.TenantTLS.GetCertificate(ch)
|
return ws.BrandTLS.GetCertificate(ch)
|
||||||
}
|
}
|
||||||
ws.log.Trace("using default, self-signed certificate")
|
ws.log.Trace("using default, self-signed certificate")
|
||||||
return &cert, nil
|
return &cert, nil
|
||||||
|
|
|
@ -15,11 +15,12 @@ import (
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
"github.com/pires/go-proxyproto"
|
"github.com/pires/go-proxyproto"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
"goauthentik.io/internal/config"
|
"goauthentik.io/internal/config"
|
||||||
"goauthentik.io/internal/gounicorn"
|
"goauthentik.io/internal/gounicorn"
|
||||||
"goauthentik.io/internal/outpost/proxyv2"
|
"goauthentik.io/internal/outpost/proxyv2"
|
||||||
"goauthentik.io/internal/utils/web"
|
"goauthentik.io/internal/utils/web"
|
||||||
"goauthentik.io/internal/web/tenant_tls"
|
"goauthentik.io/internal/web/brand_tls"
|
||||||
)
|
)
|
||||||
|
|
||||||
type WebServer struct {
|
type WebServer struct {
|
||||||
|
@ -29,7 +30,7 @@ type WebServer struct {
|
||||||
stop chan struct{} // channel for waiting shutdown
|
stop chan struct{} // channel for waiting shutdown
|
||||||
|
|
||||||
ProxyServer *proxyv2.ProxyServer
|
ProxyServer *proxyv2.ProxyServer
|
||||||
TenantTLS *tenant_tls.Watcher
|
BrandTLS *brand_tls.Watcher
|
||||||
|
|
||||||
g *gounicorn.GoUnicorn
|
g *gounicorn.GoUnicorn
|
||||||
gr bool
|
gr bool
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
#!/bin/bash -e
|
#!/usr/bin/env -S bash -e
|
||||||
MODE_FILE="${TMPDIR}/authentik-mode"
|
MODE_FILE="${TMPDIR}/authentik-mode"
|
||||||
|
|
||||||
function log {
|
function log {
|
||||||
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
# flake8: noqa
|
||||||
|
from lifecycle.migrate import BaseMigration
|
||||||
|
|
||||||
|
SQL_STATEMENT = """
|
||||||
|
BEGIN TRANSACTION;
|
||||||
|
ALTER TABLE authentik_tenants_tenant RENAME TO authentik_brands_brand;
|
||||||
|
UPDATE django_migrations SET app = replace(app, 'authentik_tenants', 'authentik_brands');
|
||||||
|
UPDATE django_content_type SET app_label = replace(app_label, 'authentik_tenants', 'authentik_brands');
|
||||||
|
COMMIT;
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(BaseMigration):
|
||||||
|
def needs_migration(self) -> bool:
|
||||||
|
self.cur.execute(
|
||||||
|
"select * from information_schema.tables where table_name =" " 'django_migrations';"
|
||||||
|
)
|
||||||
|
# No migration table, assume new installation
|
||||||
|
if not bool(self.cur.rowcount):
|
||||||
|
return False
|
||||||
|
self.cur.execute("select * from django_migrations where app = 'authentik_brands';")
|
||||||
|
return not bool(self.cur.rowcount)
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
self.cur.execute(SQL_STATEMENT)
|
File diff suppressed because it is too large
Load Diff
|
@ -133,6 +133,7 @@ django-guardian = "*"
|
||||||
django-model-utils = "*"
|
django-model-utils = "*"
|
||||||
django-prometheus = "*"
|
django-prometheus = "*"
|
||||||
django-redis = "*"
|
django-redis = "*"
|
||||||
|
django-tenants = { git = "https://github.com/hho6643/django-tenants.git", branch="hho6643-psycopg3_fixes" }
|
||||||
djangorestframework = "*"
|
djangorestframework = "*"
|
||||||
djangorestframework-guardian = "*"
|
djangorestframework-guardian = "*"
|
||||||
docker = "*"
|
docker = "*"
|
||||||
|
|
1604
schema.yml
1604
schema.yml
File diff suppressed because it is too large
Load Diff
|
@ -3,7 +3,7 @@ import Page from "../page.js";
|
||||||
export class LdapForm extends Page {
|
export class LdapForm extends Page {
|
||||||
async setBindFlow(selector: string) {
|
async setBindFlow(selector: string) {
|
||||||
await this.searchSelect(
|
await this.searchSelect(
|
||||||
'>>>ak-tenanted-flow-search[name="authorizationFlow"] input[type="text"]',
|
'>>>ak-branded-flow-search[name="authorizationFlow"] input[type="text"]',
|
||||||
"authorizationFlow",
|
"authorizationFlow",
|
||||||
`button*=${selector}`,
|
`button*=${selector}`,
|
||||||
);
|
);
|
||||||
|
|
|
@ -3,7 +3,7 @@ import Page from "../page.js";
|
||||||
export class RadiusForm extends Page {
|
export class RadiusForm extends Page {
|
||||||
async setAuthenticationFlow(selector: string) {
|
async setAuthenticationFlow(selector: string) {
|
||||||
await this.searchSelect(
|
await this.searchSelect(
|
||||||
'>>>ak-tenanted-flow-search[name="authorizationFlow"] input[type="text"]',
|
'>>>ak-branded-flow-search[name="authorizationFlow"] input[type="text"]',
|
||||||
"authorizationFlow",
|
"authorizationFlow",
|
||||||
`button*=${selector}`,
|
`button*=${selector}`,
|
||||||
);
|
);
|
||||||
|
|
|
@ -0,0 +1,296 @@
|
||||||
|
import { ROUTES } from "@goauthentik/admin/Routes";
|
||||||
|
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||||
|
import {
|
||||||
|
EVENT_API_DRAWER_TOGGLE,
|
||||||
|
EVENT_NOTIFICATION_DRAWER_TOGGLE,
|
||||||
|
EVENT_SIDEBAR_TOGGLE,
|
||||||
|
VERSION,
|
||||||
|
} from "@goauthentik/common/constants";
|
||||||
|
import { configureSentry } from "@goauthentik/common/sentry";
|
||||||
|
import { me } from "@goauthentik/common/users";
|
||||||
|
import { WebsocketClient } from "@goauthentik/common/ws";
|
||||||
|
import { Interface } from "@goauthentik/elements/Base";
|
||||||
|
import "@goauthentik/elements/ak-locale-context";
|
||||||
|
import "@goauthentik/elements/enterprise/EnterpriseStatusBanner";
|
||||||
|
import "@goauthentik/elements/messages/MessageContainer";
|
||||||
|
import "@goauthentik/elements/messages/MessageContainer";
|
||||||
|
import "@goauthentik/elements/notifications/APIDrawer";
|
||||||
|
import "@goauthentik/elements/notifications/NotificationDrawer";
|
||||||
|
import { ID_REGEX, SLUG_REGEX, UUID_REGEX } from "@goauthentik/elements/router/Route";
|
||||||
|
import { getURLParam, updateURLParams } from "@goauthentik/elements/router/RouteMatch";
|
||||||
|
import "@goauthentik/elements/router/RouterOutlet";
|
||||||
|
import "@goauthentik/elements/sidebar/Sidebar";
|
||||||
|
import "@goauthentik/elements/sidebar/SidebarItem";
|
||||||
|
import { spread } from "@open-wc/lit-helpers";
|
||||||
|
|
||||||
|
import { msg, str } from "@lit/localize";
|
||||||
|
import { CSSResult, TemplateResult, css, html, nothing } from "lit";
|
||||||
|
import { customElement, property, state } from "lit/decorators.js";
|
||||||
|
import { map } from "lit/directives/map.js";
|
||||||
|
|
||||||
|
import PFButton from "@patternfly/patternfly/components/Button/button.css";
|
||||||
|
import PFDrawer from "@patternfly/patternfly/components/Drawer/drawer.css";
|
||||||
|
import PFPage from "@patternfly/patternfly/components/Page/page.css";
|
||||||
|
import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||||
|
|
||||||
|
import {
|
||||||
|
AdminApi,
|
||||||
|
CapabilitiesEnum,
|
||||||
|
CoreApi,
|
||||||
|
SessionUser,
|
||||||
|
UiThemeEnum,
|
||||||
|
Version,
|
||||||
|
} from "@goauthentik/api";
|
||||||
|
|
||||||
|
@customElement("ak-interface-admin")
|
||||||
|
export class AdminInterface extends Interface {
|
||||||
|
@property({ type: Boolean })
|
||||||
|
sidebarOpen = true;
|
||||||
|
|
||||||
|
@property({ type: Boolean })
|
||||||
|
notificationDrawerOpen = getURLParam("notificationDrawerOpen", false);
|
||||||
|
|
||||||
|
@property({ type: Boolean })
|
||||||
|
apiDrawerOpen = getURLParam("apiDrawerOpen", false);
|
||||||
|
|
||||||
|
ws: WebsocketClient;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
version?: Version;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
user?: SessionUser;
|
||||||
|
|
||||||
|
static get styles(): CSSResult[] {
|
||||||
|
return [
|
||||||
|
PFBase,
|
||||||
|
PFPage,
|
||||||
|
PFButton,
|
||||||
|
PFDrawer,
|
||||||
|
css`
|
||||||
|
.pf-c-page__main,
|
||||||
|
.pf-c-drawer__content,
|
||||||
|
.pf-c-page__drawer {
|
||||||
|
z-index: auto !important;
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
.display-none {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.pf-c-page {
|
||||||
|
background-color: var(--pf-c-page--BackgroundColor) !important;
|
||||||
|
}
|
||||||
|
/* Global page background colour */
|
||||||
|
:host([theme="dark"]) .pf-c-page {
|
||||||
|
--pf-c-page--BackgroundColor: var(--ak-dark-background);
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.ws = new WebsocketClient();
|
||||||
|
this.sidebarOpen = window.innerWidth >= 1280;
|
||||||
|
window.addEventListener("resize", () => {
|
||||||
|
this.sidebarOpen = window.innerWidth >= 1280;
|
||||||
|
});
|
||||||
|
window.addEventListener(EVENT_SIDEBAR_TOGGLE, () => {
|
||||||
|
this.sidebarOpen = !this.sidebarOpen;
|
||||||
|
});
|
||||||
|
window.addEventListener(EVENT_NOTIFICATION_DRAWER_TOGGLE, () => {
|
||||||
|
this.notificationDrawerOpen = !this.notificationDrawerOpen;
|
||||||
|
updateURLParams({
|
||||||
|
notificationDrawerOpen: this.notificationDrawerOpen,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
window.addEventListener(EVENT_API_DRAWER_TOGGLE, () => {
|
||||||
|
this.apiDrawerOpen = !this.apiDrawerOpen;
|
||||||
|
updateURLParams({
|
||||||
|
apiDrawerOpen: this.apiDrawerOpen,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async firstUpdated(): Promise<void> {
|
||||||
|
configureSentry(true);
|
||||||
|
this.version = await new AdminApi(DEFAULT_CONFIG).adminVersionRetrieve();
|
||||||
|
this.user = await me();
|
||||||
|
const canAccessAdmin =
|
||||||
|
this.user.user.isSuperuser ||
|
||||||
|
// TODO: somehow add `access_admin_interface` to the API schema
|
||||||
|
this.user.user.systemPermissions.includes("access_admin_interface");
|
||||||
|
if (!canAccessAdmin && this.user.user.pk > 0) {
|
||||||
|
window.location.assign("/if/user/");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render(): TemplateResult {
|
||||||
|
return html` <ak-locale-context>
|
||||||
|
<div class="pf-c-page">
|
||||||
|
<ak-sidebar
|
||||||
|
class="pf-c-page__sidebar ${this.sidebarOpen
|
||||||
|
? "pf-m-expanded"
|
||||||
|
: "pf-m-collapsed"} ${this.activeTheme === UiThemeEnum.Light
|
||||||
|
? "pf-m-light"
|
||||||
|
: ""}"
|
||||||
|
>
|
||||||
|
${this.renderSidebarItems()}
|
||||||
|
</ak-sidebar>
|
||||||
|
<div class="pf-c-page__drawer">
|
||||||
|
<div
|
||||||
|
class="pf-c-drawer ${this.notificationDrawerOpen || this.apiDrawerOpen
|
||||||
|
? "pf-m-expanded"
|
||||||
|
: "pf-m-collapsed"}"
|
||||||
|
>
|
||||||
|
<div class="pf-c-drawer__main">
|
||||||
|
<div class="pf-c-drawer__content">
|
||||||
|
<div class="pf-c-drawer__body">
|
||||||
|
<main class="pf-c-page__main">
|
||||||
|
<ak-router-outlet
|
||||||
|
role="main"
|
||||||
|
class="pf-c-page__main"
|
||||||
|
tabindex="-1"
|
||||||
|
id="main-content"
|
||||||
|
defaultUrl="/administration/overview"
|
||||||
|
.routes=${ROUTES}
|
||||||
|
>
|
||||||
|
</ak-router-outlet>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ak-notification-drawer
|
||||||
|
class="pf-c-drawer__panel pf-m-width-33 ${this
|
||||||
|
.notificationDrawerOpen
|
||||||
|
? ""
|
||||||
|
: "display-none"}"
|
||||||
|
?hidden=${!this.notificationDrawerOpen}
|
||||||
|
></ak-notification-drawer>
|
||||||
|
<ak-api-drawer
|
||||||
|
class="pf-c-drawer__panel pf-m-width-33 ${this.apiDrawerOpen
|
||||||
|
? ""
|
||||||
|
: "display-none"}"
|
||||||
|
?hidden=${!this.apiDrawerOpen}
|
||||||
|
></ak-api-drawer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div></div
|
||||||
|
></ak-locale-context>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderSidebarItems(): TemplateResult {
|
||||||
|
// The second attribute type is of string[] to help with the 'activeWhen' control, which was
|
||||||
|
// commonplace and singular enough to merit its own handler.
|
||||||
|
type SidebarEntry = [
|
||||||
|
path: string | null,
|
||||||
|
label: string,
|
||||||
|
attributes?: Record<string, any> | string[] | null, // eslint-disable-line
|
||||||
|
children?: SidebarEntry[],
|
||||||
|
];
|
||||||
|
|
||||||
|
// prettier-ignore
|
||||||
|
const sidebarContent: SidebarEntry[] = [
|
||||||
|
["/if/user/", msg("User interface"), { "?isAbsoluteLink": true, "?highlight": true }],
|
||||||
|
[null, msg("Dashboards"), { "?expanded": true }, [
|
||||||
|
["/administration/overview", msg("Overview")],
|
||||||
|
["/administration/dashboard/users", msg("User Statistics")],
|
||||||
|
["/administration/system-tasks", msg("System Tasks")]]],
|
||||||
|
[null, msg("Applications"), null, [
|
||||||
|
["/core/providers", msg("Providers"), [`^/core/providers/(?<id>${ID_REGEX})$`]],
|
||||||
|
["/core/applications", msg("Applications"), [`^/core/applications/(?<slug>${SLUG_REGEX})$`]],
|
||||||
|
["/outpost/outposts", msg("Outposts")]]],
|
||||||
|
[null, msg("Events"), null, [
|
||||||
|
["/events/log", msg("Logs"), [`^/events/log/(?<id>${UUID_REGEX})$`]],
|
||||||
|
["/events/rules", msg("Notification Rules")],
|
||||||
|
["/events/transports", msg("Notification Transports")]]],
|
||||||
|
[null, msg("Customisation"), null, [
|
||||||
|
["/policy/policies", msg("Policies")],
|
||||||
|
["/core/property-mappings", msg("Property Mappings")],
|
||||||
|
["/blueprints/instances", msg("Blueprints")],
|
||||||
|
["/policy/reputation", msg("Reputation scores")]]],
|
||||||
|
[null, msg("Flows and Stages"), null, [
|
||||||
|
["/flow/flows", msg("Flows"), [`^/flow/flows/(?<slug>${SLUG_REGEX})$`]],
|
||||||
|
["/flow/stages", msg("Stages")],
|
||||||
|
["/flow/stages/prompts", msg("Prompts")]]],
|
||||||
|
[null, msg("Directory"), null, [
|
||||||
|
["/identity/users", msg("Users"), [`^/identity/users/(?<id>${ID_REGEX})$`]],
|
||||||
|
["/identity/groups", msg("Groups"), [`^/identity/groups/(?<id>${UUID_REGEX})$`]],
|
||||||
|
["/identity/roles", msg("Roles"), [`^/identity/roles/(?<id>${UUID_REGEX})$`]],
|
||||||
|
["/core/sources", msg("Federation and Social login"), [`^/core/sources/(?<slug>${SLUG_REGEX})$`]],
|
||||||
|
["/core/tokens", msg("Tokens and App passwords")],
|
||||||
|
["/flow/stages/invitations", msg("Invitations")]]],
|
||||||
|
[null, msg("System"), null, [
|
||||||
|
["/core/brands", msg("Brands")],
|
||||||
|
["/crypto/certificates", msg("Certificates")],
|
||||||
|
["/outpost/integrations", msg("Outpost Integrations")]]]
|
||||||
|
];
|
||||||
|
|
||||||
|
// Typescript requires the type here to correctly type the recursive path
|
||||||
|
type SidebarRenderer = (_: SidebarEntry) => TemplateResult;
|
||||||
|
|
||||||
|
const renderOneSidebarItem: SidebarRenderer = ([path, label, attributes, children]) => {
|
||||||
|
const properties = Array.isArray(attributes)
|
||||||
|
? { ".activeWhen": attributes }
|
||||||
|
: attributes ?? {};
|
||||||
|
if (path) {
|
||||||
|
properties["path"] = path;
|
||||||
|
}
|
||||||
|
return html`<ak-sidebar-item ${spread(properties)}>
|
||||||
|
${label ? html`<span slot="label">${label}</span>` : nothing}
|
||||||
|
${map(children, renderOneSidebarItem)}
|
||||||
|
</ak-sidebar-item>`;
|
||||||
|
};
|
||||||
|
|
||||||
|
// prettier-ignore
|
||||||
|
return html`
|
||||||
|
${this.renderNewVersionMessage()}
|
||||||
|
${this.renderImpersonationMessage()}
|
||||||
|
${map(sidebarContent, renderOneSidebarItem)}
|
||||||
|
${this.renderEnterpriseMessage()}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderNewVersionMessage() {
|
||||||
|
return this.version && this.version.versionCurrent !== VERSION
|
||||||
|
? html`
|
||||||
|
<ak-sidebar-item ?highlight=${true}>
|
||||||
|
<span slot="label"
|
||||||
|
>${msg("A newer version of the frontend is available.")}</span
|
||||||
|
>
|
||||||
|
</ak-sidebar-item>
|
||||||
|
`
|
||||||
|
: nothing;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderImpersonationMessage() {
|
||||||
|
return this.user?.original
|
||||||
|
? html`<ak-sidebar-item
|
||||||
|
?highlight=${true}
|
||||||
|
@click=${() => {
|
||||||
|
new CoreApi(DEFAULT_CONFIG).coreUsersImpersonateEndRetrieve().then(() => {
|
||||||
|
window.location.reload();
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span slot="label"
|
||||||
|
>${msg(
|
||||||
|
str`You're currently impersonating ${this.user.user.username}. Click to stop.`,
|
||||||
|
)}</span
|
||||||
|
>
|
||||||
|
</ak-sidebar-item>`
|
||||||
|
: nothing;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderEnterpriseMessage() {
|
||||||
|
return this.config?.capabilities.includes(CapabilitiesEnum.IsEnterprise)
|
||||||
|
? html`
|
||||||
|
<ak-sidebar-item>
|
||||||
|
<span slot="label">${msg("Enterprise")}</span>
|
||||||
|
<ak-sidebar-item path="/enterprise/licenses">
|
||||||
|
<span slot="label">${msg("Licenses")}</span>
|
||||||
|
</ak-sidebar-item>
|
||||||
|
</ak-sidebar-item>
|
||||||
|
`
|
||||||
|
: nothing;
|
||||||
|
}
|
||||||
|
}
|
|
@ -52,9 +52,9 @@ export const ROUTES: Route[] = [
|
||||||
await import("@goauthentik/admin/tokens/TokenListPage");
|
await import("@goauthentik/admin/tokens/TokenListPage");
|
||||||
return html`<ak-token-list></ak-token-list>`;
|
return html`<ak-token-list></ak-token-list>`;
|
||||||
}),
|
}),
|
||||||
new Route(new RegExp("^/core/tenants$"), async () => {
|
new Route(new RegExp("^/core/brands"), async () => {
|
||||||
await import("@goauthentik/admin/tenants/TenantListPage");
|
await import("@goauthentik/admin/brands/BrandListPage");
|
||||||
return html`<ak-tenant-list></ak-tenant-list>`;
|
return html`<ak-brand-list></ak-brand-list>`;
|
||||||
}),
|
}),
|
||||||
new Route(new RegExp("^/policy/policies$"), async () => {
|
new Route(new RegExp("^/policy/policies$"), async () => {
|
||||||
await import("@goauthentik/admin/policies/PolicyListPage");
|
await import("@goauthentik/admin/policies/PolicyListPage");
|
||||||
|
|
|
@ -57,7 +57,7 @@ export class RecentEventsCard extends Table<Event> {
|
||||||
new TableColumn(msg("User"), "user"),
|
new TableColumn(msg("User"), "user"),
|
||||||
new TableColumn(msg("Creation Date"), "created"),
|
new TableColumn(msg("Creation Date"), "created"),
|
||||||
new TableColumn(msg("Client IP"), "client_ip"),
|
new TableColumn(msg("Client IP"), "client_ip"),
|
||||||
new TableColumn(msg("Tenant"), "tenant_name"),
|
new TableColumn(msg("Brand"), "brand_name"),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -88,7 +88,7 @@ export class RecentEventsCard extends Table<Event> {
|
||||||
html`<span>${item.created?.toLocaleString()}</span>`,
|
html`<span>${item.created?.toLocaleString()}</span>`,
|
||||||
html` <div>${item.clientIp || msg("-")}</div>
|
html` <div>${item.clientIp || msg("-")}</div>
|
||||||
<small>${EventGeo(item)}</small>`,
|
<small>${EventGeo(item)}</small>`,
|
||||||
html`<span>${item.tenant?.name || msg("-")}</span>`,
|
html`<span>${item.brand?.name || msg("-")}</span>`,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import "@goauthentik/admin/common/ak-core-group-search";
|
import "@goauthentik/admin/common/ak-core-group-search";
|
||||||
import "@goauthentik/admin/common/ak-crypto-certificate-search";
|
import "@goauthentik/admin/common/ak-crypto-certificate-search";
|
||||||
import "@goauthentik/admin/common/ak-flow-search/ak-tenanted-flow-search";
|
import "@goauthentik/admin/common/ak-flow-search/ak-branded-flow-search";
|
||||||
import { first } from "@goauthentik/common/utils";
|
import { first } from "@goauthentik/common/utils";
|
||||||
import "@goauthentik/components/ak-number-input";
|
import "@goauthentik/components/ak-number-input";
|
||||||
import "@goauthentik/components/ak-radio-input";
|
import "@goauthentik/components/ak-radio-input";
|
||||||
|
@ -49,12 +49,12 @@ export class ApplicationWizardApplicationDetails extends BaseProviderPanel {
|
||||||
?required=${true}
|
?required=${true}
|
||||||
name="authorizationFlow"
|
name="authorizationFlow"
|
||||||
>
|
>
|
||||||
<ak-tenanted-flow-search
|
<ak-branded-flow-search
|
||||||
flowType=${FlowsInstancesListDesignationEnum.Authentication}
|
flowType=${FlowsInstancesListDesignationEnum.Authentication}
|
||||||
.currentFlow=${provider?.authorizationFlow}
|
.currentFlow=${provider?.authorizationFlow}
|
||||||
.tenantFlow=${rootInterface()?.tenant?.flowAuthentication}
|
.brandFlow=${rootInterface()?.brand?.flowAuthentication}
|
||||||
required
|
required
|
||||||
></ak-tenanted-flow-search>
|
></ak-branded-flow-search>
|
||||||
<p class="pf-c-form__helper-text">${msg("Flow used for users to authenticate.")}</p>
|
<p class="pf-c-form__helper-text">${msg("Flow used for users to authenticate.")}</p>
|
||||||
</ak-form-element-horizontal>
|
</ak-form-element-horizontal>
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import "@goauthentik/admin/common/ak-crypto-certificate-search";
|
import "@goauthentik/admin/common/ak-crypto-certificate-search";
|
||||||
import "@goauthentik/admin/common/ak-flow-search/ak-tenanted-flow-search";
|
import "@goauthentik/admin/common/ak-flow-search/ak-branted-flow-search";
|
||||||
import {
|
import {
|
||||||
clientTypeOptions,
|
clientTypeOptions,
|
||||||
issuerModeOptions,
|
issuerModeOptions,
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import "@goauthentik/admin/common/ak-crypto-certificate-search";
|
import "@goauthentik/admin/common/ak-crypto-certificate-search";
|
||||||
import "@goauthentik/admin/common/ak-flow-search/ak-tenanted-flow-search";
|
import "@goauthentik/admin/common/ak-flow-search/ak-branded-flow-search";
|
||||||
import { ascii_letters, digits, first, randomString } from "@goauthentik/common/utils";
|
import { ascii_letters, digits, first, randomString } from "@goauthentik/common/utils";
|
||||||
import "@goauthentik/components/ak-text-input";
|
import "@goauthentik/components/ak-text-input";
|
||||||
import { rootInterface } from "@goauthentik/elements/Base";
|
import { rootInterface } from "@goauthentik/elements/Base";
|
||||||
|
@ -34,12 +34,12 @@ export class ApplicationWizardAuthenticationByRadius extends BaseProviderPanel {
|
||||||
?required=${true}
|
?required=${true}
|
||||||
name="authorizationFlow"
|
name="authorizationFlow"
|
||||||
>
|
>
|
||||||
<ak-tenanted-flow-search
|
<ak-branded-flow-search
|
||||||
flowType=${FlowsInstancesListDesignationEnum.Authentication}
|
flowType=${FlowsInstancesListDesignationEnum.Authentication}
|
||||||
.currentFlow=${provider?.authorizationFlow}
|
.currentFlow=${provider?.authorizationFlow}
|
||||||
.tenantFlow=${rootInterface()?.tenant?.flowAuthentication}
|
.brandFlow=${rootInterface()?.brand?.flowAuthentication}
|
||||||
required
|
required
|
||||||
></ak-tenanted-flow-search>
|
></ak-branded-flow-search>
|
||||||
<p class="pf-c-form__helper-text">${msg("Flow used for users to authenticate.")}</p>
|
<p class="pf-c-form__helper-text">${msg("Flow used for users to authenticate.")}</p>
|
||||||
</ak-form-element-horizontal>
|
</ak-form-element-horizontal>
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import "@goauthentik/admin/common/ak-core-group-search";
|
import "@goauthentik/admin/common/ak-core-group-search";
|
||||||
import "@goauthentik/admin/common/ak-crypto-certificate-search";
|
import "@goauthentik/admin/common/ak-crypto-certificate-search";
|
||||||
import "@goauthentik/admin/common/ak-flow-search/ak-tenanted-flow-search";
|
import "@goauthentik/admin/common/ak-flow-search/ak-branded-flow-search";
|
||||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||||
import "@goauthentik/components/ak-number-input";
|
import "@goauthentik/components/ak-number-input";
|
||||||
import "@goauthentik/components/ak-radio-input";
|
import "@goauthentik/components/ak-radio-input";
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import "@goauthentik/admin/common/ak-crypto-certificate-search";
|
import "@goauthentik/admin/common/ak-crypto-certificate-search";
|
||||||
import "@goauthentik/admin/common/ak-flow-search/ak-tenanted-flow-search";
|
import "@goauthentik/admin/common/ak-flow-search/ak-branded-flow-search";
|
||||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||||
import { first } from "@goauthentik/common/utils";
|
import { first } from "@goauthentik/common/utils";
|
||||||
import "@goauthentik/components/ak-switch-input";
|
import "@goauthentik/components/ak-switch-input";
|
||||||
|
|
|
@ -8,40 +8,40 @@ import "@goauthentik/elements/forms/FormGroup";
|
||||||
import "@goauthentik/elements/forms/HorizontalFormElement";
|
import "@goauthentik/elements/forms/HorizontalFormElement";
|
||||||
import { ModelForm } from "@goauthentik/elements/forms/ModelForm";
|
import { ModelForm } from "@goauthentik/elements/forms/ModelForm";
|
||||||
import "@goauthentik/elements/forms/SearchSelect";
|
import "@goauthentik/elements/forms/SearchSelect";
|
||||||
import { DefaultTenant } from "@goauthentik/elements/sidebar/SidebarBrand";
|
import { DefaultBrand } from "@goauthentik/elements/sidebar/SidebarBrand";
|
||||||
import YAML from "yaml";
|
import YAML from "yaml";
|
||||||
|
|
||||||
import { msg } from "@lit/localize";
|
import { msg } from "@lit/localize";
|
||||||
import { TemplateResult, html } from "lit";
|
import { TemplateResult, html } from "lit";
|
||||||
import { customElement } from "lit/decorators.js";
|
import { customElement } from "lit/decorators.js";
|
||||||
|
|
||||||
import { CoreApi, FlowsInstancesListDesignationEnum, Tenant } from "@goauthentik/api";
|
import { Brand, CoreApi, FlowsInstancesListDesignationEnum } from "@goauthentik/api";
|
||||||
|
|
||||||
@customElement("ak-tenant-form")
|
@customElement("ak-brand-form")
|
||||||
export class TenantForm extends ModelForm<Tenant, string> {
|
export class BrandForm extends ModelForm<Brand, string> {
|
||||||
loadInstance(pk: string): Promise<Tenant> {
|
loadInstance(pk: string): Promise<Brand> {
|
||||||
return new CoreApi(DEFAULT_CONFIG).coreTenantsRetrieve({
|
return new CoreApi(DEFAULT_CONFIG).coreBrandsRetrieve({
|
||||||
tenantUuid: pk,
|
brandUuid: pk,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getSuccessMessage(): string {
|
getSuccessMessage(): string {
|
||||||
if (this.instance) {
|
if (this.instance) {
|
||||||
return msg("Successfully updated tenant.");
|
return msg("Successfully updated brand.");
|
||||||
} else {
|
} else {
|
||||||
return msg("Successfully created tenant.");
|
return msg("Successfully created brand.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async send(data: Tenant): Promise<Tenant> {
|
async send(data: Brand): Promise<Brand> {
|
||||||
if (this.instance?.tenantUuid) {
|
if (this.instance?.brandUuid) {
|
||||||
return new CoreApi(DEFAULT_CONFIG).coreTenantsUpdate({
|
return new CoreApi(DEFAULT_CONFIG).coreBrandsUpdate({
|
||||||
tenantUuid: this.instance.tenantUuid,
|
brandUuid: this.instance.brandUuid,
|
||||||
tenantRequest: data,
|
brandRequest: data,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
return new CoreApi(DEFAULT_CONFIG).coreTenantsCreate({
|
return new CoreApi(DEFAULT_CONFIG).coreBrandsCreate({
|
||||||
tenantRequest: data,
|
brandRequest: data,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -79,7 +79,7 @@ export class TenantForm extends ModelForm<Tenant, string> {
|
||||||
<span class="pf-c-switch__label">${msg("Default")}</span>
|
<span class="pf-c-switch__label">${msg("Default")}</span>
|
||||||
</label>
|
</label>
|
||||||
<p class="pf-c-form__helper-text">
|
<p class="pf-c-form__helper-text">
|
||||||
${msg("Use this tenant for each domain that doesn't have a dedicated tenant.")}
|
${msg("Use this brand for each domain that doesn't have a dedicated brand.")}
|
||||||
</p>
|
</p>
|
||||||
</ak-form-element-horizontal>
|
</ak-form-element-horizontal>
|
||||||
|
|
||||||
|
@ -95,7 +95,7 @@ export class TenantForm extends ModelForm<Tenant, string> {
|
||||||
type="text"
|
type="text"
|
||||||
value="${first(
|
value="${first(
|
||||||
this.instance?.brandingTitle,
|
this.instance?.brandingTitle,
|
||||||
DefaultTenant.brandingTitle,
|
DefaultBrand.brandingTitle,
|
||||||
)}"
|
)}"
|
||||||
class="pf-c-form-control"
|
class="pf-c-form-control"
|
||||||
required
|
required
|
||||||
|
@ -111,10 +111,7 @@ export class TenantForm extends ModelForm<Tenant, string> {
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value="${first(
|
value="${first(this.instance?.brandingLogo, DefaultBrand.brandingLogo)}"
|
||||||
this.instance?.brandingLogo,
|
|
||||||
DefaultTenant.brandingLogo,
|
|
||||||
)}"
|
|
||||||
class="pf-c-form-control"
|
class="pf-c-form-control"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
@ -131,7 +128,7 @@ export class TenantForm extends ModelForm<Tenant, string> {
|
||||||
type="text"
|
type="text"
|
||||||
value="${first(
|
value="${first(
|
||||||
this.instance?.brandingFavicon,
|
this.instance?.brandingFavicon,
|
||||||
DefaultTenant.brandingFavicon,
|
DefaultBrand.brandingFavicon,
|
||||||
)}"
|
)}"
|
||||||
class="pf-c-form-control"
|
class="pf-c-form-control"
|
||||||
required
|
required
|
||||||
|
@ -274,7 +271,7 @@ export class TenantForm extends ModelForm<Tenant, string> {
|
||||||
</ak-codemirror>
|
</ak-codemirror>
|
||||||
<p class="pf-c-form__helper-text">
|
<p class="pf-c-form__helper-text">
|
||||||
${msg(
|
${msg(
|
||||||
"Set custom attributes using YAML or JSON. Any attributes set here will be inherited by users, if the request is handled by this tenant.",
|
"Set custom attributes using YAML or JSON. Any attributes set here will be inherited by users, if the request is handled by this brand.",
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
</ak-form-element-horizontal>
|
</ak-form-element-horizontal>
|
|
@ -1,4 +1,4 @@
|
||||||
import "@goauthentik/admin/tenants/TenantForm";
|
import "@goauthentik/admin/brands/BrandForm";
|
||||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||||
import { uiConfig } from "@goauthentik/common/ui/config";
|
import { uiConfig } from "@goauthentik/common/ui/config";
|
||||||
import "@goauthentik/components/ak-status-label";
|
import "@goauthentik/components/ak-status-label";
|
||||||
|
@ -16,21 +16,21 @@ import { msg } from "@lit/localize";
|
||||||
import { TemplateResult, html } from "lit";
|
import { TemplateResult, html } from "lit";
|
||||||
import { customElement, property } from "lit/decorators.js";
|
import { customElement, property } from "lit/decorators.js";
|
||||||
|
|
||||||
import { CoreApi, RbacPermissionsAssignedByUsersListModelEnum, Tenant } from "@goauthentik/api";
|
import { Brand, CoreApi, RbacPermissionsAssignedByUsersListModelEnum } from "@goauthentik/api";
|
||||||
|
|
||||||
@customElement("ak-tenant-list")
|
@customElement("ak-brand-list")
|
||||||
export class TenantListPage extends TablePage<Tenant> {
|
export class BrandListPage extends TablePage<Brand> {
|
||||||
searchEnabled(): boolean {
|
searchEnabled(): boolean {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
pageTitle(): string {
|
pageTitle(): string {
|
||||||
return msg("Tenants");
|
return msg("Brands");
|
||||||
}
|
}
|
||||||
pageDescription(): string {
|
pageDescription(): string {
|
||||||
return msg("Configure visual settings and defaults for different domains.");
|
return msg("Configure visual settings and defaults for different domains.");
|
||||||
}
|
}
|
||||||
pageIcon(): string {
|
pageIcon(): string {
|
||||||
return "pf-icon pf-icon-tenant";
|
return "pf-icon pf-icon-brand";
|
||||||
}
|
}
|
||||||
|
|
||||||
checkbox = true;
|
checkbox = true;
|
||||||
|
@ -38,8 +38,8 @@ export class TenantListPage extends TablePage<Tenant> {
|
||||||
@property()
|
@property()
|
||||||
order = "domain";
|
order = "domain";
|
||||||
|
|
||||||
async apiEndpoint(page: number): Promise<PaginatedResponse<Tenant>> {
|
async apiEndpoint(page: number): Promise<PaginatedResponse<Brand>> {
|
||||||
return new CoreApi(DEFAULT_CONFIG).coreTenantsList({
|
return new CoreApi(DEFAULT_CONFIG).coreBrandsList({
|
||||||
ordering: this.order,
|
ordering: this.order,
|
||||||
page: page,
|
page: page,
|
||||||
pageSize: (await uiConfig()).pagination.perPage,
|
pageSize: (await uiConfig()).pagination.perPage,
|
||||||
|
@ -58,19 +58,19 @@ export class TenantListPage extends TablePage<Tenant> {
|
||||||
renderToolbarSelected(): TemplateResult {
|
renderToolbarSelected(): TemplateResult {
|
||||||
const disabled = this.selectedElements.length < 1;
|
const disabled = this.selectedElements.length < 1;
|
||||||
return html`<ak-forms-delete-bulk
|
return html`<ak-forms-delete-bulk
|
||||||
objectLabel=${msg("Tenant(s)")}
|
objectLabel=${msg("Brand(s)")}
|
||||||
.objects=${this.selectedElements}
|
.objects=${this.selectedElements}
|
||||||
.metadata=${(item: Tenant) => {
|
.metadata=${(item: Brand) => {
|
||||||
return [{ key: msg("Domain"), value: item.domain }];
|
return [{ key: msg("Domain"), value: item.domain }];
|
||||||
}}
|
}}
|
||||||
.usedBy=${(item: Tenant) => {
|
.usedBy=${(item: Brand) => {
|
||||||
return new CoreApi(DEFAULT_CONFIG).coreTenantsUsedByList({
|
return new CoreApi(DEFAULT_CONFIG).coreBrandsUsedByList({
|
||||||
tenantUuid: item.tenantUuid,
|
brandUuid: item.brandUuid,
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
.delete=${(item: Tenant) => {
|
.delete=${(item: Brand) => {
|
||||||
return new CoreApi(DEFAULT_CONFIG).coreTenantsDestroy({
|
return new CoreApi(DEFAULT_CONFIG).coreBrandsDestroy({
|
||||||
tenantUuid: item.tenantUuid,
|
brandUuid: item.brandUuid,
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
@ -80,14 +80,14 @@ export class TenantListPage extends TablePage<Tenant> {
|
||||||
</ak-forms-delete-bulk>`;
|
</ak-forms-delete-bulk>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
row(item: Tenant): TemplateResult[] {
|
row(item: Brand): TemplateResult[] {
|
||||||
return [
|
return [
|
||||||
html`${item.domain}`,
|
html`${item.domain}`,
|
||||||
html`<ak-status-label ?good=${item._default}></ak-status-label>`,
|
html`<ak-status-label ?good=${item._default}></ak-status-label>`,
|
||||||
html`<ak-forms-modal>
|
html`<ak-forms-modal>
|
||||||
<span slot="submit"> ${msg("Update")} </span>
|
<span slot="submit"> ${msg("Update")} </span>
|
||||||
<span slot="header"> ${msg("Update Tenant")} </span>
|
<span slot="header"> ${msg("Update Brand")} </span>
|
||||||
<ak-tenant-form slot="form" .instancePk=${item.tenantUuid}> </ak-tenant-form>
|
<ak-brand-form slot="form" .instancePk=${item.brandUuid}> </ak-brand-form>
|
||||||
<button slot="trigger" class="pf-c-button pf-m-plain">
|
<button slot="trigger" class="pf-c-button pf-m-plain">
|
||||||
<pf-tooltip position="top" content=${msg("Edit")}>
|
<pf-tooltip position="top" content=${msg("Edit")}>
|
||||||
<i class="fas fa-edit"></i>
|
<i class="fas fa-edit"></i>
|
||||||
|
@ -96,8 +96,8 @@ export class TenantListPage extends TablePage<Tenant> {
|
||||||
</ak-forms-modal>
|
</ak-forms-modal>
|
||||||
|
|
||||||
<ak-rbac-object-permission-modal
|
<ak-rbac-object-permission-modal
|
||||||
model=${RbacPermissionsAssignedByUsersListModelEnum.TenantsTenant}
|
model=${RbacPermissionsAssignedByUsersListModelEnum.BrandsBrand}
|
||||||
objectPk=${item.tenantUuid}
|
objectPk=${item.brandUuid}
|
||||||
>
|
>
|
||||||
</ak-rbac-object-permission-modal>`,
|
</ak-rbac-object-permission-modal>`,
|
||||||
];
|
];
|
||||||
|
@ -107,8 +107,8 @@ export class TenantListPage extends TablePage<Tenant> {
|
||||||
return html`
|
return html`
|
||||||
<ak-forms-modal>
|
<ak-forms-modal>
|
||||||
<span slot="submit"> ${msg("Create")} </span>
|
<span slot="submit"> ${msg("Create")} </span>
|
||||||
<span slot="header"> ${msg("Create Tenant")} </span>
|
<span slot="header"> ${msg("Create Brand")} </span>
|
||||||
<ak-tenant-form slot="form"> </ak-tenant-form>
|
<ak-brand-form slot="form"> </ak-brand-form>
|
||||||
<button slot="trigger" class="pf-c-button pf-m-primary">${msg("Create")}</button>
|
<button slot="trigger" class="pf-c-button pf-m-primary">${msg("Create")}</button>
|
||||||
</ak-forms-modal>
|
</ak-forms-modal>
|
||||||
`;
|
`;
|
|
@ -28,7 +28,7 @@ export function getFlowValue(flow: Flow | undefined): string | undefined {
|
||||||
*
|
*
|
||||||
* A wrapper around SearchSelect that understands the basic semantics of querying about Flows. This
|
* A wrapper around SearchSelect that understands the basic semantics of querying about Flows. This
|
||||||
* code eliminates the long blocks of unreadable invocation that were embedded in every provider, as well as in
|
* code eliminates the long blocks of unreadable invocation that were embedded in every provider, as well as in
|
||||||
* sources, tenants, and applications.
|
* sources, brands, and applications.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
@ -94,7 +94,7 @@ export class FlowSearch<T extends Flow> extends CustomListenerElement(AKElement)
|
||||||
}
|
}
|
||||||
|
|
||||||
/* This is the most commonly overridden method of this class. About half of the Flow Searches
|
/* This is the most commonly overridden method of this class. About half of the Flow Searches
|
||||||
* use this method, but several have more complex needs, such as relating to the tenant, or just
|
* use this method, but several have more complex needs, such as relating to the brand, or just
|
||||||
* returning false.
|
* returning false.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,34 @@
|
||||||
|
import { customElement, property } from "lit/decorators.js";
|
||||||
|
|
||||||
|
import type { Flow } from "@goauthentik/api";
|
||||||
|
|
||||||
|
import FlowSearch from "./FlowSearch";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search for flows that may have a fallback specified by the brand settings
|
||||||
|
*
|
||||||
|
* @element ak-branded-flow-search
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
@customElement("ak-branded-flow-search")
|
||||||
|
export class AkBrandedFlowSearch<T extends Flow> extends FlowSearch<T> {
|
||||||
|
/**
|
||||||
|
* The Associated ID of the flow the brand has, to compare if possible
|
||||||
|
*
|
||||||
|
* @attr
|
||||||
|
*/
|
||||||
|
@property({ attribute: false, type: String })
|
||||||
|
brandFlow?: string;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.selected = this.selected.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
selected(flow: Flow): boolean {
|
||||||
|
return super.selected(flow) || flow.pk === this.brandFlow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AkBrandedFlowSearch;
|
|
@ -1,34 +0,0 @@
|
||||||
import { customElement, property } from "lit/decorators.js";
|
|
||||||
|
|
||||||
import type { Flow } from "@goauthentik/api";
|
|
||||||
|
|
||||||
import FlowSearch from "./FlowSearch";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Search for flows that may have a fallback specified by the tenant settings
|
|
||||||
*
|
|
||||||
* @element ak-tenanted-flow-search
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
@customElement("ak-tenanted-flow-search")
|
|
||||||
export class AkTenantedFlowSearch<T extends Flow> extends FlowSearch<T> {
|
|
||||||
/**
|
|
||||||
* The Associated ID of the flow the tenant has, to compare if possible
|
|
||||||
*
|
|
||||||
* @attr
|
|
||||||
*/
|
|
||||||
@property({ attribute: false, type: String })
|
|
||||||
tenantFlow?: string;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
this.selected = this.selected.bind(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
selected(flow: Flow): boolean {
|
|
||||||
return super.selected(flow) || flow.pk === this.tenantFlow;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default AkTenantedFlowSearch;
|
|
|
@ -50,7 +50,7 @@ export class EventListPage extends TablePage<Event> {
|
||||||
new TableColumn(msg("User"), "user"),
|
new TableColumn(msg("User"), "user"),
|
||||||
new TableColumn(msg("Creation Date"), "created"),
|
new TableColumn(msg("Creation Date"), "created"),
|
||||||
new TableColumn(msg("Client IP"), "client_ip"),
|
new TableColumn(msg("Client IP"), "client_ip"),
|
||||||
new TableColumn(msg("Tenant"), "tenant_name"),
|
new TableColumn(msg("Brand"), "brand_name"),
|
||||||
new TableColumn(msg("Actions")),
|
new TableColumn(msg("Actions")),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
@ -75,7 +75,7 @@ export class EventListPage extends TablePage<Event> {
|
||||||
html`<div>${item.clientIp || msg("-")}</div>
|
html`<div>${item.clientIp || msg("-")}</div>
|
||||||
|
|
||||||
<small>${EventGeo(item)}</small>`,
|
<small>${EventGeo(item)}</small>`,
|
||||||
html`<span>${item.tenant?.name || msg("-")}</span>`,
|
html`<span>${item.brand?.name || msg("-")}</span>`,
|
||||||
html`<a href="#/events/log/${item.pk}">
|
html`<a href="#/events/log/${item.pk}">
|
||||||
<pf-tooltip position="top" content=${msg("Show details")}>
|
<pf-tooltip position="top" content=${msg("Show details")}>
|
||||||
<i class="fas fa-share-square"></i>
|
<i class="fas fa-share-square"></i>
|
||||||
|
|
|
@ -139,12 +139,12 @@ export class EventViewPage extends AKElement {
|
||||||
<div class="pf-c-description-list__group">
|
<div class="pf-c-description-list__group">
|
||||||
<dt class="pf-c-description-list__term">
|
<dt class="pf-c-description-list__term">
|
||||||
<span class="pf-c-description-list__text"
|
<span class="pf-c-description-list__text"
|
||||||
>${msg("Tenant")}</span
|
>${msg("Brand")}</span
|
||||||
>
|
>
|
||||||
</dt>
|
</dt>
|
||||||
<dd class="pf-c-description-list__description">
|
<dd class="pf-c-description-list__description">
|
||||||
<div class="pf-c-description-list__text">
|
<div class="pf-c-description-list__text">
|
||||||
${this.event.tenant?.name || msg("-")}
|
${this.event.brand?.name || msg("-")}
|
||||||
</div>
|
</div>
|
||||||
</dd>
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -293,7 +293,7 @@ export class RelatedUserList extends Table<User> {
|
||||||
${msg("Set password")}
|
${msg("Set password")}
|
||||||
</button>
|
</button>
|
||||||
</ak-forms-modal>
|
</ak-forms-modal>
|
||||||
${rootInterface()?.tenant?.flowRecovery
|
${rootInterface()?.brand?.flowRecovery
|
||||||
? html`
|
? html`
|
||||||
<ak-action-button
|
<ak-action-button
|
||||||
class="pf-m-secondary"
|
class="pf-m-secondary"
|
||||||
|
@ -355,7 +355,7 @@ export class RelatedUserList extends Table<User> {
|
||||||
`
|
`
|
||||||
: html` <p>
|
: html` <p>
|
||||||
${msg(
|
${msg(
|
||||||
"To let a user directly reset a their password, configure a recovery flow on the currently active tenant.",
|
"To let a user directly reset a their password, configure a recovery flow on the currently active brand.",
|
||||||
)}
|
)}
|
||||||
</p>`}
|
</p>`}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import "@goauthentik/admin/common/ak-crypto-certificate-search";
|
import "@goauthentik/admin/common/ak-crypto-certificate-search";
|
||||||
import "@goauthentik/admin/common/ak-flow-search/ak-tenanted-flow-search";
|
import "@goauthentik/admin/common/ak-flow-search/ak-branded-flow-search";
|
||||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||||
import { first } from "@goauthentik/common/utils";
|
import { first } from "@goauthentik/common/utils";
|
||||||
import { rootInterface } from "@goauthentik/elements/Base";
|
import { rootInterface } from "@goauthentik/elements/Base";
|
||||||
|
@ -56,7 +56,7 @@ export class LDAPProviderFormPage extends ModelForm<LDAPProvider, number> {
|
||||||
// All Provider objects have an Authorization flow, but not all providers have an Authentication
|
// All Provider objects have an Authorization flow, but not all providers have an Authentication
|
||||||
// flow. LDAP needs only one field, but it is not an Authorization field, it is an
|
// flow. LDAP needs only one field, but it is not an Authorization field, it is an
|
||||||
// Authentication field. So, yeah, we're using the authorization field to store the
|
// Authentication field. So, yeah, we're using the authorization field to store the
|
||||||
// authentication information, which is why the ak-tenanted-flow-search call down there looks so
|
// authentication information, which is why the ak-branded-flow-search call down there looks so
|
||||||
// weird-- we're looking up Authentication flows, but we're storing them in the Authorization
|
// weird-- we're looking up Authentication flows, but we're storing them in the Authorization
|
||||||
// field of the target Provider.
|
// field of the target Provider.
|
||||||
renderForm(): TemplateResult {
|
renderForm(): TemplateResult {
|
||||||
|
@ -73,12 +73,12 @@ export class LDAPProviderFormPage extends ModelForm<LDAPProvider, number> {
|
||||||
?required=${true}
|
?required=${true}
|
||||||
name="authorizationFlow"
|
name="authorizationFlow"
|
||||||
>
|
>
|
||||||
<ak-tenanted-flow-search
|
<ak-branded-flow-search
|
||||||
flowType=${FlowsInstancesListDesignationEnum.Authentication}
|
flowType=${FlowsInstancesListDesignationEnum.Authentication}
|
||||||
.currentFlow=${this.instance?.authorizationFlow}
|
.currentFlow=${this.instance?.authorizationFlow}
|
||||||
.tenantFlow=${rootInterface()?.tenant?.flowAuthentication}
|
.brandFlow=${rootInterface()?.brand?.flowAuthentication}
|
||||||
required
|
required
|
||||||
></ak-tenanted-flow-search>
|
></ak-branded-flow-search>
|
||||||
<p class="pf-c-form__helper-text">${msg("Flow used for users to authenticate.")}</p>
|
<p class="pf-c-form__helper-text">${msg("Flow used for users to authenticate.")}</p>
|
||||||
</ak-form-element-horizontal>
|
</ak-form-element-horizontal>
|
||||||
<ak-form-element-horizontal label=${msg("Search group")} name="searchGroup">
|
<ak-form-element-horizontal label=${msg("Search group")} name="searchGroup">
|
||||||
|
|
|
@ -45,7 +45,7 @@ export class RadiusProviderFormPage extends ModelForm<RadiusProvider, number> {
|
||||||
// All Provider objects have an Authorization flow, but not all providers have an Authentication
|
// All Provider objects have an Authorization flow, but not all providers have an Authentication
|
||||||
// flow. Radius needs only one field, but it is not the Authorization field, it is an
|
// flow. Radius needs only one field, but it is not the Authorization field, it is an
|
||||||
// Authentication field. So, yeah, we're using the authorization field to store the
|
// Authentication field. So, yeah, we're using the authorization field to store the
|
||||||
// authentication information, which is why the ak-tenanted-flow-search call down there looks so
|
// authentication information, which is why the ak-branded-flow-search call down there looks so
|
||||||
// weird-- we're looking up Authentication flows, but we're storing them in the Authorization
|
// weird-- we're looking up Authentication flows, but we're storing them in the Authorization
|
||||||
// field of the target Provider.
|
// field of the target Provider.
|
||||||
renderForm(): TemplateResult {
|
renderForm(): TemplateResult {
|
||||||
|
@ -62,12 +62,12 @@ export class RadiusProviderFormPage extends ModelForm<RadiusProvider, number> {
|
||||||
?required=${true}
|
?required=${true}
|
||||||
name="authorizationFlow"
|
name="authorizationFlow"
|
||||||
>
|
>
|
||||||
<ak-tenanted-flow-search
|
<ak-branded-flow-search
|
||||||
flowType=${FlowsInstancesListDesignationEnum.Authentication}
|
flowType=${FlowsInstancesListDesignationEnum.Authentication}
|
||||||
.currentFlow=${this.instance?.authorizationFlow}
|
.currentFlow=${this.instance?.authorizationFlow}
|
||||||
.tenantFlow=${rootInterface()?.tenant?.flowAuthentication}
|
.brandFlow=${rootInterface()?.brand?.flowAuthentication}
|
||||||
required
|
required
|
||||||
></ak-tenanted-flow-search>
|
></ak-branded-flow-search>
|
||||||
<p class="pf-c-form__helper-text">${msg("Flow used for users to authenticate.")}</p>
|
<p class="pf-c-form__helper-text">${msg("Flow used for users to authenticate.")}</p>
|
||||||
</ak-form-element-horizontal>
|
</ak-form-element-horizontal>
|
||||||
<ak-form-element-horizontal name="mfaSupport">
|
<ak-form-element-horizontal name="mfaSupport">
|
||||||
|
|
|
@ -28,7 +28,7 @@ class PreviewStageHost implements StageHost {
|
||||||
challenge = undefined;
|
challenge = undefined;
|
||||||
flowSlug = undefined;
|
flowSlug = undefined;
|
||||||
loading = false;
|
loading = false;
|
||||||
tenant = undefined;
|
brand = undefined;
|
||||||
async submit(payload: unknown): Promise<boolean> {
|
async submit(payload: unknown): Promise<boolean> {
|
||||||
this.promptForm.previewResult = payload;
|
this.promptForm.previewResult = payload;
|
||||||
return false;
|
return false;
|
||||||
|
|
|
@ -63,7 +63,7 @@ export const requestRecoveryLink = (user: User) =>
|
||||||
showMessage({
|
showMessage({
|
||||||
level: MessageLevel.error,
|
level: MessageLevel.error,
|
||||||
message: msg(
|
message: msg(
|
||||||
"The current tenant must have a recovery flow configured to use a recovery link",
|
"The current brand must have a recovery flow configured to use a recovery link",
|
||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
|
@ -355,7 +355,7 @@ export class UserListPage extends TablePage<User> {
|
||||||
${msg("Set password")}
|
${msg("Set password")}
|
||||||
</button>
|
</button>
|
||||||
</ak-forms-modal>
|
</ak-forms-modal>
|
||||||
${rootInterface()?.tenant?.flowRecovery
|
${rootInterface()?.brand?.flowRecovery
|
||||||
? html`
|
? html`
|
||||||
<ak-action-button
|
<ak-action-button
|
||||||
class="pf-m-secondary"
|
class="pf-m-secondary"
|
||||||
|
@ -373,7 +373,7 @@ export class UserListPage extends TablePage<User> {
|
||||||
`
|
`
|
||||||
: html` <p>
|
: html` <p>
|
||||||
${msg(
|
${msg(
|
||||||
"To let a user directly reset a their password, configure a recovery flow on the currently active tenant.",
|
"To let a user directly reset a their password, configure a recovery flow on the currently active brand.",
|
||||||
)}
|
)}
|
||||||
</p>`}
|
</p>`}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -6,7 +6,7 @@ import {
|
||||||
import { EVENT_LOCALE_REQUEST, EVENT_REFRESH, VERSION } from "@goauthentik/common/constants";
|
import { EVENT_LOCALE_REQUEST, EVENT_REFRESH, VERSION } from "@goauthentik/common/constants";
|
||||||
import { globalAK } from "@goauthentik/common/global";
|
import { globalAK } from "@goauthentik/common/global";
|
||||||
|
|
||||||
import { Config, Configuration, CoreApi, CurrentTenant, RootApi } from "@goauthentik/api";
|
import { Config, Configuration, CoreApi, CurrentBrand, RootApi } from "@goauthentik/api";
|
||||||
|
|
||||||
let globalConfigPromise: Promise<Config> | undefined = Promise.resolve(globalAK().config);
|
let globalConfigPromise: Promise<Config> | undefined = Promise.resolve(globalAK().config);
|
||||||
export function config(): Promise<Config> {
|
export function config(): Promise<Config> {
|
||||||
|
@ -16,7 +16,7 @@ export function config(): Promise<Config> {
|
||||||
return globalConfigPromise;
|
return globalConfigPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function tenantSetFavicon(tenant: CurrentTenant) {
|
export function brandSetFavicon(brand: CurrentBrand) {
|
||||||
/**
|
/**
|
||||||
* <link rel="icon" href="/static/dist/assets/icons/icon.png">
|
* <link rel="icon" href="/static/dist/assets/icons/icon.png">
|
||||||
* <link rel="shortcut icon" href="/static/dist/assets/icons/icon.png">
|
* <link rel="shortcut icon" href="/static/dist/assets/icons/icon.png">
|
||||||
|
@ -29,36 +29,36 @@ export function tenantSetFavicon(tenant: CurrentTenant) {
|
||||||
relIcon.rel = rel;
|
relIcon.rel = rel;
|
||||||
document.getElementsByTagName("head")[0].appendChild(relIcon);
|
document.getElementsByTagName("head")[0].appendChild(relIcon);
|
||||||
}
|
}
|
||||||
relIcon.href = tenant.brandingFavicon;
|
relIcon.href = brand.brandingFavicon;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function tenantSetLocale(tenant: CurrentTenant) {
|
export function brandSetLocale(brand: CurrentBrand) {
|
||||||
if (tenant.defaultLocale === "") {
|
if (brand.defaultLocale === "") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
console.debug("authentik/locale: setting locale from tenant default");
|
console.debug("authentik/locale: setting locale from brand default");
|
||||||
window.dispatchEvent(
|
window.dispatchEvent(
|
||||||
new CustomEvent(EVENT_LOCALE_REQUEST, {
|
new CustomEvent(EVENT_LOCALE_REQUEST, {
|
||||||
composed: true,
|
composed: true,
|
||||||
bubbles: true,
|
bubbles: true,
|
||||||
detail: { locale: tenant.defaultLocale },
|
detail: { locale: brand.defaultLocale },
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let globalTenantPromise: Promise<CurrentTenant> | undefined = Promise.resolve(globalAK().tenant);
|
let globalBrandPromise: Promise<CurrentBrand> | undefined = Promise.resolve(globalAK().brand);
|
||||||
export function tenant(): Promise<CurrentTenant> {
|
export function brand(): Promise<CurrentBrand> {
|
||||||
if (!globalTenantPromise) {
|
if (!globalBrandPromise) {
|
||||||
globalTenantPromise = new CoreApi(DEFAULT_CONFIG)
|
globalBrandPromise = new CoreApi(DEFAULT_CONFIG)
|
||||||
.coreTenantsCurrentRetrieve()
|
.coreBrandsCurrentRetrieve()
|
||||||
.then((tenant) => {
|
.then((brand) => {
|
||||||
tenantSetFavicon(tenant);
|
brandSetFavicon(brand);
|
||||||
tenantSetLocale(tenant);
|
brandSetLocale(brand);
|
||||||
return tenant;
|
return brand;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return globalTenantPromise;
|
return globalBrandPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getMetaContent(key: string): string {
|
export function getMetaContent(key: string): string {
|
||||||
|
@ -75,7 +75,7 @@ export const DEFAULT_CONFIG = new Configuration({
|
||||||
middleware: [
|
middleware: [
|
||||||
new CSRFMiddleware(),
|
new CSRFMiddleware(),
|
||||||
new EventMiddleware(),
|
new EventMiddleware(),
|
||||||
new LoggingMiddleware(globalAK().tenant),
|
new LoggingMiddleware(globalAK().brand),
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -90,9 +90,9 @@ window.addEventListener(EVENT_REFRESH, () => {
|
||||||
// Upon global refresh, disregard whatever was pre-hydrated and
|
// Upon global refresh, disregard whatever was pre-hydrated and
|
||||||
// actually load info from API
|
// actually load info from API
|
||||||
globalConfigPromise = undefined;
|
globalConfigPromise = undefined;
|
||||||
globalTenantPromise = undefined;
|
globalBrandPromise = undefined;
|
||||||
config();
|
config();
|
||||||
tenant();
|
brand();
|
||||||
});
|
});
|
||||||
|
|
||||||
console.debug(`authentik(early): version ${VERSION}, apiBase ${DEFAULT_CONFIG.basePath}`);
|
console.debug(`authentik(early): version ${VERSION}, apiBase ${DEFAULT_CONFIG.basePath}`);
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { EVENT_REQUEST_POST } from "@goauthentik/common/constants";
|
||||||
import { getCookie } from "@goauthentik/common/utils";
|
import { getCookie } from "@goauthentik/common/utils";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
CurrentTenant,
|
CurrentBrand,
|
||||||
FetchParams,
|
FetchParams,
|
||||||
Middleware,
|
Middleware,
|
||||||
RequestContext,
|
RequestContext,
|
||||||
|
@ -18,13 +18,13 @@ export interface RequestInfo {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class LoggingMiddleware implements Middleware {
|
export class LoggingMiddleware implements Middleware {
|
||||||
tenant: CurrentTenant;
|
brand: CurrentBrand;
|
||||||
constructor(tenant: CurrentTenant) {
|
constructor(brand: CurrentBrand) {
|
||||||
this.tenant = tenant;
|
this.brand = brand;
|
||||||
}
|
}
|
||||||
|
|
||||||
post(context: ResponseContext): Promise<Response | void> {
|
post(context: ResponseContext): Promise<Response | void> {
|
||||||
let msg = `authentik/api[${this.tenant.matchedDomain}]: `;
|
let msg = `authentik/api[${this.brand.matchedDomain}]: `;
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/API/console#styling_console_output
|
// https://developer.mozilla.org/en-US/docs/Web/API/console#styling_console_output
|
||||||
msg += `%c${context.response.status}%c ${context.init.method} ${context.url}`;
|
msg += `%c${context.response.status}%c ${context.init.method} ${context.url}`;
|
||||||
let style = "";
|
let style = "";
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue