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_is_secure = SerializerMethodField()
|
||||
runtime = SerializerMethodField()
|
||||
tenant = SerializerMethodField()
|
||||
brand = SerializerMethodField()
|
||||
server_time = SerializerMethodField()
|
||||
embedded_outpost_host = SerializerMethodField()
|
||||
|
||||
|
@ -69,9 +69,9 @@ class SystemInfoSerializer(PassiveSerializer):
|
|||
"uname": " ".join(platform.uname()),
|
||||
}
|
||||
|
||||
def get_tenant(self, request: Request) -> str:
|
||||
"""Currently active tenant"""
|
||||
return str(request._request.tenant)
|
||||
def get_brand(self, request: Request) -> str:
|
||||
"""Currently active brand"""
|
||||
return str(request._request.brand)
|
||||
|
||||
def get_server_time(self, request: Request) -> datetime:
|
||||
"""Current server time"""
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
{% load static %}
|
||||
|
||||
{% block title %}
|
||||
API Browser - {{ tenant.branding_title }}
|
||||
API Browser - {{ brand.branding_title }}
|
||||
{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
|
|
|
@ -62,7 +62,7 @@ class ConfigView(APIView):
|
|||
|
||||
permission_classes = [AllowAny]
|
||||
|
||||
def get_capabilities(self) -> list[Capabilities]:
|
||||
def get_capabilities(self, request: Request) -> list[Capabilities]:
|
||||
"""Get all capabilities this server instance supports"""
|
||||
caps = []
|
||||
deb_test = settings.DEBUG or settings.TEST
|
||||
|
@ -70,7 +70,7 @@ class ConfigView(APIView):
|
|||
caps.append(Capabilities.CAN_SAVE_MEDIA)
|
||||
if GEOIP_READER.enabled:
|
||||
caps.append(Capabilities.CAN_GEO_IP)
|
||||
if CONFIG.get_bool("impersonation"):
|
||||
if request.tenant.impersonation:
|
||||
caps.append(Capabilities.CAN_IMPERSONATE)
|
||||
if settings.DEBUG: # pragma: no cover
|
||||
caps.append(Capabilities.CAN_DEBUG)
|
||||
|
@ -81,7 +81,7 @@ class ConfigView(APIView):
|
|||
caps.append(result)
|
||||
return caps
|
||||
|
||||
def get_config(self) -> ConfigSerializer:
|
||||
def get_config(self, request: Request) -> ConfigSerializer:
|
||||
"""Get Config"""
|
||||
return ConfigSerializer(
|
||||
{
|
||||
|
@ -92,7 +92,7 @@ class ConfigView(APIView):
|
|||
"send_pii": CONFIG.get("error_reporting.send_pii"),
|
||||
"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_flows": CONFIG.get_int("cache.timeout_flows"),
|
||||
"cache_timeout_policies": CONFIG.get_int("cache.timeout_policies"),
|
||||
|
@ -103,4 +103,4 @@ class ConfigView(APIView):
|
|||
@extend_schema(responses={200: ConfigSerializer(many=False)})
|
||||
def get(self, request: Request) -> Response:
|
||||
"""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.tests import apply_blueprint
|
||||
from authentik.blueprints.v1.importer import Importer
|
||||
from authentik.tenants.models import Tenant
|
||||
from authentik.brands.models import Brand
|
||||
|
||||
|
||||
class TestPackaged(TransactionTestCase):
|
||||
"""Empty class, test methods are added dynamically"""
|
||||
|
||||
@apply_blueprint("default/default-tenant.yaml")
|
||||
@apply_blueprint("default/default-brand.yaml")
|
||||
def test_decorator_static(self):
|
||||
"""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:
|
||||
|
|
|
@ -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):
|
||||
replaces = [
|
||||
("authentik_tenants", "0001_initial"),
|
||||
("authentik_tenants", "0002_default"),
|
||||
("authentik_tenants", "0003_tenant_branding_favicon"),
|
||||
("authentik_tenants", "0004_tenant_event_retention"),
|
||||
("authentik_tenants", "0005_tenant_web_certificate"),
|
||||
("authentik_brands", "0001_initial"),
|
||||
("authentik_brands", "0002_default"),
|
||||
("authentik_brands", "0003_tenant_branding_favicon"),
|
||||
("authentik_brands", "0004_tenant_event_retention"),
|
||||
("authentik_brands", "0005_tenant_web_certificate"),
|
||||
]
|
||||
|
||||
initial = True
|
||||
|
@ -25,10 +25,10 @@ class Migration(migrations.Migration):
|
|||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="Tenant",
|
||||
name="Brand",
|
||||
fields=[
|
||||
(
|
||||
"tenant_uuid",
|
||||
"brand_uuid",
|
||||
models.UUIDField(
|
||||
default=uuid.uuid4, editable=False, primary_key=True, serialize=False
|
||||
),
|
||||
|
@ -37,7 +37,7 @@ class Migration(migrations.Migration):
|
|||
"domain",
|
||||
models.TextField(
|
||||
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`"
|
||||
)
|
||||
),
|
||||
|
@ -53,7 +53,7 @@ class Migration(migrations.Migration):
|
|||
models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="tenant_authentication",
|
||||
related_name="brand_authentication",
|
||||
to="authentik_flows.flow",
|
||||
),
|
||||
),
|
||||
|
@ -62,7 +62,7 @@ class Migration(migrations.Migration):
|
|||
models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="tenant_invalidation",
|
||||
related_name="brand_invalidation",
|
||||
to="authentik_flows.flow",
|
||||
),
|
||||
),
|
||||
|
@ -71,7 +71,7 @@ class Migration(migrations.Migration):
|
|||
models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="tenant_recovery",
|
||||
related_name="brand_recovery",
|
||||
to="authentik_flows.flow",
|
||||
),
|
||||
),
|
||||
|
@ -80,23 +80,23 @@ class Migration(migrations.Migration):
|
|||
models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="tenant_unenrollment",
|
||||
related_name="brand_unenrollment",
|
||||
to="authentik_flows.flow",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "Tenant",
|
||||
"verbose_name_plural": "Tenants",
|
||||
"verbose_name": "Brand",
|
||||
"verbose_name_plural": "Brands",
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="tenant",
|
||||
model_name="brand",
|
||||
name="branding_favicon",
|
||||
field=models.TextField(default="/static/dist/assets/icons/icon.png"),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="tenant",
|
||||
model_name="brand",
|
||||
name="event_retention",
|
||||
field=models.TextField(
|
||||
default="days=365",
|
||||
|
@ -108,7 +108,7 @@ class Migration(migrations.Migration):
|
|||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="tenant",
|
||||
model_name="brand",
|
||||
name="web_certificate",
|
||||
field=models.ForeignKey(
|
||||
default=None,
|
|
@ -8,17 +8,17 @@ class Migration(migrations.Migration):
|
|||
dependencies = [
|
||||
("authentik_stages_prompt", "0007_prompt_placeholder_expression"),
|
||||
("authentik_flows", "0021_auto_20211227_2103"),
|
||||
("authentik_tenants", "0001_squashed_0005_tenant_web_certificate"),
|
||||
("authentik_brands", "0001_squashed_0005_tenant_web_certificate"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="tenant",
|
||||
model_name="brand",
|
||||
name="flow_user_settings",
|
||||
field=models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="tenant_user_settings",
|
||||
related_name="brand_user_settings",
|
||||
to="authentik_flows.flow",
|
||||
),
|
||||
),
|
|
@ -5,12 +5,12 @@ from django.db import migrations, models
|
|||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("authentik_tenants", "0002_tenant_flow_user_settings"),
|
||||
("authentik_brands", "0002_tenant_flow_user_settings"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="tenant",
|
||||
model_name="brand",
|
||||
name="attributes",
|
||||
field=models.JSONField(blank=True, default=dict),
|
||||
),
|
|
@ -7,17 +7,17 @@ from django.db import migrations, models
|
|||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("authentik_flows", "0023_flow_denied_action"),
|
||||
("authentik_tenants", "0003_tenant_attributes"),
|
||||
("authentik_brands", "0003_tenant_attributes"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="tenant",
|
||||
model_name="brand",
|
||||
name="flow_device_code",
|
||||
field=models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="tenant_device_code",
|
||||
related_name="brand_device_code",
|
||||
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.urls import reverse
|
||||
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.lib.config import CONFIG
|
||||
from authentik.lib.utils.time import timedelta_from_string
|
||||
from authentik.tenants.api import Themes
|
||||
from authentik.tenants.models import Tenant
|
||||
|
||||
|
||||
class TestTenants(APITestCase):
|
||||
"""Test tenants"""
|
||||
class TestBrands(APITestCase):
|
||||
"""Test brands"""
|
||||
|
||||
def test_current_tenant(self):
|
||||
"""Test Current tenant API"""
|
||||
tenant = create_test_tenant()
|
||||
def test_current_brand(self):
|
||||
"""Test Current brand API"""
|
||||
brand = create_test_brand()
|
||||
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_favicon": "/static/dist/assets/icons/icon.png",
|
||||
"branding_title": "authentik",
|
||||
"matched_domain": tenant.domain,
|
||||
"ui_footer_links": CONFIG.get("footer_links"),
|
||||
"matched_domain": brand.domain,
|
||||
"ui_footer_links": [],
|
||||
"ui_theme": Themes.AUTOMATIC,
|
||||
"default_locale": "",
|
||||
},
|
||||
)
|
||||
|
||||
def test_tenant_subdomain(self):
|
||||
"""Test Current tenant API"""
|
||||
Tenant.objects.all().delete()
|
||||
Tenant.objects.create(domain="bar.baz", branding_title="custom")
|
||||
def test_brand_subdomain(self):
|
||||
"""Test Current brand API"""
|
||||
Brand.objects.all().delete()
|
||||
Brand.objects.create(domain="bar.baz", branding_title="custom")
|
||||
self.assertJSONEqual(
|
||||
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(),
|
||||
{
|
||||
"branding_logo": "/static/dist/assets/icons/icon_left_brand.svg",
|
||||
"branding_favicon": "/static/dist/assets/icons/icon.png",
|
||||
"branding_title": "custom",
|
||||
"matched_domain": "bar.baz",
|
||||
"ui_footer_links": CONFIG.get("footer_links"),
|
||||
"ui_footer_links": [],
|
||||
"ui_theme": Themes.AUTOMATIC,
|
||||
"default_locale": "",
|
||||
},
|
||||
)
|
||||
|
||||
def test_fallback(self):
|
||||
"""Test fallback tenant"""
|
||||
Tenant.objects.all().delete()
|
||||
"""Test fallback brand"""
|
||||
Brand.objects.all().delete()
|
||||
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_favicon": "/static/dist/assets/icons/icon.png",
|
||||
"branding_title": "authentik",
|
||||
"matched_domain": "fallback",
|
||||
"ui_footer_links": CONFIG.get("footer_links"),
|
||||
"ui_footer_links": [],
|
||||
"ui_theme": Themes.AUTOMATIC,
|
||||
"default_locale": "",
|
||||
},
|
||||
)
|
||||
|
||||
def test_event_retention(self):
|
||||
"""Test tenant's event retention"""
|
||||
tenant = Tenant.objects.create(
|
||||
"""Test brand's event retention"""
|
||||
brand = Brand.objects.create(
|
||||
domain="foo",
|
||||
default=True,
|
||||
branding_title="custom",
|
||||
|
@ -75,7 +75,7 @@ class TestTenants(APITestCase):
|
|||
)
|
||||
factory = RequestFactory()
|
||||
request = factory.get("/")
|
||||
request.tenant = tenant
|
||||
request.brand = brand
|
||||
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(
|
||||
|
@ -87,8 +87,8 @@ class TestTenants(APITestCase):
|
|||
)
|
||||
|
||||
def test_create_default_multiple(self):
|
||||
"""Test attempted creation of multiple default tenants"""
|
||||
Tenant.objects.create(
|
||||
"""Test attempted creation of multiple default brands"""
|
||||
Brand.objects.create(
|
||||
domain="foo",
|
||||
default=True,
|
||||
branding_title="custom",
|
||||
|
@ -97,6 +97,6 @@ class TestTenants(APITestCase):
|
|||
user = create_test_admin_user()
|
||||
self.client.force_login(user)
|
||||
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)
|
|
@ -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.api.decorators import permission_required
|
||||
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.utils import LinkSerializer, PassiveSerializer, is_dict
|
||||
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.tasks import send_mails
|
||||
from authentik.stages.email.utils import TemplateEmailMessage
|
||||
from authentik.tenants.models import Tenant
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
|
@ -227,7 +227,7 @@ class UserSelfSerializer(ModelSerializer):
|
|||
}
|
||||
|
||||
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", {})
|
||||
|
||||
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)
|
||||
|
||||
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"""
|
||||
tenant: Tenant = self.request._request.tenant
|
||||
brand: Brand = self.request._request.brand
|
||||
# Check that there is a recovery flow, if not return an error
|
||||
flow = tenant.flow_recovery
|
||||
flow = brand.flow_recovery
|
||||
if not flow:
|
||||
LOGGER.debug("No recovery flow set")
|
||||
return None, None
|
||||
|
@ -624,7 +624,7 @@ class UserViewSet(UsedByMixin, ModelViewSet):
|
|||
@action(detail=True, methods=["POST"])
|
||||
def impersonate(self, request: Request, pk: int) -> Response:
|
||||
"""Impersonate a user"""
|
||||
if not CONFIG.get_bool("impersonation"):
|
||||
if not request.tenant.impersonation:
|
||||
LOGGER.debug("User attempted to impersonate", user=request.user)
|
||||
return Response(status=401)
|
||||
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,
|
||||
including the users attributes"""
|
||||
final_attributes = {}
|
||||
if request and hasattr(request, "tenant"):
|
||||
always_merger.merge(final_attributes, request.tenant.attributes)
|
||||
if request and hasattr(request, "brand"):
|
||||
always_merger.merge(final_attributes, request.brand.attributes)
|
||||
for group in self.all_groups().order_by("name"):
|
||||
always_merger.merge(final_attributes, group.attributes)
|
||||
always_merger.merge(final_attributes, self.attributes)
|
||||
|
@ -262,7 +262,7 @@ class User(SerializerModel, GuardianUserMixin, AbstractUser):
|
|||
except Exception as exc:
|
||||
LOGGER.warning("Failed to get default locale", exc=exc)
|
||||
if request:
|
||||
return request.tenant.locale
|
||||
return request.brand.locale
|
||||
return ""
|
||||
|
||||
@property
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
window.authentik = {
|
||||
locale: "{{ LANGUAGE_CODE }}",
|
||||
config: JSON.parse('{{ config_json|escapejs }}'),
|
||||
tenant: JSON.parse('{{ tenant_json|escapejs }}'),
|
||||
brand: JSON.parse('{{ brand_json|escapejs }}'),
|
||||
versionFamily: "{{ version_family }}",
|
||||
versionSubdomain: "{{ version_subdomain }}",
|
||||
build: "{{ build }}",
|
||||
|
|
|
@ -7,9 +7,9 @@
|
|||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
|
||||
<title>{% block title %}{% trans title|default:tenant.branding_title %}{% endblock %}</title>
|
||||
<link rel="icon" href="{{ tenant.branding_favicon }}">
|
||||
<link rel="shortcut icon" href="{{ tenant.branding_favicon }}">
|
||||
<title>{% block title %}{% trans title|default:brand.branding_title %}{% endblock %}</title>
|
||||
<link rel="icon" href="{{ brand.branding_favicon }}">
|
||||
<link rel="shortcut icon" href="{{ brand.branding_favicon }}">
|
||||
{% block head_before %}
|
||||
{% endblock %}
|
||||
<link rel="stylesheet" type="text/css" href="{% static 'dist/authentik.css' %}">
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
{% load i18n %}
|
||||
|
||||
{% block title %}
|
||||
{% trans 'End session' %} - {{ tenant.branding_title }}
|
||||
{% trans 'End session' %} - {{ brand.branding_title }}
|
||||
{% endblock %}
|
||||
|
||||
{% block card_title %}
|
||||
|
@ -16,7 +16,7 @@ You've logged out of {{ application }}.
|
|||
{% block card %}
|
||||
<form method="POST" class="pf-c-form">
|
||||
<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.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
|
@ -26,7 +26,7 @@ You've logged out of {{ application }}.
|
|||
</a>
|
||||
|
||||
<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 }}
|
||||
{% endblocktrans %}
|
||||
</a>
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
{% load i18n %}
|
||||
|
||||
{% block title %}
|
||||
{{ tenant.branding_title }}
|
||||
{{ brand.branding_title }}
|
||||
{% endblock %}
|
||||
|
||||
{% block card_title %}
|
||||
|
|
|
@ -60,7 +60,7 @@
|
|||
<div class="ak-login-container">
|
||||
<header class="pf-c-login__header">
|
||||
<div class="pf-c-brand ak-brand">
|
||||
<img src="{{ tenant.branding_logo }}" alt="authentik Logo" />
|
||||
<img src="{{ brand.branding_logo }}" alt="authentik Logo" />
|
||||
</div>
|
||||
</header>
|
||||
{% block main_container %}
|
||||
|
|
|
@ -3,10 +3,10 @@ from unittest.mock import MagicMock, patch
|
|||
|
||||
from django.urls import reverse
|
||||
|
||||
from authentik.brands.models import Brand
|
||||
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.tenants.models import Tenant
|
||||
|
||||
|
||||
class TestApplicationsViews(FlowTestCase):
|
||||
|
@ -21,9 +21,9 @@ class TestApplicationsViews(FlowTestCase):
|
|||
def test_check_redirect(self):
|
||||
"""Test redirect"""
|
||||
empty_flow = create_test_flow()
|
||||
tenant: Tenant = create_test_tenant()
|
||||
tenant.flow_authentication = empty_flow
|
||||
tenant.save()
|
||||
brand: Brand = create_test_brand()
|
||||
brand.flow_authentication = empty_flow
|
||||
brand.save()
|
||||
response = self.client.get(
|
||||
reverse(
|
||||
"authentik_core:application-launch",
|
||||
|
@ -45,9 +45,9 @@ class TestApplicationsViews(FlowTestCase):
|
|||
"""Test redirect"""
|
||||
self.client.force_login(self.user)
|
||||
empty_flow = create_test_flow()
|
||||
tenant: Tenant = create_test_tenant()
|
||||
tenant.flow_authentication = empty_flow
|
||||
tenant.save()
|
||||
brand: Brand = create_test_brand()
|
||||
brand.flow_authentication = empty_flow
|
||||
brand.save()
|
||||
response = self.client.get(
|
||||
reverse(
|
||||
"authentik_core:application-launch",
|
||||
|
|
|
@ -7,6 +7,7 @@ from django.core.cache import cache
|
|||
from django.urls.base import reverse
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
from authentik.brands.models import Brand
|
||||
from authentik.core.models import (
|
||||
USER_ATTRIBUTE_TOKEN_EXPIRING,
|
||||
AuthenticatedSession,
|
||||
|
@ -14,11 +15,10 @@ from authentik.core.models import (
|
|||
User,
|
||||
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.lib.generators import generate_id, generate_key
|
||||
from authentik.stages.email.models import EmailStage
|
||||
from authentik.tenants.models import Tenant
|
||||
|
||||
|
||||
class TestUsersAPI(APITestCase):
|
||||
|
@ -80,9 +80,9 @@ class TestUsersAPI(APITestCase):
|
|||
def test_recovery(self):
|
||||
"""Test user recovery link (no recovery flow set)"""
|
||||
flow = create_test_flow(FlowDesignation.RECOVERY)
|
||||
tenant: Tenant = create_test_tenant()
|
||||
tenant.flow_recovery = flow
|
||||
tenant.save()
|
||||
brand: Brand = create_test_brand()
|
||||
brand.flow_recovery = flow
|
||||
brand.save()
|
||||
self.client.force_login(self.admin)
|
||||
response = self.client.get(
|
||||
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.save()
|
||||
flow = create_test_flow(designation=FlowDesignation.RECOVERY)
|
||||
tenant: Tenant = create_test_tenant()
|
||||
tenant.flow_recovery = flow
|
||||
tenant.save()
|
||||
brand: Brand = create_test_brand()
|
||||
brand.flow_recovery = flow
|
||||
brand.save()
|
||||
self.client.force_login(self.admin)
|
||||
response = self.client.get(
|
||||
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.save()
|
||||
flow = create_test_flow(FlowDesignation.RECOVERY)
|
||||
tenant: Tenant = create_test_tenant()
|
||||
tenant.flow_recovery = flow
|
||||
tenant.save()
|
||||
brand: Brand = create_test_brand()
|
||||
brand.flow_recovery = flow
|
||||
brand.save()
|
||||
|
||||
stage = EmailStage.objects.create(name="email")
|
||||
|
||||
|
|
|
@ -3,12 +3,12 @@ from typing import Optional
|
|||
|
||||
from django.utils.text import slugify
|
||||
|
||||
from authentik.brands.models import Brand
|
||||
from authentik.core.models import Group, User
|
||||
from authentik.crypto.builder import CertificateBuilder
|
||||
from authentik.crypto.models import CertificateKeyPair
|
||||
from authentik.flows.models import Flow, FlowDesignation
|
||||
from authentik.lib.generators import generate_id
|
||||
from authentik.tenants.models import Tenant
|
||||
|
||||
|
||||
def create_test_flow(
|
||||
|
@ -43,12 +43,12 @@ def create_test_admin_user(name: Optional[str] = None, **kwargs) -> User:
|
|||
return user
|
||||
|
||||
|
||||
def create_test_tenant(**kwargs) -> Tenant:
|
||||
"""Generate a test tenant, removing all other tenants to make sure this one
|
||||
def create_test_brand(**kwargs) -> Brand:
|
||||
"""Generate a test brand, removing all other brands to make sure this one
|
||||
matches."""
|
||||
uid = generate_id(20)
|
||||
Tenant.objects.all().delete()
|
||||
return Tenant.objects.create(domain=uid, default=True, **kwargs)
|
||||
Brand.objects.all().delete()
|
||||
return Brand.objects.create(domain=uid, default=True, **kwargs)
|
||||
|
||||
|
||||
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.admin.tasks import LOCAL_VERSION
|
||||
from authentik.api.v3.config import ConfigView
|
||||
from authentik.brands.api import CurrentBrandSerializer
|
||||
from authentik.flows.models import Flow
|
||||
from authentik.tenants.api import CurrentTenantSerializer
|
||||
|
||||
|
||||
class InterfaceView(TemplateView):
|
||||
|
@ -18,7 +18,7 @@ class InterfaceView(TemplateView):
|
|||
|
||||
def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
|
||||
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_subdomain"] = f"version-{LOCAL_VERSION.major}-{LOCAL_VERSION.minor}"
|
||||
kwargs["build"] = get_build_hash()
|
||||
|
|
|
@ -35,7 +35,7 @@ class EventSerializer(ModelSerializer):
|
|||
"client_ip",
|
||||
"created",
|
||||
"expires",
|
||||
"tenant",
|
||||
"brand",
|
||||
]
|
||||
|
||||
|
||||
|
@ -76,10 +76,10 @@ class EventsFilter(django_filters.FilterSet):
|
|||
field_name="action",
|
||||
lookup_expr="icontains",
|
||||
)
|
||||
tenant_name = django_filters.CharFilter(
|
||||
field_name="tenant",
|
||||
brand_name = django_filters.CharFilter(
|
||||
field_name="brand",
|
||||
lookup_expr="name",
|
||||
label="Tenant name",
|
||||
label="Brand name",
|
||||
)
|
||||
|
||||
def filter_context_model_pk(self, queryset, name, value):
|
||||
|
|
|
@ -305,7 +305,7 @@ class Migration(migrations.Migration):
|
|||
migrations.AddField(
|
||||
model_name="event",
|
||||
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(
|
||||
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 authentik import get_full_version
|
||||
from authentik.brands.models import Brand
|
||||
from authentik.brands.utils import DEFAULT_BRAND
|
||||
from authentik.core.middleware import (
|
||||
SESSION_KEY_IMPERSONATE_ORIGINAL_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.policies.models import PolicyBindingModel
|
||||
from authentik.stages.email.utils import TemplateEmailMessage
|
||||
from authentik.tenants.models import Tenant
|
||||
from authentik.tenants.utils import DEFAULT_TENANT
|
||||
|
||||
LOGGER = get_logger()
|
||||
if TYPE_CHECKING:
|
||||
|
@ -50,13 +50,13 @@ if TYPE_CHECKING:
|
|||
|
||||
def default_event_duration():
|
||||
"""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)
|
||||
|
||||
|
||||
def default_tenant():
|
||||
"""Get a default value for tenant"""
|
||||
return sanitize_dict(model_to_dict(DEFAULT_TENANT))
|
||||
def default_brand():
|
||||
"""Get a default value for brand"""
|
||||
return sanitize_dict(model_to_dict(DEFAULT_BRAND))
|
||||
|
||||
|
||||
class NotificationTransportError(SentryIgnoredException):
|
||||
|
@ -170,7 +170,7 @@ class Event(SerializerModel, ExpiringModel):
|
|||
context = models.JSONField(default=dict, blank=True)
|
||||
client_ip = models.GenericIPAddressField(null=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
|
||||
expires = models.DateTimeField(default=default_event_duration)
|
||||
|
@ -223,13 +223,13 @@ class Event(SerializerModel, ExpiringModel):
|
|||
if QS_QUERY in self.context["http_request"]["args"]:
|
||||
wrapped = self.context["http_request"]["args"][QS_QUERY]
|
||||
self.context["http_request"]["args"] = cleanse_dict(QueryDict(wrapped))
|
||||
if hasattr(request, "tenant"):
|
||||
tenant: Tenant = request.tenant
|
||||
if hasattr(request, "brand"):
|
||||
brand: Brand = request.brand
|
||||
# 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
|
||||
self.created = now()
|
||||
self.expires = self.created + timedelta_from_string(tenant.event_retention)
|
||||
self.tenant = sanitize_dict(model_to_dict(tenant))
|
||||
self.expires = self.created + timedelta_from_string(brand.event_retention)
|
||||
self.brand = sanitize_dict(model_to_dict(brand))
|
||||
if hasattr(request, "user"):
|
||||
original_user = None
|
||||
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.password.stage import PLAN_CONTEXT_METHOD, PLAN_CONTEXT_METHOD_ARGS
|
||||
from authentik.stages.user_write.signals import user_write
|
||||
from authentik.tenants.utils import get_current_tenant
|
||||
|
||||
SESSION_LOGIN_EVENT = "login_event"
|
||||
|
||||
|
@ -93,5 +94,5 @@ def event_post_save_notification(sender, instance: Event, **_):
|
|||
@receiver(pre_delete, sender=User)
|
||||
def event_user_pre_delete_cleanup(sender, instance: User, **_):
|
||||
"""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)
|
||||
|
|
|
@ -6,12 +6,12 @@ from django.test import RequestFactory, TestCase
|
|||
from django.views.debug import SafeExceptionReporterFilter
|
||||
from guardian.shortcuts import get_anonymous_user
|
||||
|
||||
from authentik.brands.models import Brand
|
||||
from authentik.core.models import Group
|
||||
from authentik.events.models import Event
|
||||
from authentik.flows.views.executor import QS_QUERY
|
||||
from authentik.lib.generators import generate_id
|
||||
from authentik.policies.dummy.models import DummyPolicy
|
||||
from authentik.tenants.models import Tenant
|
||||
|
||||
|
||||
class TestEvents(TestCase):
|
||||
|
@ -87,19 +87,19 @@ class TestEvents(TestCase):
|
|||
},
|
||||
)
|
||||
|
||||
def test_from_http_tenant(self):
|
||||
"""Test from_http tenant"""
|
||||
# Test tenant
|
||||
def test_from_http_brand(self):
|
||||
"""Test from_http brand"""
|
||||
# Test brand
|
||||
request = self.factory.get("/")
|
||||
tenant = Tenant(domain="test-tenant")
|
||||
setattr(request, "tenant", tenant)
|
||||
brand = Brand(domain="test-brand")
|
||||
setattr(request, "brand", brand)
|
||||
event = Event.new("unittest").from_http(request)
|
||||
self.assertEqual(
|
||||
event.tenant,
|
||||
event.brand,
|
||||
{
|
||||
"app": "authentik_tenants",
|
||||
"model_name": "tenant",
|
||||
"name": "Tenant test-tenant",
|
||||
"pk": tenant.pk.hex,
|
||||
"app": "authentik_brands",
|
||||
"model_name": "brand",
|
||||
"name": "Brand test-brand",
|
||||
"pk": brand.pk.hex,
|
||||
},
|
||||
)
|
||||
|
|
|
@ -22,6 +22,7 @@ from sentry_sdk.api import set_tag
|
|||
from sentry_sdk.hub import Hub
|
||||
from structlog.stdlib import BoundLogger, get_logger
|
||||
|
||||
from authentik.brands.models import Brand
|
||||
from authentik.core.models import Application
|
||||
from authentik.events.models import Event, EventAction, cleanse_dict
|
||||
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.urls import is_url_absolute, redirect_with_qs
|
||||
from authentik.policies.engine import PolicyEngine
|
||||
from authentik.tenants.models import Tenant
|
||||
|
||||
LOGGER = get_logger()
|
||||
# Argument used to redirect user after login
|
||||
|
@ -490,11 +490,11 @@ class ToDefaultFlow(View):
|
|||
|
||||
def get_flow(self) -> Flow:
|
||||
"""Get a flow for the selected designation"""
|
||||
tenant: Tenant = self.request.tenant
|
||||
brand: Brand = self.request.brand
|
||||
flow = None
|
||||
# First, attempt to get default flow from tenant
|
||||
# First, attempt to get default flow from brand
|
||||
if self.designation == FlowDesignation.AUTHENTICATION:
|
||||
flow = tenant.flow_authentication
|
||||
flow = brand.flow_authentication
|
||||
# Check if we have a default flow from application
|
||||
application: Optional[Application] = self.request.session.get(
|
||||
SESSION_KEY_APPLICATION_PRE
|
||||
|
@ -502,7 +502,7 @@ class ToDefaultFlow(View):
|
|||
if application and application.provider and application.provider.authentication_flow:
|
||||
flow = application.provider.authentication_flow
|
||||
elif self.designation == FlowDesignation.INVALIDATION:
|
||||
flow = tenant.flow_invalidation
|
||||
flow = brand.flow_invalidation
|
||||
if flow:
|
||||
return flow
|
||||
# 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.utils.http import get_http_session
|
||||
from authentik.tenants.utils import get_current_tenant
|
||||
|
||||
GRAVATAR_URL = "https://secure.gravatar.com"
|
||||
DEFAULT_AVATAR = static("dist/assets/images/user_default.png")
|
||||
|
@ -183,7 +184,7 @@ def get_avatar(user: "User") -> str:
|
|||
"initials": avatar_mode_generated,
|
||||
"gravatar": avatar_mode_gravatar,
|
||||
}
|
||||
modes: str = CONFIG.get("avatars", "none")
|
||||
modes: str = get_current_tenant().avatars
|
||||
for mode in modes.split(","):
|
||||
avatar = None
|
||||
if mode in mode_map:
|
||||
|
|
|
@ -35,8 +35,8 @@ redis:
|
|||
tls_reqs: "none"
|
||||
|
||||
# broker:
|
||||
# url: ""
|
||||
# transport_options: ""
|
||||
# url: ""
|
||||
# transport_options: ""
|
||||
|
||||
cache:
|
||||
# url: ""
|
||||
|
@ -46,10 +46,10 @@ cache:
|
|||
timeout_reputation: 300
|
||||
|
||||
# channel:
|
||||
# url: ""
|
||||
# url: ""
|
||||
|
||||
# result_backend:
|
||||
# url: ""
|
||||
# url: ""
|
||||
|
||||
paths:
|
||||
media: ./media
|
||||
|
@ -105,19 +105,12 @@ reputation:
|
|||
cookie_domain: null
|
||||
disable_update_check: false
|
||||
disable_startup_analytics: false
|
||||
avatars: env://AUTHENTIK_AUTHENTIK__AVATARS?gravatar,initials
|
||||
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
|
||||
default_token_length: 60
|
||||
impersonation: true
|
||||
|
||||
tenant_management_key: ""
|
||||
|
||||
blueprints_dir: /blueprints
|
||||
|
||||
|
|
|
@ -19,6 +19,7 @@ from structlog.stdlib import get_logger
|
|||
|
||||
from authentik import __version__, get_build_hash
|
||||
from authentik.blueprints.models import ManagedModel
|
||||
from authentik.brands.models import Brand
|
||||
from authentik.core.models import (
|
||||
USER_PATH_SYSTEM_PREFIX,
|
||||
Provider,
|
||||
|
@ -34,7 +35,6 @@ from authentik.lib.models import InheritanceForeignKey, SerializerModel
|
|||
from authentik.lib.sentry import SentryIgnoredException
|
||||
from authentik.lib.utils.errors import exception_to_string
|
||||
from authentik.outposts.controllers.k8s.utils import get_namespace
|
||||
from authentik.tenants.models import Tenant
|
||||
|
||||
OUR_VERSION = parse(__version__)
|
||||
OUTPOST_HELLO_INTERVAL = 10
|
||||
|
@ -407,9 +407,9 @@ class Outpost(SerializerModel, ManagedModel):
|
|||
else:
|
||||
objects.append(provider)
|
||||
if self.managed:
|
||||
for tenant in Tenant.objects.filter(web_certificate__isnull=False):
|
||||
objects.append(tenant)
|
||||
objects.append(tenant.web_certificate)
|
||||
for brand in Brand.objects.filter(web_certificate__isnull=False):
|
||||
objects.append(brand)
|
||||
objects.append(brand.web_certificate)
|
||||
return objects
|
||||
|
||||
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 structlog.stdlib import get_logger
|
||||
|
||||
from authentik.brands.models import Brand
|
||||
from authentik.core.models import Provider
|
||||
from authentik.crypto.models import CertificateKeyPair
|
||||
from authentik.lib.utils.reflection import class_to_path
|
||||
from authentik.outposts.models import Outpost, OutpostServiceConnection
|
||||
from authentik.outposts.tasks import CACHE_KEY_OUTPOST_DOWN, outpost_controller, outpost_post_save
|
||||
from authentik.tenants.models import Tenant
|
||||
|
||||
LOGGER = get_logger()
|
||||
UPDATE_TRIGGERING_MODELS = (
|
||||
|
@ -18,7 +18,7 @@ UPDATE_TRIGGERING_MODELS = (
|
|||
OutpostServiceConnection,
|
||||
Provider,
|
||||
CertificateKeyPair,
|
||||
Tenant,
|
||||
Brand,
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -161,7 +161,7 @@ class Migration(migrations.Migration):
|
|||
("authentik.stages.user_login", "authentik Stages.User Login"),
|
||||
("authentik.stages.user_logout", "authentik Stages.User Logout"),
|
||||
("authentik.stages.user_write", "authentik Stages.User Write"),
|
||||
("authentik.tenants", "authentik Tenants"),
|
||||
("authentik.brands", "authentik Brands"),
|
||||
("authentik.core", "authentik Core"),
|
||||
("authentik.blueprints", "authentik Blueprints"),
|
||||
],
|
||||
|
|
|
@ -67,7 +67,7 @@ class Migration(migrations.Migration):
|
|||
("authentik.stages.user_login", "authentik Stages.User Login"),
|
||||
("authentik.stages.user_logout", "authentik Stages.User Logout"),
|
||||
("authentik.stages.user_write", "authentik Stages.User Write"),
|
||||
("authentik.tenants", "authentik Tenants"),
|
||||
("authentik.brands", "authentik Brands"),
|
||||
("authentik.blueprints", "authentik Blueprints"),
|
||||
("authentik.core", "authentik Core"),
|
||||
],
|
||||
|
|
|
@ -143,7 +143,7 @@ class PasswordPolicy(Policy):
|
|||
user_inputs.append(request.user.name)
|
||||
user_inputs.append(request.user.email)
|
||||
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
|
||||
# long passwords we can be reasonably sure that they'll surpass the score anyways
|
||||
# See https://github.com/dropbox/zxcvbn#runtime-latency
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
{% load i18n %}
|
||||
|
||||
{% block title %}
|
||||
{% trans 'Permission denied' %} - {{ tenant.branding_title }}
|
||||
{% trans 'Permission denied' %} - {{ brand.branding_title }}
|
||||
{% endblock %}
|
||||
|
||||
{% block card_title %}
|
||||
|
|
|
@ -4,7 +4,7 @@ from urllib.parse import urlencode
|
|||
from django.urls import reverse
|
||||
|
||||
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.providers.oauth2.models import DeviceToken, OAuth2Provider
|
||||
from authentik.providers.oauth2.tests.utils import OAuthTestCase
|
||||
|
@ -28,9 +28,9 @@ class TesOAuth2DeviceInit(OAuthTestCase):
|
|||
self.user = create_test_admin_user()
|
||||
self.client.force_login(self.user)
|
||||
self.device_flow = create_test_flow()
|
||||
self.tenant = create_test_tenant()
|
||||
self.tenant.flow_device_code = self.device_flow
|
||||
self.tenant.save()
|
||||
self.brand = create_test_brand()
|
||||
self.brand.flow_device_code = self.device_flow
|
||||
self.brand.save()
|
||||
|
||||
def test_device_init(self):
|
||||
"""Test device init"""
|
||||
|
@ -48,8 +48,8 @@ class TesOAuth2DeviceInit(OAuthTestCase):
|
|||
|
||||
def test_no_flow(self):
|
||||
"""Test no flow"""
|
||||
self.tenant.flow_device_code = None
|
||||
self.tenant.save()
|
||||
self.brand.flow_device_code = None
|
||||
self.brand.save()
|
||||
res = self.client.get(reverse("authentik_providers_oauth2_root:device-login"))
|
||||
self.assertEqual(res.status_code, 404)
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@ from rest_framework.exceptions import ErrorDetail
|
|||
from rest_framework.fields import CharField, IntegerField
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.brands.models import Brand
|
||||
from authentik.core.models import Application
|
||||
from authentik.flows.challenge import Challenge, ChallengeResponse, ChallengeTypes
|
||||
from authentik.flows.exceptions import FlowNonApplicableException
|
||||
|
@ -26,7 +27,6 @@ from authentik.stages.consent.stage import (
|
|||
PLAN_CONTEXT_CONSENT_HEADER,
|
||||
PLAN_CONTEXT_CONSENT_PERMISSIONS,
|
||||
)
|
||||
from authentik.tenants.models import Tenant
|
||||
|
||||
LOGGER = get_logger()
|
||||
QS_KEY_CODE = "code" # nosec
|
||||
|
@ -88,10 +88,10 @@ class DeviceEntryView(View):
|
|||
"""View used to initiate the device-code flow, url entered by endusers"""
|
||||
|
||||
def dispatch(self, request: HttpRequest) -> HttpResponse:
|
||||
tenant: Tenant = request.tenant
|
||||
device_flow = tenant.flow_device_code
|
||||
brand: Brand = request.brand
|
||||
device_flow = brand.flow_device_code
|
||||
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)
|
||||
if QS_KEY_CODE in request.GET:
|
||||
validation = validate_code(request.GET[QS_KEY_CODE], request)
|
||||
|
|
|
@ -97,7 +97,7 @@ class GitHubUserTeamsView(View):
|
|||
"created_at": "",
|
||||
"updated_at": "",
|
||||
"organization": {
|
||||
"login": slugify(request.tenant.branding_title),
|
||||
"login": slugify(request.brand.branding_title),
|
||||
"id": 1,
|
||||
"node_id": "",
|
||||
"url": "",
|
||||
|
@ -109,7 +109,7 @@ class GitHubUserTeamsView(View):
|
|||
"public_members_url": "",
|
||||
"avatar_url": "",
|
||||
"description": "",
|
||||
"name": request.tenant.branding_title,
|
||||
"name": request.brand.branding_title,
|
||||
"company": "",
|
||||
"blog": "",
|
||||
"location": "",
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
"""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
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
"""root settings for authentik"""
|
||||
import importlib
|
||||
import os
|
||||
from collections import OrderedDict
|
||||
from hashlib import sha512
|
||||
from pathlib import Path
|
||||
from urllib.parse import quote_plus
|
||||
|
@ -49,14 +50,24 @@ AUTHENTICATION_BACKENDS = [
|
|||
DEFAULT_AUTO_FIELD = "django.db.models.AutoField"
|
||||
|
||||
# Application definition
|
||||
INSTALLED_APPS = [
|
||||
SHARED_APPS = [
|
||||
"django_tenants",
|
||||
"authentik.tenants",
|
||||
"daphne",
|
||||
"django.contrib.auth",
|
||||
"django.contrib.contenttypes",
|
||||
"django.contrib.sessions",
|
||||
"django.contrib.messages",
|
||||
"django.contrib.staticfiles",
|
||||
"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.api",
|
||||
"authentik.crypto",
|
||||
|
@ -102,16 +113,14 @@ INSTALLED_APPS = [
|
|||
"authentik.stages.user_login",
|
||||
"authentik.stages.user_logout",
|
||||
"authentik.stages.user_write",
|
||||
"authentik.tenants",
|
||||
"authentik.brands",
|
||||
"authentik.blueprints",
|
||||
"rest_framework",
|
||||
"django_filters",
|
||||
"drf_spectacular",
|
||||
"guardian",
|
||||
"django_prometheus",
|
||||
"channels",
|
||||
]
|
||||
|
||||
TENANT_MODEL = "authentik_tenants.Tenant"
|
||||
TENANT_DOMAIN_MODEL = "authentik_tenants.Domain"
|
||||
|
||||
GUARDIAN_MONKEY_PATCH = False
|
||||
|
||||
SPECTACULAR_SETTINGS = {
|
||||
|
@ -214,12 +223,14 @@ SESSION_EXPIRE_AT_BROWSER_CLOSE = True
|
|||
MESSAGE_STORAGE = "authentik.root.messages.storage.ChannelsStorage"
|
||||
|
||||
MIDDLEWARE = [
|
||||
"django_tenants.middleware.default.DefaultTenantMiddleware",
|
||||
"authentik.root.middleware.LoggingMiddleware",
|
||||
"django_prometheus.middleware.PrometheusBeforeMiddleware",
|
||||
"authentik.brands.middleware.TenantMiddleware",
|
||||
"authentik.root.middleware.SessionMiddleware",
|
||||
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
||||
"authentik.core.middleware.RequestIDMiddleware",
|
||||
"authentik.tenants.middleware.TenantMiddleware",
|
||||
"authentik.brands.middleware.BrandMiddleware",
|
||||
"authentik.events.middleware.AuditMiddleware",
|
||||
"django.middleware.security.SecurityMiddleware",
|
||||
"django.middleware.common.CommonMiddleware",
|
||||
|
@ -243,7 +254,7 @@ TEMPLATES = [
|
|||
"django.template.context_processors.request",
|
||||
"django.contrib.auth.context_processors.auth",
|
||||
"django.contrib.messages.context_processors.messages",
|
||||
"authentik.tenants.utils.context_processor",
|
||||
"authentik.brands.utils.context_processor",
|
||||
],
|
||||
},
|
||||
},
|
||||
|
@ -265,6 +276,7 @@ CHANNEL_LAYERS = {
|
|||
# Database
|
||||
# https://docs.djangoproject.com/en/2.1/ref/settings/#databases
|
||||
|
||||
ORIGINAL_BACKEND = "django_prometheus.db.backends.postgresql"
|
||||
DATABASES = {
|
||||
"default": {
|
||||
"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
|
||||
DATABASES["default"]["CONN_MAX_AGE"] = None # persistent
|
||||
|
||||
DATABASE_ROUTERS = ("django_tenants.routers.TenantSyncRouter",)
|
||||
|
||||
# Email
|
||||
# These values should never actually be used, emails are only sent from email stages, which
|
||||
# loads the config directly from CONFIG
|
||||
|
@ -378,6 +392,8 @@ LOGGING = get_logger_config()
|
|||
|
||||
|
||||
_DISALLOWED_ITEMS = [
|
||||
"SHARED_APPS",
|
||||
"TENANT_APPS",
|
||||
"INSTALLED_APPS",
|
||||
"MIDDLEWARE",
|
||||
"AUTHENTICATION_BACKENDS",
|
||||
|
@ -389,7 +405,8 @@ def _update_settings(app_path: str):
|
|||
try:
|
||||
settings_module = importlib.import_module(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", []))
|
||||
AUTHENTICATION_BACKENDS.extend(getattr(settings_module, "AUTHENTICATION_BACKENDS", []))
|
||||
CELERY["beat_schedule"].update(getattr(settings_module, "CELERY_BEAT_SCHEDULE", {}))
|
||||
|
@ -401,7 +418,7 @@ def _update_settings(app_path: str):
|
|||
|
||||
|
||||
# Load subapps's settings
|
||||
for _app in INSTALLED_APPS:
|
||||
for _app in set(SHARED_APPS + TENANT_APPS):
|
||||
if not _app.startswith("authentik"):
|
||||
continue
|
||||
_update_settings(f"{_app}.settings")
|
||||
|
@ -410,14 +427,14 @@ _update_settings("data.user_settings")
|
|||
if DEBUG:
|
||||
CELERY["task_always_eager"] = True
|
||||
os.environ[ENV_GIT_HASH_KEY] = "dev"
|
||||
INSTALLED_APPS.append("silk")
|
||||
SHARED_APPS.append("silk")
|
||||
SILKY_PYTHON_PROFILER = True
|
||||
MIDDLEWARE = ["silk.middleware.SilkyMiddleware"] + MIDDLEWARE
|
||||
REST_FRAMEWORK["DEFAULT_RENDERER_CLASSES"].append(
|
||||
"rest_framework.renderers.BrowsableAPIRenderer"
|
||||
)
|
||||
|
||||
INSTALLED_APPS.append("authentik.core")
|
||||
TENANT_APPS.append("authentik.core")
|
||||
|
||||
CONFIG.log("info", "Booting authentik", version=__version__)
|
||||
|
||||
|
@ -425,7 +442,10 @@ CONFIG.log("info", "Booting authentik", version=__version__)
|
|||
try:
|
||||
importlib.import_module("authentik.enterprise.apps")
|
||||
CONFIG.log("info", "Enabled authentik enterprise")
|
||||
INSTALLED_APPS.append("authentik.enterprise")
|
||||
TENANT_APPS.append("authentik.enterprise")
|
||||
_update_settings("authentik.enterprise.settings")
|
||||
except ImportError:
|
||||
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={
|
||||
"type": ChallengeTypes.NATIVE.value,
|
||||
"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=__(
|
||||
"%(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,
|
||||
|
|
|
@ -6,6 +6,7 @@ from django.test.client import RequestFactory
|
|||
from django.urls import reverse
|
||||
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.events.models import Event, EventAction
|
||||
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.models import AuthenticatorValidateStage, DeviceClasses
|
||||
from authentik.stages.user_login.models import UserLoginStage
|
||||
from authentik.tenants.utils import get_tenant_for_request
|
||||
|
||||
|
||||
class AuthenticatorValidateStageDuoTests(FlowTestCase):
|
||||
|
@ -36,7 +36,7 @@ class AuthenticatorValidateStageDuoTests(FlowTestCase):
|
|||
middleware = SessionMiddleware(dummy_get_response)
|
||||
middleware.process_request(request)
|
||||
request.session.save()
|
||||
setattr(request, "tenant", get_tenant_for_request(request))
|
||||
setattr(request, "brand", get_brand_for_request(request))
|
||||
|
||||
stage = AuthenticatorDuoStage.objects.create(
|
||||
name=generate_id(),
|
||||
|
|
|
@ -89,7 +89,7 @@ class AuthenticatorWebAuthnStageView(ChallengeStageView):
|
|||
|
||||
registration_options: PublicKeyCredentialCreationOptions = generate_registration_options(
|
||||
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_name=user.username,
|
||||
user_display_name=user.name,
|
||||
|
|
|
@ -1,137 +1,114 @@
|
|||
"""Serializer for tenant models"""
|
||||
from typing import Any
|
||||
"""Serializer for tenants models"""
|
||||
from hmac import compare_digest
|
||||
|
||||
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 django_tenants.utils import get_tenant
|
||||
from rest_framework import permissions
|
||||
from rest_framework.authentication import get_authorization_header
|
||||
from rest_framework.fields import ReadOnlyField
|
||||
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.response import Response
|
||||
from rest_framework.serializers import ModelSerializer
|
||||
from rest_framework.views import View
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from authentik.api.authorization import SecretKeyFilter
|
||||
from authentik.core.api.used_by import UsedByMixin
|
||||
from authentik.core.api.utils import PassiveSerializer
|
||||
from authentik.api.authentication import validate_auth
|
||||
from authentik.lib.config import CONFIG
|
||||
from authentik.tenants.models import Tenant
|
||||
from authentik.tenants.models import Domain, Tenant
|
||||
|
||||
|
||||
class FooterLinkSerializer(PassiveSerializer):
|
||||
"""Links returned in Config API"""
|
||||
|
||||
href = CharField(read_only=True)
|
||||
name = CharField(read_only=True)
|
||||
class TenantManagementKeyPermission(permissions.BasePermission):
|
||||
def has_permission(self, request: Request, view: View) -> bool:
|
||||
token = validate_auth(get_authorization_header(request))
|
||||
tenant_management_key = CONFIG.get("tenant_management_key")
|
||||
if compare_digest("", tenant_management_key):
|
||||
return False
|
||||
return compare_digest(token, tenant_management_key)
|
||||
|
||||
|
||||
class TenantSerializer(ModelSerializer):
|
||||
"""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:
|
||||
model = Tenant
|
||||
fields = [
|
||||
"tenant_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",
|
||||
"schema_name",
|
||||
"name",
|
||||
]
|
||||
|
||||
|
||||
class Themes(models.TextChoices):
|
||||
"""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):
|
||||
class TenantViewSet(ModelViewSet):
|
||||
"""Tenant Viewset"""
|
||||
|
||||
queryset = Tenant.objects.all()
|
||||
serializer_class = TenantSerializer
|
||||
search_fields = [
|
||||
"domain",
|
||||
"branding_title",
|
||||
"web_certificate__name",
|
||||
"name",
|
||||
"schema_name",
|
||||
"domains__domain",
|
||||
]
|
||||
filterset_fields = [
|
||||
"tenant_uuid",
|
||||
ordering = ["schema_name"]
|
||||
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",
|
||||
"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",
|
||||
"tenant__name",
|
||||
"tenant__schema_name",
|
||||
]
|
||||
ordering = ["domain"]
|
||||
permission_classes = [TenantManagementKeyPermission]
|
||||
filter_backends = [OrderingFilter, SearchFilter]
|
||||
|
||||
filter_backends = [SecretKeyFilter, OrderingFilter, SearchFilter]
|
||||
|
||||
@extend_schema(
|
||||
responses=CurrentTenantSerializer(many=False),
|
||||
)
|
||||
@action(methods=["GET"], detail=False, permission_classes=[AllowAny])
|
||||
def current(self, request: Request) -> Response:
|
||||
"""Get current tenant"""
|
||||
tenant: Tenant = request._request.tenant
|
||||
return Response(CurrentTenantSerializer(tenant).data)
|
||||
class SettingsSerializer(ModelSerializer):
|
||||
"""Settings Serializer"""
|
||||
|
||||
name = ReadOnlyField()
|
||||
domains = DomainSerializer(read_only=True, many=True)
|
||||
|
||||
class Meta:
|
||||
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
|
||||
|
||||
|
||||
class AuthentikTenantsConfig(AppConfig):
|
||||
"""authentik Tenant app"""
|
||||
"""authentik tenants app"""
|
||||
|
||||
name = "authentik.tenants"
|
||||
label = "authentik_tenants"
|
||||
|
|
|
@ -1,15 +1,13 @@
|
|||
"""Inject tenant 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 django.http import HttpRequest, HttpResponse
|
||||
from sentry_sdk.api import set_tag
|
||||
|
||||
from authentik.tenants.utils import get_tenant_for_request
|
||||
|
||||
|
||||
class TenantMiddleware:
|
||||
class CurrentTenantMiddleware:
|
||||
"""Add current tenant to http request"""
|
||||
|
||||
get_response: Callable[[HttpRequest], HttpResponse]
|
||||
|
@ -22,8 +20,5 @@ class TenantMiddleware:
|
|||
tenant = get_tenant_for_request(request)
|
||||
setattr(request, "tenant", tenant)
|
||||
set_tag("authentik.tenant_uuid", tenant.tenant_uuid.hex)
|
||||
set_tag("authentik.tenant_domain", tenant.domain)
|
||||
locale = tenant.default_locale
|
||||
if locale != "":
|
||||
activate(locale)
|
||||
set_tag("authentik.tenant_domain_regex", tenant.domain_regex)
|
||||
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 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_tenants.models import DomainMixin, TenantMixin
|
||||
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 Tenant(SerializerModel):
|
||||
"""Single tenant"""
|
||||
class Tenant(TenantMixin, SerializerModel):
|
||||
"""Tenant"""
|
||||
|
||||
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=_(
|
||||
"Domain that activates this tenant. Can be a superset, i.e. `a.b` for `aa.b` and `ba.b`"
|
||||
)
|
||||
)
|
||||
default = models.BooleanField(
|
||||
default=False,
|
||||
)
|
||||
|
||||
branding_title = models.TextField(default="authentik")
|
||||
|
||||
branding_logo = models.TextField(default="/static/dist/assets/icons/icon_left_brand.svg")
|
||||
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)."
|
||||
"When enabled, all the events caused by a user will be deleted upon the user's deletion."
|
||||
),
|
||||
default=True,
|
||||
)
|
||||
|
||||
web_certificate = models.ForeignKey(
|
||||
CertificateKeyPair,
|
||||
null=True,
|
||||
default=None,
|
||||
on_delete=models.SET_DEFAULT,
|
||||
help_text=_("Web Certificate used by the authentik Core webserver."),
|
||||
impersonation = models.BooleanField(
|
||||
help_text=_("Globally enable/disable impersonation."), default=True
|
||||
)
|
||||
footer_links = models.JSONField(
|
||||
help_text=_("The option configures the footer links on the flow executor pages."),
|
||||
default=list,
|
||||
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
|
||||
def serializer(self) -> Serializer:
|
||||
|
@ -74,21 +62,24 @@ class Tenant(SerializerModel):
|
|||
|
||||
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:
|
||||
if self.default:
|
||||
return "Default tenant"
|
||||
return f"Tenant {self.domain}"
|
||||
return f"Tenant {self.domain_regex}"
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Tenant")
|
||||
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"""
|
||||
from authentik.tenants.api import TenantViewSet
|
||||
from django.urls import path
|
||||
|
||||
from authentik.tenants.api import SettingsView, TenantViewSet
|
||||
|
||||
api_urlpatterns = [
|
||||
("core/tenants", TenantViewSet),
|
||||
path("admin/settings/", SettingsView.as_view(), name="tenant_settings"),
|
||||
(
|
||||
"tenants",
|
||||
TenantViewSet,
|
||||
),
|
||||
]
|
||||
|
|
|
@ -1,42 +1,8 @@
|
|||
"""Tenant utilities"""
|
||||
from typing import Any
|
||||
from django.db import connection
|
||||
|
||||
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
|
||||
|
||||
_q_default = Q(default=True)
|
||||
DEFAULT_TENANT = Tenant(domain="fallback")
|
||||
|
||||
|
||||
def get_tenant_for_request(request: HttpRequest) -> Tenant:
|
||||
"""Get tenant object for current request"""
|
||||
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(),
|
||||
}
|
||||
def get_current_tenant() -> Tenant:
|
||||
"""Get tenant for current request"""
|
||||
return connection.tenant
|
||||
|
|
|
@ -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
|
|
@ -2,148 +2,148 @@ version: 1
|
|||
metadata:
|
||||
name: Default - User settings flow
|
||||
entries:
|
||||
- attrs:
|
||||
designation: stage_configuration
|
||||
name: User settings
|
||||
title: Update your info
|
||||
authentication: require_authenticated
|
||||
identifiers:
|
||||
slug: default-user-settings-flow
|
||||
model: authentik_flows.flow
|
||||
id: flow
|
||||
- attrs:
|
||||
order: 200
|
||||
placeholder: Username
|
||||
placeholder_expression: false
|
||||
initial_value: |
|
||||
try:
|
||||
return user.username
|
||||
except:
|
||||
return ''
|
||||
initial_value_expression: true
|
||||
required: true
|
||||
type: text
|
||||
field_key: username
|
||||
label: Username
|
||||
identifiers:
|
||||
name: default-user-settings-field-username
|
||||
id: prompt-field-username
|
||||
model: authentik_stages_prompt.prompt
|
||||
- attrs:
|
||||
order: 201
|
||||
placeholder: Name
|
||||
placeholder_expression: false
|
||||
initial_value: |
|
||||
try:
|
||||
return user.name
|
||||
except:
|
||||
return ''
|
||||
initial_value_expression: true
|
||||
required: true
|
||||
type: text
|
||||
field_key: name
|
||||
label: Name
|
||||
identifiers:
|
||||
name: default-user-settings-field-name
|
||||
id: prompt-field-name
|
||||
model: authentik_stages_prompt.prompt
|
||||
- attrs:
|
||||
order: 202
|
||||
placeholder: Email
|
||||
placeholder_expression: false
|
||||
initial_value: |
|
||||
try:
|
||||
return user.email
|
||||
except:
|
||||
return ''
|
||||
initial_value_expression: true
|
||||
required: true
|
||||
type: email
|
||||
field_key: email
|
||||
label: Email
|
||||
identifiers:
|
||||
name: default-user-settings-field-email
|
||||
id: prompt-field-email
|
||||
model: authentik_stages_prompt.prompt
|
||||
- attrs:
|
||||
order: 203
|
||||
placeholder: Locale
|
||||
placeholder_expression: false
|
||||
initial_value: |
|
||||
try:
|
||||
return user.attributes.get("settings", {}).get("locale", "")
|
||||
except:
|
||||
return ''
|
||||
initial_value_expression: true
|
||||
required: true
|
||||
type: ak-locale
|
||||
field_key: attributes.settings.locale
|
||||
label: Locale
|
||||
identifiers:
|
||||
name: default-user-settings-field-locale
|
||||
id: prompt-field-locale
|
||||
model: authentik_stages_prompt.prompt
|
||||
- attrs:
|
||||
expression: |
|
||||
from authentik.lib.config import CONFIG
|
||||
from authentik.core.models import (
|
||||
USER_ATTRIBUTE_CHANGE_EMAIL,
|
||||
USER_ATTRIBUTE_CHANGE_NAME,
|
||||
USER_ATTRIBUTE_CHANGE_USERNAME
|
||||
)
|
||||
prompt_data = request.context.get("prompt_data")
|
||||
- attrs:
|
||||
designation: stage_configuration
|
||||
name: User settings
|
||||
title: Update your info
|
||||
authentication: require_authenticated
|
||||
identifiers:
|
||||
slug: default-user-settings-flow
|
||||
model: authentik_flows.flow
|
||||
id: flow
|
||||
- attrs:
|
||||
order: 200
|
||||
placeholder: Username
|
||||
placeholder_expression: false
|
||||
initial_value: |
|
||||
try:
|
||||
return user.username
|
||||
except:
|
||||
return ''
|
||||
initial_value_expression: true
|
||||
required: true
|
||||
type: text
|
||||
field_key: username
|
||||
label: Username
|
||||
identifiers:
|
||||
name: default-user-settings-field-username
|
||||
id: prompt-field-username
|
||||
model: authentik_stages_prompt.prompt
|
||||
- attrs:
|
||||
order: 201
|
||||
placeholder: Name
|
||||
placeholder_expression: false
|
||||
initial_value: |
|
||||
try:
|
||||
return user.name
|
||||
except:
|
||||
return ''
|
||||
initial_value_expression: true
|
||||
required: true
|
||||
type: text
|
||||
field_key: name
|
||||
label: Name
|
||||
identifiers:
|
||||
name: default-user-settings-field-name
|
||||
id: prompt-field-name
|
||||
model: authentik_stages_prompt.prompt
|
||||
- attrs:
|
||||
order: 202
|
||||
placeholder: Email
|
||||
placeholder_expression: false
|
||||
initial_value: |
|
||||
try:
|
||||
return user.email
|
||||
except:
|
||||
return ''
|
||||
initial_value_expression: true
|
||||
required: true
|
||||
type: email
|
||||
field_key: email
|
||||
label: Email
|
||||
identifiers:
|
||||
name: default-user-settings-field-email
|
||||
id: prompt-field-email
|
||||
model: authentik_stages_prompt.prompt
|
||||
- attrs:
|
||||
order: 203
|
||||
placeholder: Locale
|
||||
placeholder_expression: false
|
||||
initial_value: |
|
||||
try:
|
||||
return user.attributes.get("settings", {}).get("locale", "")
|
||||
except:
|
||||
return ''
|
||||
initial_value_expression: true
|
||||
required: true
|
||||
type: ak-locale
|
||||
field_key: attributes.settings.locale
|
||||
label: Locale
|
||||
identifiers:
|
||||
name: default-user-settings-field-locale
|
||||
id: prompt-field-locale
|
||||
model: authentik_stages_prompt.prompt
|
||||
- attrs:
|
||||
expression: |
|
||||
from authentik.lib.config import CONFIG
|
||||
from authentik.core.models import (
|
||||
USER_ATTRIBUTE_CHANGE_EMAIL,
|
||||
USER_ATTRIBUTE_CHANGE_NAME,
|
||||
USER_ATTRIBUTE_CHANGE_USERNAME
|
||||
)
|
||||
prompt_data = request.context.get("prompt_data")
|
||||
|
||||
if not request.user.group_attributes(request.http_request).get(
|
||||
USER_ATTRIBUTE_CHANGE_EMAIL, CONFIG.get_bool("default_user_change_email", True)
|
||||
):
|
||||
if prompt_data.get("email") != request.user.email:
|
||||
ak_message("Not allowed to change email address.")
|
||||
return False
|
||||
if not request.user.group_attributes(request.http_request).get(
|
||||
USER_ATTRIBUTE_CHANGE_EMAIL, request.tenant.default_user_change_email
|
||||
):
|
||||
if prompt_data.get("email") != request.user.email:
|
||||
ak_message("Not allowed to change email address.")
|
||||
return False
|
||||
|
||||
if not request.user.group_attributes(request.http_request).get(
|
||||
USER_ATTRIBUTE_CHANGE_NAME, CONFIG.get_bool("default_user_change_name", True)
|
||||
):
|
||||
if prompt_data.get("name") != request.user.name:
|
||||
ak_message("Not allowed to change name.")
|
||||
return False
|
||||
if not request.user.group_attributes(request.http_request).get(
|
||||
USER_ATTRIBUTE_CHANGE_NAME, request.tenant.default_user_change_name
|
||||
):
|
||||
if prompt_data.get("name") != request.user.name:
|
||||
ak_message("Not allowed to change name.")
|
||||
return False
|
||||
|
||||
if not request.user.group_attributes(request.http_request).get(
|
||||
USER_ATTRIBUTE_CHANGE_USERNAME, CONFIG.get_bool("default_user_change_username", True)
|
||||
):
|
||||
if prompt_data.get("username") != request.user.username:
|
||||
ak_message("Not allowed to change username.")
|
||||
return False
|
||||
if not request.user.group_attributes(request.http_request).get(
|
||||
USER_ATTRIBUTE_CHANGE_USERNAME, request.tenant.default_user_change_username
|
||||
):
|
||||
if prompt_data.get("username") != request.user.username:
|
||||
ak_message("Not allowed to change username.")
|
||||
return False
|
||||
|
||||
return True
|
||||
identifiers:
|
||||
name: default-user-settings-authorization
|
||||
id: default-user-settings-authorization
|
||||
model: authentik_policies_expression.expressionpolicy
|
||||
- identifiers:
|
||||
name: default-user-settings-write
|
||||
attrs:
|
||||
user_creation_mode: never_create
|
||||
id: default-user-settings-write
|
||||
model: authentik_stages_user_write.userwritestage
|
||||
- attrs:
|
||||
fields:
|
||||
- !KeyOf prompt-field-username
|
||||
- !KeyOf prompt-field-name
|
||||
- !KeyOf prompt-field-email
|
||||
- !KeyOf prompt-field-locale
|
||||
validation_policies:
|
||||
- !KeyOf default-user-settings-authorization
|
||||
identifiers:
|
||||
name: default-user-settings
|
||||
id: default-user-settings
|
||||
model: authentik_stages_prompt.promptstage
|
||||
- identifiers:
|
||||
order: 20
|
||||
stage: !KeyOf default-user-settings
|
||||
target: !KeyOf flow
|
||||
model: authentik_flows.flowstagebinding
|
||||
- identifiers:
|
||||
order: 100
|
||||
stage: !KeyOf default-user-settings-write
|
||||
target: !KeyOf flow
|
||||
model: authentik_flows.flowstagebinding
|
||||
return True
|
||||
identifiers:
|
||||
name: default-user-settings-authorization
|
||||
id: default-user-settings-authorization
|
||||
model: authentik_policies_expression.expressionpolicy
|
||||
- identifiers:
|
||||
name: default-user-settings-write
|
||||
attrs:
|
||||
user_creation_mode: never_create
|
||||
id: default-user-settings-write
|
||||
model: authentik_stages_user_write.userwritestage
|
||||
- attrs:
|
||||
fields:
|
||||
- !KeyOf prompt-field-username
|
||||
- !KeyOf prompt-field-name
|
||||
- !KeyOf prompt-field-email
|
||||
- !KeyOf prompt-field-locale
|
||||
validation_policies:
|
||||
- !KeyOf default-user-settings-authorization
|
||||
identifiers:
|
||||
name: default-user-settings
|
||||
id: default-user-settings
|
||||
model: authentik_stages_prompt.promptstage
|
||||
- identifiers:
|
||||
order: 20
|
||||
stage: !KeyOf default-user-settings
|
||||
target: !KeyOf flow
|
||||
model: authentik_flows.flowstagebinding
|
||||
- identifiers:
|
||||
order: 100
|
||||
stage: !KeyOf default-user-settings-write
|
||||
target: !KeyOf flow
|
||||
model: authentik_flows.flowstagebinding
|
||||
|
|
|
@ -41,6 +41,80 @@
|
|||
"type": "array",
|
||||
"items": {
|
||||
"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",
|
||||
"required": [
|
||||
|
@ -2528,7 +2602,7 @@
|
|||
],
|
||||
"properties": {
|
||||
"model": {
|
||||
"const": "authentik_tenants.tenant"
|
||||
"const": "authentik_brands.brand"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
|
@ -2550,10 +2624,10 @@
|
|||
}
|
||||
},
|
||||
"attrs": {
|
||||
"$ref": "#/$defs/model_authentik_tenants.tenant"
|
||||
"$ref": "#/$defs/model_authentik_brands.brand"
|
||||
},
|
||||
"identifiers": {
|
||||
"$ref": "#/$defs/model_authentik_tenants.tenant"
|
||||
"$ref": "#/$defs/model_authentik_brands.brand"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -2821,6 +2895,43 @@
|
|||
}
|
||||
},
|
||||
"$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": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
@ -2907,10 +3018,10 @@
|
|||
"format": "date-time",
|
||||
"title": "Expires"
|
||||
},
|
||||
"tenant": {
|
||||
"brand": {
|
||||
"type": "object",
|
||||
"additionalProperties": true,
|
||||
"title": "Tenant"
|
||||
"title": "Brand"
|
||||
}
|
||||
},
|
||||
"required": []
|
||||
|
@ -3016,10 +3127,10 @@
|
|||
"format": "date-time",
|
||||
"title": "Expires"
|
||||
},
|
||||
"tenant": {
|
||||
"brand": {
|
||||
"type": "object",
|
||||
"additionalProperties": true,
|
||||
"title": "Tenant"
|
||||
"title": "Brand"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
|
@ -3427,6 +3538,7 @@
|
|||
],
|
||||
"enum": [
|
||||
null,
|
||||
"authentik.tenants",
|
||||
"authentik.admin",
|
||||
"authentik.api",
|
||||
"authentik.crypto",
|
||||
|
@ -3472,7 +3584,7 @@
|
|||
"authentik.stages.user_login",
|
||||
"authentik.stages.user_logout",
|
||||
"authentik.stages.user_write",
|
||||
"authentik.tenants",
|
||||
"authentik.brands",
|
||||
"authentik.blueprints",
|
||||
"authentik.core",
|
||||
"authentik.enterprise"
|
||||
|
@ -3487,6 +3599,8 @@
|
|||
],
|
||||
"enum": [
|
||||
null,
|
||||
"authentik_tenants.tenant",
|
||||
"authentik_tenants.domain",
|
||||
"authentik_crypto.certificatekeypair",
|
||||
"authentik_events.event",
|
||||
"authentik_events.notificationtransport",
|
||||
|
@ -3554,7 +3668,7 @@
|
|||
"authentik_stages_user_login.userloginstage",
|
||||
"authentik_stages_user_logout.userlogoutstage",
|
||||
"authentik_stages_user_write.userwritestage",
|
||||
"authentik_tenants.tenant",
|
||||
"authentik_brands.brand",
|
||||
"authentik_blueprints.blueprintinstance",
|
||||
"authentik_core.group",
|
||||
"authentik_core.user",
|
||||
|
@ -8390,14 +8504,14 @@
|
|||
},
|
||||
"required": []
|
||||
},
|
||||
"model_authentik_tenants.tenant": {
|
||||
"model_authentik_brands.brand": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"domain": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"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": {
|
||||
"type": "boolean",
|
||||
|
|
|
@ -19,7 +19,7 @@ import (
|
|||
sentryutils "goauthentik.io/internal/utils/sentry"
|
||||
webutils "goauthentik.io/internal/utils/web"
|
||||
"goauthentik.io/internal/web"
|
||||
"goauthentik.io/internal/web/tenant_tls"
|
||||
"goauthentik.io/internal/web/brand_tls"
|
||||
)
|
||||
|
||||
var rootCmd = &cobra.Command{
|
||||
|
@ -95,11 +95,11 @@ func attemptProxyStart(ws *web.WebServer, u *url.URL) {
|
|||
}
|
||||
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
|
||||
tw := tenant_tls.NewWatcher(ac.Client)
|
||||
tw := brand_tls.NewWatcher(ac.Client)
|
||||
go tw.Start()
|
||||
ws.TenantTLS = tw
|
||||
ws.BrandTLS = tw
|
||||
ac.AddRefreshHandler(func() {
|
||||
tw.Check()
|
||||
})
|
||||
|
|
|
@ -30,9 +30,9 @@ func (ls *LDAPServer) getCurrentProvider(pk int32) *ProviderInstance {
|
|||
}
|
||||
|
||||
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 {
|
||||
ls.log.WithError(err).Warning("failed to fetch tenant config")
|
||||
ls.log.WithError(err).Warning("failed to fetch brand config")
|
||||
return ""
|
||||
}
|
||||
flow := req.GetFlowInvalidation()
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
package tenant_tls
|
||||
package brand_tls
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
|
@ -6,6 +6,7 @@ import (
|
|||
"time"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"goauthentik.io/api/v3"
|
||||
"goauthentik.io/internal/crypto"
|
||||
"goauthentik.io/internal/outpost/ak"
|
||||
|
@ -16,12 +17,12 @@ type Watcher struct {
|
|||
log *log.Entry
|
||||
cs *ak.CryptoStore
|
||||
fallback *tls.Certificate
|
||||
tenants []api.Tenant
|
||||
brands []api.Brand
|
||||
}
|
||||
|
||||
func NewWatcher(client *api.APIClient) *Watcher {
|
||||
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()
|
||||
if err != nil {
|
||||
l.WithError(err).Error("failed to generate default cert")
|
||||
|
@ -37,20 +38,20 @@ func NewWatcher(client *api.APIClient) *Watcher {
|
|||
|
||||
func (w *Watcher) Start() {
|
||||
ticker := time.NewTicker(time.Minute * 3)
|
||||
w.log.Info("Starting Tenant TLS Checker")
|
||||
w.log.Info("Starting Brand TLS Checker")
|
||||
for ; true; <-ticker.C {
|
||||
w.Check()
|
||||
}
|
||||
}
|
||||
|
||||
func (w *Watcher) Check() {
|
||||
w.log.Info("updating tenant certificates")
|
||||
tenants, _, err := w.client.CoreApi.CoreTenantsListExecute(api.ApiCoreTenantsListRequest{})
|
||||
w.log.Info("updating brand certificates")
|
||||
brands, _, err := w.client.CoreApi.CoreBrandsListExecute(api.ApiCoreBrandsListRequest{})
|
||||
if err != nil {
|
||||
w.log.WithError(err).Warning("failed to get tenants")
|
||||
w.log.WithError(err).Warning("failed to get brands")
|
||||
return
|
||||
}
|
||||
for _, t := range tenants.Results {
|
||||
for _, t := range brands.Results {
|
||||
if kp := t.WebCertificate.Get(); kp != nil {
|
||||
err := w.cs.AddKeypair(*kp)
|
||||
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) {
|
||||
var bestSelection *api.Tenant
|
||||
for _, t := range w.tenants {
|
||||
var bestSelection *api.Brand
|
||||
for _, t := range w.brands {
|
||||
if t.WebCertificate.Get() == nil {
|
||||
continue
|
||||
}
|
|
@ -5,6 +5,7 @@ import (
|
|||
"net"
|
||||
|
||||
"github.com/pires/go-proxyproto"
|
||||
|
||||
"goauthentik.io/internal/config"
|
||||
"goauthentik.io/internal/crypto"
|
||||
"goauthentik.io/internal/utils"
|
||||
|
@ -26,8 +27,8 @@ func (ws *WebServer) GetCertificate() func(ch *tls.ClientHelloInfo) (*tls.Certif
|
|||
return appCert, nil
|
||||
}
|
||||
}
|
||||
if ws.TenantTLS != nil {
|
||||
return ws.TenantTLS.GetCertificate(ch)
|
||||
if ws.BrandTLS != nil {
|
||||
return ws.BrandTLS.GetCertificate(ch)
|
||||
}
|
||||
ws.log.Trace("using default, self-signed certificate")
|
||||
return &cert, nil
|
||||
|
|
|
@ -15,11 +15,12 @@ import (
|
|||
"github.com/gorilla/mux"
|
||||
"github.com/pires/go-proxyproto"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"goauthentik.io/internal/config"
|
||||
"goauthentik.io/internal/gounicorn"
|
||||
"goauthentik.io/internal/outpost/proxyv2"
|
||||
"goauthentik.io/internal/utils/web"
|
||||
"goauthentik.io/internal/web/tenant_tls"
|
||||
"goauthentik.io/internal/web/brand_tls"
|
||||
)
|
||||
|
||||
type WebServer struct {
|
||||
|
@ -29,7 +30,7 @@ type WebServer struct {
|
|||
stop chan struct{} // channel for waiting shutdown
|
||||
|
||||
ProxyServer *proxyv2.ProxyServer
|
||||
TenantTLS *tenant_tls.Watcher
|
||||
BrandTLS *brand_tls.Watcher
|
||||
|
||||
g *gounicorn.GoUnicorn
|
||||
gr bool
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
#!/bin/bash -e
|
||||
#!/usr/bin/env -S bash -e
|
||||
MODE_FILE="${TMPDIR}/authentik-mode"
|
||||
|
||||
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-prometheus = "*"
|
||||
django-redis = "*"
|
||||
django-tenants = { git = "https://github.com/hho6643/django-tenants.git", branch="hho6643-psycopg3_fixes" }
|
||||
djangorestframework = "*"
|
||||
djangorestframework-guardian = "*"
|
||||
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 {
|
||||
async setBindFlow(selector: string) {
|
||||
await this.searchSelect(
|
||||
'>>>ak-tenanted-flow-search[name="authorizationFlow"] input[type="text"]',
|
||||
'>>>ak-branded-flow-search[name="authorizationFlow"] input[type="text"]',
|
||||
"authorizationFlow",
|
||||
`button*=${selector}`,
|
||||
);
|
||||
|
|
|
@ -3,7 +3,7 @@ import Page from "../page.js";
|
|||
export class RadiusForm extends Page {
|
||||
async setAuthenticationFlow(selector: string) {
|
||||
await this.searchSelect(
|
||||
'>>>ak-tenanted-flow-search[name="authorizationFlow"] input[type="text"]',
|
||||
'>>>ak-branded-flow-search[name="authorizationFlow"] input[type="text"]',
|
||||
"authorizationFlow",
|
||||
`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");
|
||||
return html`<ak-token-list></ak-token-list>`;
|
||||
}),
|
||||
new Route(new RegExp("^/core/tenants$"), async () => {
|
||||
await import("@goauthentik/admin/tenants/TenantListPage");
|
||||
return html`<ak-tenant-list></ak-tenant-list>`;
|
||||
new Route(new RegExp("^/core/brands"), async () => {
|
||||
await import("@goauthentik/admin/brands/BrandListPage");
|
||||
return html`<ak-brand-list></ak-brand-list>`;
|
||||
}),
|
||||
new Route(new RegExp("^/policy/policies$"), async () => {
|
||||
await import("@goauthentik/admin/policies/PolicyListPage");
|
||||
|
|
|
@ -57,7 +57,7 @@ export class RecentEventsCard extends Table<Event> {
|
|||
new TableColumn(msg("User"), "user"),
|
||||
new TableColumn(msg("Creation Date"), "created"),
|
||||
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` <div>${item.clientIp || msg("-")}</div>
|
||||
<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-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 "@goauthentik/components/ak-number-input";
|
||||
import "@goauthentik/components/ak-radio-input";
|
||||
|
@ -49,12 +49,12 @@ export class ApplicationWizardApplicationDetails extends BaseProviderPanel {
|
|||
?required=${true}
|
||||
name="authorizationFlow"
|
||||
>
|
||||
<ak-tenanted-flow-search
|
||||
<ak-branded-flow-search
|
||||
flowType=${FlowsInstancesListDesignationEnum.Authentication}
|
||||
.currentFlow=${provider?.authorizationFlow}
|
||||
.tenantFlow=${rootInterface()?.tenant?.flowAuthentication}
|
||||
.brandFlow=${rootInterface()?.brand?.flowAuthentication}
|
||||
required
|
||||
></ak-tenanted-flow-search>
|
||||
></ak-branded-flow-search>
|
||||
<p class="pf-c-form__helper-text">${msg("Flow used for users to authenticate.")}</p>
|
||||
</ak-form-element-horizontal>
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
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 {
|
||||
clientTypeOptions,
|
||||
issuerModeOptions,
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
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 "@goauthentik/components/ak-text-input";
|
||||
import { rootInterface } from "@goauthentik/elements/Base";
|
||||
|
@ -34,12 +34,12 @@ export class ApplicationWizardAuthenticationByRadius extends BaseProviderPanel {
|
|||
?required=${true}
|
||||
name="authorizationFlow"
|
||||
>
|
||||
<ak-tenanted-flow-search
|
||||
<ak-branded-flow-search
|
||||
flowType=${FlowsInstancesListDesignationEnum.Authentication}
|
||||
.currentFlow=${provider?.authorizationFlow}
|
||||
.tenantFlow=${rootInterface()?.tenant?.flowAuthentication}
|
||||
.brandFlow=${rootInterface()?.brand?.flowAuthentication}
|
||||
required
|
||||
></ak-tenanted-flow-search>
|
||||
></ak-branded-flow-search>
|
||||
<p class="pf-c-form__helper-text">${msg("Flow used for users to authenticate.")}</p>
|
||||
</ak-form-element-horizontal>
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import "@goauthentik/admin/common/ak-core-group-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 "@goauthentik/components/ak-number-input";
|
||||
import "@goauthentik/components/ak-radio-input";
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
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 { first } from "@goauthentik/common/utils";
|
||||
import "@goauthentik/components/ak-switch-input";
|
||||
|
|
|
@ -8,40 +8,40 @@ import "@goauthentik/elements/forms/FormGroup";
|
|||
import "@goauthentik/elements/forms/HorizontalFormElement";
|
||||
import { ModelForm } from "@goauthentik/elements/forms/ModelForm";
|
||||
import "@goauthentik/elements/forms/SearchSelect";
|
||||
import { DefaultTenant } from "@goauthentik/elements/sidebar/SidebarBrand";
|
||||
import { DefaultBrand } from "@goauthentik/elements/sidebar/SidebarBrand";
|
||||
import YAML from "yaml";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { TemplateResult, html } from "lit";
|
||||
import { customElement } from "lit/decorators.js";
|
||||
|
||||
import { CoreApi, FlowsInstancesListDesignationEnum, Tenant } from "@goauthentik/api";
|
||||
import { Brand, CoreApi, FlowsInstancesListDesignationEnum } from "@goauthentik/api";
|
||||
|
||||
@customElement("ak-tenant-form")
|
||||
export class TenantForm extends ModelForm<Tenant, string> {
|
||||
loadInstance(pk: string): Promise<Tenant> {
|
||||
return new CoreApi(DEFAULT_CONFIG).coreTenantsRetrieve({
|
||||
tenantUuid: pk,
|
||||
@customElement("ak-brand-form")
|
||||
export class BrandForm extends ModelForm<Brand, string> {
|
||||
loadInstance(pk: string): Promise<Brand> {
|
||||
return new CoreApi(DEFAULT_CONFIG).coreBrandsRetrieve({
|
||||
brandUuid: pk,
|
||||
});
|
||||
}
|
||||
|
||||
getSuccessMessage(): string {
|
||||
if (this.instance) {
|
||||
return msg("Successfully updated tenant.");
|
||||
return msg("Successfully updated brand.");
|
||||
} else {
|
||||
return msg("Successfully created tenant.");
|
||||
return msg("Successfully created brand.");
|
||||
}
|
||||
}
|
||||
|
||||
async send(data: Tenant): Promise<Tenant> {
|
||||
if (this.instance?.tenantUuid) {
|
||||
return new CoreApi(DEFAULT_CONFIG).coreTenantsUpdate({
|
||||
tenantUuid: this.instance.tenantUuid,
|
||||
tenantRequest: data,
|
||||
async send(data: Brand): Promise<Brand> {
|
||||
if (this.instance?.brandUuid) {
|
||||
return new CoreApi(DEFAULT_CONFIG).coreBrandsUpdate({
|
||||
brandUuid: this.instance.brandUuid,
|
||||
brandRequest: data,
|
||||
});
|
||||
} else {
|
||||
return new CoreApi(DEFAULT_CONFIG).coreTenantsCreate({
|
||||
tenantRequest: data,
|
||||
return new CoreApi(DEFAULT_CONFIG).coreBrandsCreate({
|
||||
brandRequest: data,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -79,7 +79,7 @@ export class TenantForm extends ModelForm<Tenant, string> {
|
|||
<span class="pf-c-switch__label">${msg("Default")}</span>
|
||||
</label>
|
||||
<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>
|
||||
</ak-form-element-horizontal>
|
||||
|
||||
|
@ -95,7 +95,7 @@ export class TenantForm extends ModelForm<Tenant, string> {
|
|||
type="text"
|
||||
value="${first(
|
||||
this.instance?.brandingTitle,
|
||||
DefaultTenant.brandingTitle,
|
||||
DefaultBrand.brandingTitle,
|
||||
)}"
|
||||
class="pf-c-form-control"
|
||||
required
|
||||
|
@ -111,10 +111,7 @@ export class TenantForm extends ModelForm<Tenant, string> {
|
|||
>
|
||||
<input
|
||||
type="text"
|
||||
value="${first(
|
||||
this.instance?.brandingLogo,
|
||||
DefaultTenant.brandingLogo,
|
||||
)}"
|
||||
value="${first(this.instance?.brandingLogo, DefaultBrand.brandingLogo)}"
|
||||
class="pf-c-form-control"
|
||||
required
|
||||
/>
|
||||
|
@ -131,7 +128,7 @@ export class TenantForm extends ModelForm<Tenant, string> {
|
|||
type="text"
|
||||
value="${first(
|
||||
this.instance?.brandingFavicon,
|
||||
DefaultTenant.brandingFavicon,
|
||||
DefaultBrand.brandingFavicon,
|
||||
)}"
|
||||
class="pf-c-form-control"
|
||||
required
|
||||
|
@ -274,7 +271,7 @@ export class TenantForm extends ModelForm<Tenant, string> {
|
|||
</ak-codemirror>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${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>
|
||||
</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 { uiConfig } from "@goauthentik/common/ui/config";
|
||||
import "@goauthentik/components/ak-status-label";
|
||||
|
@ -16,21 +16,21 @@ import { msg } from "@lit/localize";
|
|||
import { TemplateResult, html } from "lit";
|
||||
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")
|
||||
export class TenantListPage extends TablePage<Tenant> {
|
||||
@customElement("ak-brand-list")
|
||||
export class BrandListPage extends TablePage<Brand> {
|
||||
searchEnabled(): boolean {
|
||||
return true;
|
||||
}
|
||||
pageTitle(): string {
|
||||
return msg("Tenants");
|
||||
return msg("Brands");
|
||||
}
|
||||
pageDescription(): string {
|
||||
return msg("Configure visual settings and defaults for different domains.");
|
||||
}
|
||||
pageIcon(): string {
|
||||
return "pf-icon pf-icon-tenant";
|
||||
return "pf-icon pf-icon-brand";
|
||||
}
|
||||
|
||||
checkbox = true;
|
||||
|
@ -38,8 +38,8 @@ export class TenantListPage extends TablePage<Tenant> {
|
|||
@property()
|
||||
order = "domain";
|
||||
|
||||
async apiEndpoint(page: number): Promise<PaginatedResponse<Tenant>> {
|
||||
return new CoreApi(DEFAULT_CONFIG).coreTenantsList({
|
||||
async apiEndpoint(page: number): Promise<PaginatedResponse<Brand>> {
|
||||
return new CoreApi(DEFAULT_CONFIG).coreBrandsList({
|
||||
ordering: this.order,
|
||||
page: page,
|
||||
pageSize: (await uiConfig()).pagination.perPage,
|
||||
|
@ -58,19 +58,19 @@ export class TenantListPage extends TablePage<Tenant> {
|
|||
renderToolbarSelected(): TemplateResult {
|
||||
const disabled = this.selectedElements.length < 1;
|
||||
return html`<ak-forms-delete-bulk
|
||||
objectLabel=${msg("Tenant(s)")}
|
||||
objectLabel=${msg("Brand(s)")}
|
||||
.objects=${this.selectedElements}
|
||||
.metadata=${(item: Tenant) => {
|
||||
.metadata=${(item: Brand) => {
|
||||
return [{ key: msg("Domain"), value: item.domain }];
|
||||
}}
|
||||
.usedBy=${(item: Tenant) => {
|
||||
return new CoreApi(DEFAULT_CONFIG).coreTenantsUsedByList({
|
||||
tenantUuid: item.tenantUuid,
|
||||
.usedBy=${(item: Brand) => {
|
||||
return new CoreApi(DEFAULT_CONFIG).coreBrandsUsedByList({
|
||||
brandUuid: item.brandUuid,
|
||||
});
|
||||
}}
|
||||
.delete=${(item: Tenant) => {
|
||||
return new CoreApi(DEFAULT_CONFIG).coreTenantsDestroy({
|
||||
tenantUuid: item.tenantUuid,
|
||||
.delete=${(item: Brand) => {
|
||||
return new CoreApi(DEFAULT_CONFIG).coreBrandsDestroy({
|
||||
brandUuid: item.brandUuid,
|
||||
});
|
||||
}}
|
||||
>
|
||||
|
@ -80,14 +80,14 @@ export class TenantListPage extends TablePage<Tenant> {
|
|||
</ak-forms-delete-bulk>`;
|
||||
}
|
||||
|
||||
row(item: Tenant): TemplateResult[] {
|
||||
row(item: Brand): TemplateResult[] {
|
||||
return [
|
||||
html`${item.domain}`,
|
||||
html`<ak-status-label ?good=${item._default}></ak-status-label>`,
|
||||
html`<ak-forms-modal>
|
||||
<span slot="submit"> ${msg("Update")} </span>
|
||||
<span slot="header"> ${msg("Update Tenant")} </span>
|
||||
<ak-tenant-form slot="form" .instancePk=${item.tenantUuid}> </ak-tenant-form>
|
||||
<span slot="header"> ${msg("Update Brand")} </span>
|
||||
<ak-brand-form slot="form" .instancePk=${item.brandUuid}> </ak-brand-form>
|
||||
<button slot="trigger" class="pf-c-button pf-m-plain">
|
||||
<pf-tooltip position="top" content=${msg("Edit")}>
|
||||
<i class="fas fa-edit"></i>
|
||||
|
@ -96,8 +96,8 @@ export class TenantListPage extends TablePage<Tenant> {
|
|||
</ak-forms-modal>
|
||||
|
||||
<ak-rbac-object-permission-modal
|
||||
model=${RbacPermissionsAssignedByUsersListModelEnum.TenantsTenant}
|
||||
objectPk=${item.tenantUuid}
|
||||
model=${RbacPermissionsAssignedByUsersListModelEnum.BrandsBrand}
|
||||
objectPk=${item.brandUuid}
|
||||
>
|
||||
</ak-rbac-object-permission-modal>`,
|
||||
];
|
||||
|
@ -107,8 +107,8 @@ export class TenantListPage extends TablePage<Tenant> {
|
|||
return html`
|
||||
<ak-forms-modal>
|
||||
<span slot="submit"> ${msg("Create")} </span>
|
||||
<span slot="header"> ${msg("Create Tenant")} </span>
|
||||
<ak-tenant-form slot="form"> </ak-tenant-form>
|
||||
<span slot="header"> ${msg("Create Brand")} </span>
|
||||
<ak-brand-form slot="form"> </ak-brand-form>
|
||||
<button slot="trigger" class="pf-c-button pf-m-primary">${msg("Create")}</button>
|
||||
</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
|
||||
* 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
|
||||
* 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.
|
||||
*/
|
||||
|
||||
|
|
|
@ -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("Creation Date"), "created"),
|
||||
new TableColumn(msg("Client IP"), "client_ip"),
|
||||
new TableColumn(msg("Tenant"), "tenant_name"),
|
||||
new TableColumn(msg("Brand"), "brand_name"),
|
||||
new TableColumn(msg("Actions")),
|
||||
];
|
||||
}
|
||||
|
@ -75,7 +75,7 @@ export class EventListPage extends TablePage<Event> {
|
|||
html`<div>${item.clientIp || msg("-")}</div>
|
||||
|
||||
<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}">
|
||||
<pf-tooltip position="top" content=${msg("Show details")}>
|
||||
<i class="fas fa-share-square"></i>
|
||||
|
|
|
@ -139,12 +139,12 @@ export class EventViewPage extends AKElement {
|
|||
<div class="pf-c-description-list__group">
|
||||
<dt class="pf-c-description-list__term">
|
||||
<span class="pf-c-description-list__text"
|
||||
>${msg("Tenant")}</span
|
||||
>${msg("Brand")}</span
|
||||
>
|
||||
</dt>
|
||||
<dd class="pf-c-description-list__description">
|
||||
<div class="pf-c-description-list__text">
|
||||
${this.event.tenant?.name || msg("-")}
|
||||
${this.event.brand?.name || msg("-")}
|
||||
</div>
|
||||
</dd>
|
||||
</div>
|
||||
|
|
|
@ -293,7 +293,7 @@ export class RelatedUserList extends Table<User> {
|
|||
${msg("Set password")}
|
||||
</button>
|
||||
</ak-forms-modal>
|
||||
${rootInterface()?.tenant?.flowRecovery
|
||||
${rootInterface()?.brand?.flowRecovery
|
||||
? html`
|
||||
<ak-action-button
|
||||
class="pf-m-secondary"
|
||||
|
@ -355,7 +355,7 @@ export class RelatedUserList extends Table<User> {
|
|||
`
|
||||
: html` <p>
|
||||
${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>`}
|
||||
</div>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
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 { first } from "@goauthentik/common/utils";
|
||||
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
|
||||
// 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 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
|
||||
// field of the target Provider.
|
||||
renderForm(): TemplateResult {
|
||||
|
@ -73,12 +73,12 @@ export class LDAPProviderFormPage extends ModelForm<LDAPProvider, number> {
|
|||
?required=${true}
|
||||
name="authorizationFlow"
|
||||
>
|
||||
<ak-tenanted-flow-search
|
||||
<ak-branded-flow-search
|
||||
flowType=${FlowsInstancesListDesignationEnum.Authentication}
|
||||
.currentFlow=${this.instance?.authorizationFlow}
|
||||
.tenantFlow=${rootInterface()?.tenant?.flowAuthentication}
|
||||
.brandFlow=${rootInterface()?.brand?.flowAuthentication}
|
||||
required
|
||||
></ak-tenanted-flow-search>
|
||||
></ak-branded-flow-search>
|
||||
<p class="pf-c-form__helper-text">${msg("Flow used for users to authenticate.")}</p>
|
||||
</ak-form-element-horizontal>
|
||||
<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
|
||||
// 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 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
|
||||
// field of the target Provider.
|
||||
renderForm(): TemplateResult {
|
||||
|
@ -62,12 +62,12 @@ export class RadiusProviderFormPage extends ModelForm<RadiusProvider, number> {
|
|||
?required=${true}
|
||||
name="authorizationFlow"
|
||||
>
|
||||
<ak-tenanted-flow-search
|
||||
<ak-branded-flow-search
|
||||
flowType=${FlowsInstancesListDesignationEnum.Authentication}
|
||||
.currentFlow=${this.instance?.authorizationFlow}
|
||||
.tenantFlow=${rootInterface()?.tenant?.flowAuthentication}
|
||||
.brandFlow=${rootInterface()?.brand?.flowAuthentication}
|
||||
required
|
||||
></ak-tenanted-flow-search>
|
||||
></ak-branded-flow-search>
|
||||
<p class="pf-c-form__helper-text">${msg("Flow used for users to authenticate.")}</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal name="mfaSupport">
|
||||
|
|
|
@ -28,7 +28,7 @@ class PreviewStageHost implements StageHost {
|
|||
challenge = undefined;
|
||||
flowSlug = undefined;
|
||||
loading = false;
|
||||
tenant = undefined;
|
||||
brand = undefined;
|
||||
async submit(payload: unknown): Promise<boolean> {
|
||||
this.promptForm.previewResult = payload;
|
||||
return false;
|
||||
|
|
|
@ -63,7 +63,7 @@ export const requestRecoveryLink = (user: User) =>
|
|||
showMessage({
|
||||
level: MessageLevel.error,
|
||||
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")}
|
||||
</button>
|
||||
</ak-forms-modal>
|
||||
${rootInterface()?.tenant?.flowRecovery
|
||||
${rootInterface()?.brand?.flowRecovery
|
||||
? html`
|
||||
<ak-action-button
|
||||
class="pf-m-secondary"
|
||||
|
@ -373,7 +373,7 @@ export class UserListPage extends TablePage<User> {
|
|||
`
|
||||
: html` <p>
|
||||
${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>`}
|
||||
</div>
|
||||
|
|
|
@ -6,7 +6,7 @@ import {
|
|||
import { EVENT_LOCALE_REQUEST, EVENT_REFRESH, VERSION } from "@goauthentik/common/constants";
|
||||
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);
|
||||
export function config(): Promise<Config> {
|
||||
|
@ -16,7 +16,7 @@ export function config(): Promise<Config> {
|
|||
return globalConfigPromise;
|
||||
}
|
||||
|
||||
export function tenantSetFavicon(tenant: CurrentTenant) {
|
||||
export function brandSetFavicon(brand: CurrentBrand) {
|
||||
/**
|
||||
* <link rel="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;
|
||||
document.getElementsByTagName("head")[0].appendChild(relIcon);
|
||||
}
|
||||
relIcon.href = tenant.brandingFavicon;
|
||||
relIcon.href = brand.brandingFavicon;
|
||||
});
|
||||
}
|
||||
|
||||
export function tenantSetLocale(tenant: CurrentTenant) {
|
||||
if (tenant.defaultLocale === "") {
|
||||
export function brandSetLocale(brand: CurrentBrand) {
|
||||
if (brand.defaultLocale === "") {
|
||||
return;
|
||||
}
|
||||
console.debug("authentik/locale: setting locale from tenant default");
|
||||
console.debug("authentik/locale: setting locale from brand default");
|
||||
window.dispatchEvent(
|
||||
new CustomEvent(EVENT_LOCALE_REQUEST, {
|
||||
composed: true,
|
||||
bubbles: true,
|
||||
detail: { locale: tenant.defaultLocale },
|
||||
detail: { locale: brand.defaultLocale },
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
let globalTenantPromise: Promise<CurrentTenant> | undefined = Promise.resolve(globalAK().tenant);
|
||||
export function tenant(): Promise<CurrentTenant> {
|
||||
if (!globalTenantPromise) {
|
||||
globalTenantPromise = new CoreApi(DEFAULT_CONFIG)
|
||||
.coreTenantsCurrentRetrieve()
|
||||
.then((tenant) => {
|
||||
tenantSetFavicon(tenant);
|
||||
tenantSetLocale(tenant);
|
||||
return tenant;
|
||||
let globalBrandPromise: Promise<CurrentBrand> | undefined = Promise.resolve(globalAK().brand);
|
||||
export function brand(): Promise<CurrentBrand> {
|
||||
if (!globalBrandPromise) {
|
||||
globalBrandPromise = new CoreApi(DEFAULT_CONFIG)
|
||||
.coreBrandsCurrentRetrieve()
|
||||
.then((brand) => {
|
||||
brandSetFavicon(brand);
|
||||
brandSetLocale(brand);
|
||||
return brand;
|
||||
});
|
||||
}
|
||||
return globalTenantPromise;
|
||||
return globalBrandPromise;
|
||||
}
|
||||
|
||||
export function getMetaContent(key: string): string {
|
||||
|
@ -75,7 +75,7 @@ export const DEFAULT_CONFIG = new Configuration({
|
|||
middleware: [
|
||||
new CSRFMiddleware(),
|
||||
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
|
||||
// actually load info from API
|
||||
globalConfigPromise = undefined;
|
||||
globalTenantPromise = undefined;
|
||||
globalBrandPromise = undefined;
|
||||
config();
|
||||
tenant();
|
||||
brand();
|
||||
});
|
||||
|
||||
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 {
|
||||
CurrentTenant,
|
||||
CurrentBrand,
|
||||
FetchParams,
|
||||
Middleware,
|
||||
RequestContext,
|
||||
|
@ -18,13 +18,13 @@ export interface RequestInfo {
|
|||
}
|
||||
|
||||
export class LoggingMiddleware implements Middleware {
|
||||
tenant: CurrentTenant;
|
||||
constructor(tenant: CurrentTenant) {
|
||||
this.tenant = tenant;
|
||||
brand: CurrentBrand;
|
||||
constructor(brand: CurrentBrand) {
|
||||
this.brand = brand;
|
||||
}
|
||||
|
||||
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
|
||||
msg += `%c${context.response.status}%c ${context.init.method} ${context.url}`;
|
||||
let style = "";
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue