diff --git a/Makefile b/Makefile index 3c925de9d..79dda0454 100644 --- a/Makefile +++ b/Makefile @@ -94,8 +94,14 @@ dev-reset: dev-drop-db dev-create-db migrate ## Drop and restore the Authentik ######################### gen-build: ## Extract the schema from the database - AUTHENTIK_DEBUG=true ak make_blueprint_schema > blueprints/schema.json - AUTHENTIK_DEBUG=true ak spectacular --file schema.yml + AUTHENTIK_DEBUG=true \ + AUTHENTIK_TENANTS__ENABLED=true \ + AUTHENTIK_OUTPOSTS__DISABLE_EMBEDDED_OUTPOST=true \ + ak make_blueprint_schema > blueprints/schema.json + AUTHENTIK_DEBUG=true \ + AUTHENTIK_TENANTS__ENABLED=true \ + AUTHENTIK_OUTPOSTS__DISABLE_EMBEDDED_OUTPOST=true \ + ak spectacular --file schema.yml gen-changelog: ## (Release) generate the changelog based from the commits since the last tag git log --pretty=format:" - %s" $(shell git describe --tags $(shell git rev-list --tags --max-count=1))...$(shell git branch --show-current) | sort > changelog.md diff --git a/authentik/tenants/api/__init__.py b/authentik/tenants/api/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/authentik/tenants/api/domains.py b/authentik/tenants/api/domains.py new file mode 100644 index 000000000..878223fb7 --- /dev/null +++ b/authentik/tenants/api/domains.py @@ -0,0 +1,32 @@ +"""Serializer for tenants models""" +from rest_framework.filters import OrderingFilter, SearchFilter +from rest_framework.serializers import ModelSerializer +from rest_framework.viewsets import ModelViewSet + +from authentik.tenants.api.tenants import TenantApiKeyPermission +from authentik.tenants.models import Domain + + +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", + "tenant__name", + "tenant__schema_name", + ] + ordering = ["domain"] + authentication_classes = [] + permission_classes = [TenantApiKeyPermission] + filter_backends = [OrderingFilter, SearchFilter] + filterset_fields = [] diff --git a/authentik/tenants/api/settings.py b/authentik/tenants/api/settings.py new file mode 100644 index 000000000..acabcb909 --- /dev/null +++ b/authentik/tenants/api/settings.py @@ -0,0 +1,51 @@ +"""Serializer for tenants models""" +from django_tenants.utils import get_public_schema_name +from rest_framework.generics import RetrieveUpdateAPIView +from rest_framework.permissions import SAFE_METHODS +from rest_framework.serializers import ModelSerializer + +from authentik.rbac.permissions import HasPermission +from authentik.tenants.models import Tenant + +class SettingsSerializer(ModelSerializer): + """Settings Serializer""" + + class Meta: + model = Tenant + fields = [ + "avatars", + "default_user_change_name", + "default_user_change_email", + "default_user_change_username", + "event_retention", + "footer_links", + "gdpr_compliance", + "impersonation", + ] + + +class SettingsView(RetrieveUpdateAPIView): + """Settings view""" + + queryset = Tenant.objects.filter(ready=True) + serializer_class = SettingsSerializer + filter_backends = [] + + def get_permissions(self): + return [ + HasPermission( + "authentik_rbac.view_system_settings" + if self.request.method in SAFE_METHODS + else "authentik_rbac.edit_system_settings" + )() + ] + + def get_object(self): + obj = self.request.tenant + self.check_object_permissions(self.request, obj) + return obj + + def perform_update(self, serializer): + # We need to be in the public schema to actually modify a tenant + with Tenant.objects.get(schema_name=get_public_schema_name()): + super().perform_update(serializer) diff --git a/authentik/tenants/api.py b/authentik/tenants/api/tenants.py similarity index 66% rename from authentik/tenants/api.py rename to authentik/tenants/api/tenants.py index 53d681426..ba0b394f9 100644 --- a/authentik/tenants/api.py +++ b/authentik/tenants/api/tenants.py @@ -5,15 +5,12 @@ from hmac import compare_digest from django.http import HttpResponseNotFound from django.http.request import urljoin from django.utils.timezone import now -from django_tenants.utils import get_public_schema_name from drf_spectacular.utils import OpenApiResponse, extend_schema from rest_framework import permissions from rest_framework.authentication import get_authorization_header from rest_framework.decorators import action from rest_framework.fields import CharField, IntegerField from rest_framework.filters import OrderingFilter, SearchFilter -from rest_framework.generics import RetrieveUpdateAPIView -from rest_framework.permissions import SAFE_METHODS from rest_framework.request import Request from rest_framework.response import Response from rest_framework.serializers import DateTimeField, ModelSerializer @@ -24,9 +21,8 @@ from authentik.api.authentication import validate_auth from authentik.core.api.utils import PassiveSerializer from authentik.core.models import User from authentik.lib.config import CONFIG -from authentik.rbac.permissions import HasPermission from authentik.recovery.lib import create_admin_group, create_recovery_token -from authentik.tenants.models import Domain, Tenant +from authentik.tenants.models import Tenant class TenantApiKeyPermission(permissions.BasePermission): @@ -91,11 +87,6 @@ class TenantViewSet(ModelViewSet): filter_backends = [OrderingFilter, SearchFilter] filterset_fields = [] - def dispatch(self, request, *args, **kwargs): - if not CONFIG.get_bool("tenants.enabled", True): - return HttpResponseNotFound() - return super().dispatch(request, *args, **kwargs) - @extend_schema( request=TenantAdminGroupRequestSerializer(), responses={ @@ -150,76 +141,3 @@ class TenantViewSet(ModelViewSet): serializer = TenantRecoveryKeyResponseSerializer({"expiry": token.expires, "url": url}) return Response(serializer.data) - -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", - "tenant__name", - "tenant__schema_name", - ] - ordering = ["domain"] - authentication_classes = [] - permission_classes = [TenantApiKeyPermission] - filter_backends = [OrderingFilter, SearchFilter] - filterset_fields = [] - - def dispatch(self, request, *args, **kwargs): - if not CONFIG.get_bool("tenants.enabled", True): - return HttpResponseNotFound() - return super().dispatch(request, *args, **kwargs) - - -class SettingsSerializer(ModelSerializer): - """Settings Serializer""" - - class Meta: - model = Tenant - fields = [ - "avatars", - "default_user_change_name", - "default_user_change_email", - "default_user_change_username", - "event_retention", - "footer_links", - "gdpr_compliance", - "impersonation", - ] - - -class SettingsView(RetrieveUpdateAPIView): - """Settings view""" - - queryset = Tenant.objects.filter(ready=True) - serializer_class = SettingsSerializer - filter_backends = [] - - def get_permissions(self): - return [ - HasPermission( - "authentik_rbac.view_system_settings" - if self.request.method in SAFE_METHODS - else "authentik_rbac.edit_system_settings" - )() - ] - - def get_object(self): - obj = self.request.tenant - self.check_object_permissions(self.request, obj) - return obj - - def perform_update(self, serializer): - # We need to be in the public schema to actually modify a tenant - with Tenant.objects.get(schema_name=get_public_schema_name()): - super().perform_update(serializer) diff --git a/authentik/tenants/checks.py b/authentik/tenants/checks.py index baaf902ef..7b63c261d 100644 --- a/authentik/tenants/checks.py +++ b/authentik/tenants/checks.py @@ -14,7 +14,7 @@ def check_embedded_outpost_disabled(app_configs, **kwargs): Error( "Embedded outpost must be disabled when tenants API is enabled.", hint="Disable embedded outpost by setting outposts.disable_embedded_outpost to " - "False, or disable the tenants API by setting tenants.enabled to False", + "True, or disable the tenants API by setting tenants.enabled to False", ) ] return [] diff --git a/authentik/tenants/models.py b/authentik/tenants/models.py index 603cd22f3..075195646 100644 --- a/authentik/tenants/models.py +++ b/authentik/tenants/models.py @@ -93,7 +93,7 @@ class Tenant(TenantMixin, SerializerModel): @property def serializer(self) -> Serializer: - from authentik.tenants.api import TenantSerializer + from authentik.tenants.api.tenants import TenantSerializer return TenantSerializer @@ -117,7 +117,7 @@ class Domain(DomainMixin, SerializerModel): @property def serializer(self) -> Serializer: - from authentik.tenants.api import DomainSerializer + from authentik.tenants.api.domains import DomainSerializer return DomainSerializer diff --git a/authentik/tenants/urls.py b/authentik/tenants/urls.py index 827d9fcf1..94d1c6ea1 100644 --- a/authentik/tenants/urls.py +++ b/authentik/tenants/urls.py @@ -1,16 +1,17 @@ """API URLs""" from django.urls import path +from authentik.lib.config import CONFIG -from authentik.tenants.api import DomainViewSet, SettingsView, TenantViewSet +from authentik.tenants.api.tenants import TenantViewSet +from authentik.tenants.api.domains import DomainViewSet +from authentik.tenants.api.settings import SettingsView api_urlpatterns = [ path("admin/settings/", SettingsView.as_view(), name="tenant_settings"), - ( - "tenants/tenants", - TenantViewSet, - ), - ( - "tenants/domains", - DomainViewSet, - ), ] + +if CONFIG.get_bool("tenants.enabled", True): + api_urlpatterns += [ + ("tenants/tenants", TenantViewSet), + ("tenants/domains", DomainViewSet), + ]