use wrapper for get_tenant, give fallback interfaces

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
This commit is contained in:
Jens Langhammer 2023-02-24 15:01:11 +01:00
parent c6f8290ca1
commit c0262f0802
No known key found for this signature in database
19 changed files with 536 additions and 49 deletions

View file

@ -18,6 +18,7 @@ from authentik.core.api.utils import PassiveSerializer
from authentik.lib.utils.reflection import get_env from authentik.lib.utils.reflection import get_env
from authentik.outposts.apps import MANAGED_OUTPOST from authentik.outposts.apps import MANAGED_OUTPOST
from authentik.outposts.models import Outpost from authentik.outposts.models import Outpost
from authentik.tenants.utils import get_tenant
class RuntimeDict(TypedDict): class RuntimeDict(TypedDict):
@ -77,7 +78,7 @@ class SystemSerializer(PassiveSerializer):
def get_tenant(self, request: Request) -> str: def get_tenant(self, request: Request) -> str:
"""Currently active tenant""" """Currently active tenant"""
return str(request._request.tenant) return str(get_tenant(request))
def get_server_time(self, request: Request) -> datetime: def get_server_time(self, request: Request) -> datetime:
"""Current server time""" """Current server time"""

View file

@ -76,7 +76,7 @@ from authentik.interfaces.views import reverse_interface
from authentik.stages.email.models import EmailStage from authentik.stages.email.models import EmailStage
from authentik.stages.email.tasks import send_mails from authentik.stages.email.tasks import send_mails
from authentik.stages.email.utils import TemplateEmailMessage from authentik.stages.email.utils import TemplateEmailMessage
from authentik.tenants.models import Tenant from authentik.tenants.utils import get_tenant
LOGGER = get_logger() LOGGER = get_logger()
@ -322,7 +322,7 @@ class UserViewSet(UsedByMixin, ModelViewSet):
def _create_recovery_link(self) -> tuple[Optional[str], Optional[Token]]: def _create_recovery_link(self) -> tuple[Optional[str], Optional[Token]]:
"""Create a recovery link (when the current tenant has a recovery flow set), """Create a recovery link (when the current tenant has a recovery flow set),
that can either be shown to an admin or sent to the user directly""" that can either be shown to an admin or sent to the user directly"""
tenant: Tenant = self.request._request.tenant tenant = get_tenant(self.request)
# Check that there is a recovery flow, if not return an error # Check that there is a recovery flow, if not return an error
flow = tenant.flow_recovery flow = tenant.flow_recovery
if not flow: if not flow:

View file

@ -33,6 +33,7 @@ from authentik.lib.models import (
) )
from authentik.lib.utils.http import get_client_ip from authentik.lib.utils.http import get_client_ip
from authentik.policies.models import PolicyBindingModel from authentik.policies.models import PolicyBindingModel
from authentik.tenants.utils import get_tenant
LOGGER = get_logger() LOGGER = get_logger()
USER_ATTRIBUTE_DEBUG = "goauthentik.io/user/debug" USER_ATTRIBUTE_DEBUG = "goauthentik.io/user/debug"
@ -168,7 +169,7 @@ class User(SerializerModel, GuardianUserMixin, AbstractUser):
including the users attributes""" including the users attributes"""
final_attributes = {} final_attributes = {}
if request and hasattr(request, "tenant"): if request and hasattr(request, "tenant"):
always_merger.merge(final_attributes, request.tenant.attributes) always_merger.merge(final_attributes, get_tenant(request).attributes)
for group in self.ak_groups.all().order_by("name"): for group in self.ak_groups.all().order_by("name"):
always_merger.merge(final_attributes, group.attributes) always_merger.merge(final_attributes, group.attributes)
always_merger.merge(final_attributes, self.attributes) always_merger.merge(final_attributes, self.attributes)
@ -227,7 +228,7 @@ class User(SerializerModel, GuardianUserMixin, AbstractUser):
except Exception as exc: except Exception as exc:
LOGGER.warning("Failed to get default locale", exc=exc) LOGGER.warning("Failed to get default locale", exc=exc)
if request: if request:
return request.tenant.locale return get_tenant(request).locale
return "" return ""
@property @property

View file

@ -41,8 +41,7 @@ from authentik.lib.utils.http import get_client_ip, get_http_session
from authentik.lib.utils.time import timedelta_from_string from authentik.lib.utils.time import timedelta_from_string
from authentik.policies.models import PolicyBindingModel from authentik.policies.models import PolicyBindingModel
from authentik.stages.email.utils import TemplateEmailMessage from authentik.stages.email.utils import TemplateEmailMessage
from authentik.tenants.models import Tenant from authentik.tenants.utils import get_fallback_tenant, get_tenant
from authentik.tenants.utils import DEFAULT_TENANT
LOGGER = get_logger() LOGGER = get_logger()
if TYPE_CHECKING: if TYPE_CHECKING:
@ -57,7 +56,7 @@ def default_event_duration():
def default_tenant(): def default_tenant():
"""Get a default value for tenant""" """Get a default value for tenant"""
return sanitize_dict(model_to_dict(DEFAULT_TENANT)) return sanitize_dict(model_to_dict(get_fallback_tenant()))
class NotificationTransportError(SentryIgnoredException): class NotificationTransportError(SentryIgnoredException):
@ -227,7 +226,7 @@ class Event(SerializerModel, ExpiringModel):
wrapped = self.context["http_request"]["args"][QS_QUERY] wrapped = self.context["http_request"]["args"][QS_QUERY]
self.context["http_request"]["args"] = QueryDict(wrapped) self.context["http_request"]["args"] = QueryDict(wrapped)
if hasattr(request, "tenant"): if hasattr(request, "tenant"):
tenant: Tenant = request.tenant tenant = get_tenant(request)
# Because self.created only gets set on save, we can't use it's value here # Because self.created only gets set on save, we can't use it's value here
# hence we set self.created to now and then use it # hence we set self.created to now and then use it
self.created = now() self.created = now()

View file

@ -21,7 +21,7 @@ from authentik.flows.models import Flow
from authentik.interfaces.models import Interface, InterfaceType from authentik.interfaces.models import Interface, InterfaceType
from authentik.lib.utils.urls import reverse_with_qs from authentik.lib.utils.urls import reverse_with_qs
from authentik.tenants.api import CurrentTenantSerializer from authentik.tenants.api import CurrentTenantSerializer
from authentik.tenants.models import Tenant from authentik.tenants.utils import get_tenant
LOGGER = get_logger() LOGGER = get_logger()
@ -48,7 +48,7 @@ def reverse_interface(
request: HttpRequest, interface_type: InterfaceType, query: Optional[QueryDict] = None, **kwargs request: HttpRequest, interface_type: InterfaceType, query: Optional[QueryDict] = None, **kwargs
): ):
"""Reverse URL to configured default interface""" """Reverse URL to configured default interface"""
tenant: Tenant = request.tenant tenant = get_tenant(request)
interface: Interface = None interface: Interface = None
if interface_type == InterfaceType.USER: if interface_type == InterfaceType.USER:
@ -90,7 +90,7 @@ class InterfaceView(View):
"""Get template context""" """Get template context"""
return { return {
"config_json": dumps(ConfigView(request=Request(self.request)).get_config().data), "config_json": dumps(ConfigView(request=Request(self.request)).get_config().data),
"tenant_json": dumps(CurrentTenantSerializer(self.request.tenant).data), "tenant_json": dumps(CurrentTenantSerializer(get_tenant(self.request)).data),
"version_family": f"{LOCAL_VERSION.major}.{LOCAL_VERSION.minor}", "version_family": f"{LOCAL_VERSION.major}.{LOCAL_VERSION.minor}",
"version_subdomain": f"version-{LOCAL_VERSION.major}-{LOCAL_VERSION.minor}", "version_subdomain": f"version-{LOCAL_VERSION.major}-{LOCAL_VERSION.minor}",
"build": get_build_hash(), "build": get_build_hash(),

View file

@ -12,6 +12,7 @@ from authentik.lib.utils.http import get_http_session
from authentik.policies.models import Policy from authentik.policies.models import Policy
from authentik.policies.types import PolicyRequest, PolicyResult from authentik.policies.types import PolicyRequest, PolicyResult
from authentik.stages.prompt.stage import PLAN_CONTEXT_PROMPT from authentik.stages.prompt.stage import PLAN_CONTEXT_PROMPT
from authentik.tenants.utils import get_tenant
LOGGER = get_logger() LOGGER = get_logger()
RE_LOWER = re.compile("[a-z]") RE_LOWER = re.compile("[a-z]")
@ -143,7 +144,8 @@ class PasswordPolicy(Policy):
user_inputs.append(request.user.name) user_inputs.append(request.user.name)
user_inputs.append(request.user.email) user_inputs.append(request.user.email)
if request.http_request: if request.http_request:
user_inputs.append(request.http_request.tenant.branding_title) tenant = get_tenant(request.http_request)
user_inputs.append(tenant.branding_title)
# Only calculate result for the first 100 characters, as with over 100 char # Only calculate result for the first 100 characters, as with over 100 char
# long passwords we can be reasonably sure that they'll surpass the score anyways # long passwords we can be reasonably sure that they'll surpass the score anyways
# See https://github.com/dropbox/zxcvbn#runtime-latency # See https://github.com/dropbox/zxcvbn#runtime-latency

View file

@ -27,7 +27,7 @@ from authentik.stages.consent.stage import (
PLAN_CONTEXT_CONSENT_HEADER, PLAN_CONTEXT_CONSENT_HEADER,
PLAN_CONTEXT_CONSENT_PERMISSIONS, PLAN_CONTEXT_CONSENT_PERMISSIONS,
) )
from authentik.tenants.models import Tenant from authentik.tenants.utils import get_tenant
LOGGER = get_logger() LOGGER = get_logger()
QS_KEY_CODE = "code" # nosec QS_KEY_CODE = "code" # nosec
@ -89,7 +89,7 @@ class DeviceEntryView(View):
"""View used to initiate the device-code flow, url entered by endusers""" """View used to initiate the device-code flow, url entered by endusers"""
def dispatch(self, request: HttpRequest) -> HttpResponse: def dispatch(self, request: HttpRequest) -> HttpResponse:
tenant: Tenant = request.tenant tenant = get_tenant(request)
device_flow = tenant.flow_device_code device_flow = tenant.flow_device_code
if not device_flow: if not device_flow:
LOGGER.info("Tenant has no device code flow configured", tenant=tenant) LOGGER.info("Tenant has no device code flow configured", tenant=tenant)

View file

@ -9,6 +9,7 @@ from django.views.decorators.csrf import csrf_exempt
from authentik.providers.oauth2.constants import SCOPE_GITHUB_ORG_READ, SCOPE_GITHUB_USER_EMAIL from authentik.providers.oauth2.constants import SCOPE_GITHUB_ORG_READ, SCOPE_GITHUB_USER_EMAIL
from authentik.providers.oauth2.models import RefreshToken from authentik.providers.oauth2.models import RefreshToken
from authentik.providers.oauth2.utils import protected_resource_view from authentik.providers.oauth2.utils import protected_resource_view
from authentik.tenants.utils import get_tenant
@method_decorator(csrf_exempt, name="dispatch") @method_decorator(csrf_exempt, name="dispatch")
@ -76,6 +77,7 @@ class GitHubUserTeamsView(View):
def get(self, request: HttpRequest, token: RefreshToken) -> HttpResponse: def get(self, request: HttpRequest, token: RefreshToken) -> HttpResponse:
"""Emulate GitHub's /user/teams API Endpoint""" """Emulate GitHub's /user/teams API Endpoint"""
user = token.user user = token.user
tenant = get_tenant(request)
orgs_response = [] orgs_response = []
for org in user.ak_groups.all(): for org in user.ak_groups.all():
@ -97,7 +99,7 @@ class GitHubUserTeamsView(View):
"created_at": "", "created_at": "",
"updated_at": "", "updated_at": "",
"organization": { "organization": {
"login": slugify(request.tenant.branding_title), "login": slugify(tenant.branding_title),
"id": 1, "id": 1,
"node_id": "", "node_id": "",
"url": "", "url": "",
@ -109,7 +111,7 @@ class GitHubUserTeamsView(View):
"public_members_url": "", "public_members_url": "",
"avatar_url": "", "avatar_url": "",
"description": "", "description": "",
"name": request.tenant.branding_title, "name": tenant.branding_title,
"company": "", "company": "",
"blog": "", "blog": "",
"location": "", "location": "",

View file

@ -17,6 +17,7 @@ from authentik.flows.challenge import (
from authentik.flows.stage import ChallengeStageView from authentik.flows.stage import ChallengeStageView
from authentik.stages.authenticator_totp.models import AuthenticatorTOTPStage from authentik.stages.authenticator_totp.models import AuthenticatorTOTPStage
from authentik.stages.authenticator_totp.settings import OTP_TOTP_ISSUER from authentik.stages.authenticator_totp.settings import OTP_TOTP_ISSUER
from authentik.tenants.utils import get_tenant
SESSION_TOTP_DEVICE = "totp_device" SESSION_TOTP_DEVICE = "totp_device"
@ -57,7 +58,7 @@ class AuthenticatorTOTPStageView(ChallengeStageView):
data={ data={
"type": ChallengeTypes.NATIVE.value, "type": ChallengeTypes.NATIVE.value,
"config_url": device.config_url.replace( "config_url": device.config_url.replace(
OTP_TOTP_ISSUER, quote(self.request.tenant.branding_title) OTP_TOTP_ISSUER, quote(get_tenant(self.request).branding_title)
), ),
} }
) )

View file

@ -33,6 +33,7 @@ from authentik.stages.authenticator_validate.models import AuthenticatorValidate
from authentik.stages.authenticator_webauthn.models import UserVerification, WebAuthnDevice from authentik.stages.authenticator_webauthn.models import UserVerification, WebAuthnDevice
from authentik.stages.authenticator_webauthn.stage import SESSION_KEY_WEBAUTHN_CHALLENGE from authentik.stages.authenticator_webauthn.stage import SESSION_KEY_WEBAUTHN_CHALLENGE
from authentik.stages.authenticator_webauthn.utils import get_origin, get_rp_id from authentik.stages.authenticator_webauthn.utils import get_origin, get_rp_id
from authentik.tenants.utils import get_tenant
LOGGER = get_logger() LOGGER = get_logger()
@ -187,7 +188,7 @@ def validate_challenge_duo(device_pk: int, stage_view: StageView, user: User) ->
type=__( type=__(
"%(brand_name)s Login request" "%(brand_name)s Login request"
% { % {
"brand_name": stage_view.request.tenant.branding_title, "brand_name": get_tenant(stage_view.request).branding_title,
} }
), ),
display_username=user.username, display_username=user.username,

View file

@ -19,7 +19,7 @@ from authentik.stages.authenticator_duo.models import AuthenticatorDuoStage, Duo
from authentik.stages.authenticator_validate.challenge import validate_challenge_duo from authentik.stages.authenticator_validate.challenge import validate_challenge_duo
from authentik.stages.authenticator_validate.models import AuthenticatorValidateStage, DeviceClasses from authentik.stages.authenticator_validate.models import AuthenticatorValidateStage, DeviceClasses
from authentik.stages.user_login.models import UserLoginStage from authentik.stages.user_login.models import UserLoginStage
from authentik.tenants.utils import get_tenant_for_request from authentik.tenants.utils import lookup_tenant_for_request
class AuthenticatorValidateStageDuoTests(FlowTestCase): class AuthenticatorValidateStageDuoTests(FlowTestCase):
@ -36,7 +36,7 @@ class AuthenticatorValidateStageDuoTests(FlowTestCase):
middleware = SessionMiddleware(dummy_get_response) middleware = SessionMiddleware(dummy_get_response)
middleware.process_request(request) middleware.process_request(request)
request.session.save() request.session.save()
setattr(request, "tenant", get_tenant_for_request(request)) setattr(request, "tenant", lookup_tenant_for_request(request))
stage = AuthenticatorDuoStage.objects.create( stage = AuthenticatorDuoStage.objects.create(
name=generate_id(), name=generate_id(),

View file

@ -29,6 +29,7 @@ from authentik.flows.challenge import (
from authentik.flows.stage import ChallengeStageView from authentik.flows.stage import ChallengeStageView
from authentik.stages.authenticator_webauthn.models import AuthenticateWebAuthnStage, WebAuthnDevice from authentik.stages.authenticator_webauthn.models import AuthenticateWebAuthnStage, WebAuthnDevice
from authentik.stages.authenticator_webauthn.utils import get_origin, get_rp_id from authentik.stages.authenticator_webauthn.utils import get_origin, get_rp_id
from authentik.tenants.utils import get_tenant
SESSION_KEY_WEBAUTHN_CHALLENGE = "authentik/stages/authenticator_webauthn/challenge" SESSION_KEY_WEBAUTHN_CHALLENGE = "authentik/stages/authenticator_webauthn/challenge"
@ -92,7 +93,7 @@ class AuthenticatorWebAuthnStageView(ChallengeStageView):
registration_options: PublicKeyCredentialCreationOptions = generate_registration_options( registration_options: PublicKeyCredentialCreationOptions = generate_registration_options(
rp_id=get_rp_id(self.request), rp_id=get_rp_id(self.request),
rp_name=self.request.tenant.branding_title, rp_name=get_tenant(self.request).branding_title,
user_id=user.uid, user_id=user.uid,
user_name=user.username, user_name=user.username,
user_display_name=user.name, user_display_name=user.name,

View file

@ -18,6 +18,7 @@ from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import PassiveSerializer from authentik.core.api.utils import PassiveSerializer
from authentik.lib.config import CONFIG from authentik.lib.config import CONFIG
from authentik.tenants.models import Tenant from authentik.tenants.models import Tenant
from authentik.tenants.utils import get_tenant
class FooterLinkSerializer(PassiveSerializer): class FooterLinkSerializer(PassiveSerializer):
@ -139,5 +140,4 @@ class TenantViewSet(UsedByMixin, ModelViewSet):
@action(methods=["GET"], detail=False, permission_classes=[AllowAny]) @action(methods=["GET"], detail=False, permission_classes=[AllowAny])
def current(self, request: Request) -> Response: def current(self, request: Request) -> Response:
"""Get current tenant""" """Get current tenant"""
tenant: Tenant = request._request.tenant return Response(CurrentTenantSerializer(get_tenant(request)).data)
return Response(CurrentTenantSerializer(tenant).data)

View file

@ -6,7 +6,7 @@ from django.http.response import HttpResponse
from django.utils.translation import activate from django.utils.translation import activate
from sentry_sdk.api import set_tag from sentry_sdk.api import set_tag
from authentik.tenants.utils import get_tenant_for_request from authentik.tenants.utils import lookup_tenant_for_request
class TenantMiddleware: class TenantMiddleware:
@ -19,7 +19,7 @@ class TenantMiddleware:
def __call__(self, request: HttpRequest) -> HttpResponse: def __call__(self, request: HttpRequest) -> HttpResponse:
if not hasattr(request, "tenant"): if not hasattr(request, "tenant"):
tenant = get_tenant_for_request(request) tenant = lookup_tenant_for_request(request)
setattr(request, "tenant", tenant) setattr(request, "tenant", tenant)
set_tag("authentik.tenant_uuid", tenant.tenant_uuid.hex) set_tag("authentik.tenant_uuid", tenant.tenant_uuid.hex)
set_tag("authentik.tenant_domain", tenant.domain) set_tag("authentik.tenant_domain", tenant.domain)

View file

@ -13,7 +13,7 @@ def migrate_set_default(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
from authentik.blueprints.models import BlueprintInstance from authentik.blueprints.models import BlueprintInstance
from authentik.blueprints.v1.importer import Importer from authentik.blueprints.v1.importer import Importer
from authentik.blueprints.v1.tasks import blueprints_discover from authentik.blueprints.v1.tasks import blueprints_discovery
from authentik.interfaces.models import InterfaceType from authentik.interfaces.models import InterfaceType
# If we don't have any tenants yet, we don't need wait for the default interface blueprint # If we don't have any tenants yet, we don't need wait for the default interface blueprint
@ -22,7 +22,7 @@ def migrate_set_default(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
interface_blueprint = BlueprintInstance.objects.filter(path="system/interfaces.yaml").first() interface_blueprint = BlueprintInstance.objects.filter(path="system/interfaces.yaml").first()
if not interface_blueprint: if not interface_blueprint:
blueprints_discover.delay().get() blueprints_discovery.delay().get()
interface_blueprint = BlueprintInstance.objects.filter( interface_blueprint = BlueprintInstance.objects.filter(
path="system/interfaces.yaml" path="system/interfaces.yaml"
).first() ).first()

View file

@ -7,8 +7,6 @@ from rest_framework.serializers import Serializer
from structlog.stdlib import get_logger from structlog.stdlib import get_logger
from authentik.crypto.models import CertificateKeyPair from authentik.crypto.models import CertificateKeyPair
from authentik.flows.models import Flow
from authentik.interfaces.models import Interface
from authentik.lib.models import SerializerModel from authentik.lib.models import SerializerModel
from authentik.lib.utils.time import timedelta_string_validator from authentik.lib.utils.time import timedelta_string_validator
@ -34,38 +32,56 @@ class Tenant(SerializerModel):
branding_favicon = models.TextField(default="/static/dist/assets/icons/icon.png") branding_favicon = models.TextField(default="/static/dist/assets/icons/icon.png")
flow_authentication = models.ForeignKey( flow_authentication = models.ForeignKey(
Flow, null=True, on_delete=models.SET_NULL, related_name="tenant_authentication" "authentik_flows.Flow",
null=True,
on_delete=models.SET_NULL,
related_name="tenant_authentication",
) )
flow_invalidation = models.ForeignKey( flow_invalidation = models.ForeignKey(
Flow, null=True, on_delete=models.SET_NULL, related_name="tenant_invalidation" "authentik_flows.Flow",
null=True,
on_delete=models.SET_NULL,
related_name="tenant_invalidation",
) )
flow_recovery = models.ForeignKey( flow_recovery = models.ForeignKey(
Flow, null=True, on_delete=models.SET_NULL, related_name="tenant_recovery" "authentik_flows.Flow", null=True, on_delete=models.SET_NULL, related_name="tenant_recovery"
) )
flow_unenrollment = models.ForeignKey( flow_unenrollment = models.ForeignKey(
Flow, null=True, on_delete=models.SET_NULL, related_name="tenant_unenrollment" "authentik_flows.Flow",
null=True,
on_delete=models.SET_NULL,
related_name="tenant_unenrollment",
) )
flow_user_settings = models.ForeignKey( flow_user_settings = models.ForeignKey(
Flow, null=True, on_delete=models.SET_NULL, related_name="tenant_user_settings" "authentik_flows.Flow",
null=True,
on_delete=models.SET_NULL,
related_name="tenant_user_settings",
) )
flow_device_code = models.ForeignKey( flow_device_code = models.ForeignKey(
Flow, null=True, on_delete=models.SET_NULL, related_name="tenant_device_code" "authentik_flows.Flow",
null=True,
on_delete=models.SET_NULL,
related_name="tenant_device_code",
) )
interface_flow = models.ForeignKey( interface_flow = models.ForeignKey(
Interface, "authentik_interfaces.Interface",
default=None,
on_delete=models.SET_NULL, on_delete=models.SET_NULL,
null=True, null=True,
related_name="tenant_flow", related_name="tenant_flow",
) )
interface_user = models.ForeignKey( interface_user = models.ForeignKey(
Interface, "authentik_interfaces.Interface",
default=None,
on_delete=models.SET_NULL, on_delete=models.SET_NULL,
null=True, null=True,
related_name="tenant_user", related_name="tenant_user",
) )
interface_admin = models.ForeignKey( interface_admin = models.ForeignKey(
Interface, "authentik_interfaces.Interface",
default=None,
on_delete=models.SET_NULL, on_delete=models.SET_NULL,
null=True, null=True,
related_name="tenant_admin", related_name="tenant_admin",

View file

@ -75,7 +75,7 @@ class TestTenants(APITestCase):
) )
factory = RequestFactory() factory = RequestFactory()
request = factory.get("/") request = factory.get("/")
request.tenant = tenant setattr(request, "tenant", tenant)
event = Event.new(action=EventAction.SYSTEM_EXCEPTION, message="test").from_http(request) event = Event.new(action=EventAction.SYSTEM_EXCEPTION, message="test").from_http(request)
self.assertEqual(event.expires.day, (event.created + timedelta_from_string("weeks=3")).day) self.assertEqual(event.expires.day, (event.created + timedelta_from_string("weeks=3")).day)
self.assertEqual( self.assertEqual(

View file

@ -1,27 +1,47 @@
"""Tenant utilities""" """Tenant utilities"""
from functools import cache
from typing import Any from typing import Any
from django.db.models import F, Q from django.db.models import F, Q
from django.db.models import Value as V from django.db.models import Value as V
from django.http.request import HttpRequest from django.http.request import HttpRequest
from rest_framework.request import Request
from sentry_sdk.hub import Hub from sentry_sdk.hub import Hub
from authentik import get_full_version from authentik import get_full_version
from authentik.interfaces.models import Interface, InterfaceType
from authentik.lib.config import CONFIG from authentik.lib.config import CONFIG
from authentik.tenants.models import Tenant from authentik.tenants.models import Tenant
_q_default = Q(default=True) _q_default = Q(default=True)
@cache
def get_fallback_tenant(): def get_fallback_tenant():
"""Get fallback tenant""" """Get fallback tenant"""
return Tenant(domain="fallback")
fallback_interface = Interface(
url_name="fallback",
type=InterfaceType.FLOW,
template="Fallback interface",
)
return Tenant(
domain="fallback",
interface_flow=fallback_interface,
interface_user=fallback_interface,
interface_admin=fallback_interface,
)
def get_tenant_for_request(request: HttpRequest) -> Tenant: def get_tenant(request: HttpRequest | Request) -> "Tenant":
"""Get the request's tenant, falls back to a fallback tenant object"""
if isinstance(request, Request):
request = request._request
return getattr(request, "tenant", get_fallback_tenant())
def lookup_tenant_for_request(request: HttpRequest) -> "Tenant":
"""Get tenant object for current request""" """Get tenant object for current request"""
from authentik.tenants.models import Tenant
db_tenants = ( db_tenants = (
Tenant.objects.annotate(host_domain=V(request.get_host())) Tenant.objects.annotate(host_domain=V(request.get_host()))
.filter(Q(host_domain__iendswith=F("domain")) | _q_default) .filter(Q(host_domain__iendswith=F("domain")) | _q_default)
@ -29,13 +49,13 @@ def get_tenant_for_request(request: HttpRequest) -> Tenant:
) )
tenants = list(db_tenants.all()) tenants = list(db_tenants.all())
if len(tenants) < 1: if len(tenants) < 1:
return DEFAULT_TENANT return get_fallback_tenant()
return tenants[0] return tenants[0]
def context_processor(request: HttpRequest) -> dict[str, Any]: def context_processor(request: HttpRequest) -> dict[str, Any]:
"""Context Processor that injects tenant object into every template""" """Context Processor that injects tenant object into every template"""
tenant = getattr(request, "tenant", DEFAULT_TENANT) tenant = getattr(request, "tenant", get_fallback_tenant())
trace = "" trace = ""
span = Hub.current.scope.span span = Hub.current.scope.span
if span: if span:
@ -46,6 +66,3 @@ def context_processor(request: HttpRequest) -> dict[str, Any]:
"sentry_trace": trace, "sentry_trace": trace,
"version": get_full_version(), "version": get_full_version(),
} }
DEFAULT_TENANT = get_fallback_tenant()

View file

@ -3676,6 +3676,24 @@ paths:
description: flow_user_settings description: flow_user_settings
schema: schema:
type: string type: string
- name: interface_admin
required: false
in: query
description: interface_admin
schema:
type: string
- name: interface_flow
required: false
in: query
description: interface_flow
schema:
type: string
- name: interface_user
required: false
in: query
description: interface_user
schema:
type: string
- name: ordering - name: ordering
required: false required: false
in: query in: query
@ -7731,6 +7749,295 @@ paths:
schema: schema:
$ref: '#/components/schemas/GenericError' $ref: '#/components/schemas/GenericError'
description: '' description: ''
/interfaces/:
get:
operationId: interfaces_list
description: Interface serializer
parameters:
- name: ordering
required: false
in: query
description: Which field to use when ordering the results.
schema:
type: string
- name: page
required: false
in: query
description: A page number within the paginated result set.
schema:
type: integer
- name: page_size
required: false
in: query
description: Number of results to return per page.
schema:
type: integer
- name: search
required: false
in: query
description: A search term.
schema:
type: string
- in: query
name: template
schema:
type: string
- in: query
name: type
schema:
type: string
enum:
- admin
- flow
- user
description: |-
* `user` - User
* `admin` - Admin
* `flow` - Flow
* `user` - User
* `admin` - Admin
* `flow` - Flow
- in: query
name: url_name
schema:
type: string
tags:
- interfaces
security:
- authentik: []
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/PaginatedInterfaceList'
description: ''
'400':
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
description: ''
'403':
content:
application/json:
schema:
$ref: '#/components/schemas/GenericError'
description: ''
post:
operationId: interfaces_create
description: Interface serializer
tags:
- interfaces
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/InterfaceRequest'
required: true
security:
- authentik: []
responses:
'201':
content:
application/json:
schema:
$ref: '#/components/schemas/Interface'
description: ''
'400':
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
description: ''
'403':
content:
application/json:
schema:
$ref: '#/components/schemas/GenericError'
description: ''
/interfaces/{interface_uuid}/:
get:
operationId: interfaces_retrieve
description: Interface serializer
parameters:
- in: path
name: interface_uuid
schema:
type: string
format: uuid
description: A UUID string identifying this interface.
required: true
tags:
- interfaces
security:
- authentik: []
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/Interface'
description: ''
'400':
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
description: ''
'403':
content:
application/json:
schema:
$ref: '#/components/schemas/GenericError'
description: ''
put:
operationId: interfaces_update
description: Interface serializer
parameters:
- in: path
name: interface_uuid
schema:
type: string
format: uuid
description: A UUID string identifying this interface.
required: true
tags:
- interfaces
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/InterfaceRequest'
required: true
security:
- authentik: []
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/Interface'
description: ''
'400':
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
description: ''
'403':
content:
application/json:
schema:
$ref: '#/components/schemas/GenericError'
description: ''
patch:
operationId: interfaces_partial_update
description: Interface serializer
parameters:
- in: path
name: interface_uuid
schema:
type: string
format: uuid
description: A UUID string identifying this interface.
required: true
tags:
- interfaces
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/PatchedInterfaceRequest'
security:
- authentik: []
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/Interface'
description: ''
'400':
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
description: ''
'403':
content:
application/json:
schema:
$ref: '#/components/schemas/GenericError'
description: ''
delete:
operationId: interfaces_destroy
description: Interface serializer
parameters:
- in: path
name: interface_uuid
schema:
type: string
format: uuid
description: A UUID string identifying this interface.
required: true
tags:
- interfaces
security:
- authentik: []
responses:
'204':
description: No response body
'400':
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
description: ''
'403':
content:
application/json:
schema:
$ref: '#/components/schemas/GenericError'
description: ''
/interfaces/{interface_uuid}/used_by/:
get:
operationId: interfaces_used_by_list
description: Get a list of all objects that use this object
parameters:
- in: path
name: interface_uuid
schema:
type: string
format: uuid
description: A UUID string identifying this interface.
required: true
tags:
- interfaces
security:
- authentik: []
responses:
'200':
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/UsedBy'
description: ''
'400':
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
description: ''
'403':
content:
application/json:
schema:
$ref: '#/components/schemas/GenericError'
description: ''
/managed/blueprints/: /managed/blueprints/:
get: get:
operationId: managed_blueprints_list operationId: managed_blueprints_list
@ -26307,6 +26614,7 @@ components:
- authentik.admin - authentik.admin
- authentik.api - authentik.api
- authentik.crypto - authentik.crypto
- authentik.interfaces
- authentik.events - authentik.events
- authentik.flows - authentik.flows
- authentik.lib - authentik.lib
@ -26357,6 +26665,7 @@ components:
* `authentik.admin` - authentik Admin * `authentik.admin` - authentik Admin
* `authentik.api` - authentik API * `authentik.api` - authentik API
* `authentik.crypto` - authentik Crypto * `authentik.crypto` - authentik Crypto
* `authentik.interfaces` - authentik Interfaces
* `authentik.events` - authentik Events * `authentik.events` - authentik Events
* `authentik.flows` - authentik Flows * `authentik.flows` - authentik Flows
* `authentik.lib` - authentik lib * `authentik.lib` - authentik lib
@ -29074,6 +29383,7 @@ components:
* `authentik.admin` - authentik Admin * `authentik.admin` - authentik Admin
* `authentik.api` - authentik API * `authentik.api` - authentik API
* `authentik.crypto` - authentik Crypto * `authentik.crypto` - authentik Crypto
* `authentik.interfaces` - authentik Interfaces
* `authentik.events` - authentik Events * `authentik.events` - authentik Events
* `authentik.flows` - authentik Flows * `authentik.flows` - authentik Flows
* `authentik.lib` - authentik lib * `authentik.lib` - authentik lib
@ -29184,6 +29494,7 @@ components:
* `authentik.admin` - authentik Admin * `authentik.admin` - authentik Admin
* `authentik.api` - authentik API * `authentik.api` - authentik API
* `authentik.crypto` - authentik Crypto * `authentik.crypto` - authentik Crypto
* `authentik.interfaces` - authentik Interfaces
* `authentik.events` - authentik Events * `authentik.events` - authentik Events
* `authentik.flows` - authentik Flows * `authentik.flows` - authentik Flows
* `authentik.lib` - authentik lib * `authentik.lib` - authentik lib
@ -30297,6 +30608,55 @@ components:
* `api` - Intent Api * `api` - Intent Api
* `recovery` - Intent Recovery * `recovery` - Intent Recovery
* `app_password` - Intent App Password * `app_password` - Intent App Password
Interface:
type: object
description: Interface serializer
properties:
interface_uuid:
type: string
format: uuid
readOnly: true
url_name:
type: string
maxLength: 50
pattern: ^[-a-zA-Z0-9_]+$
type:
$ref: '#/components/schemas/InterfaceTypeEnum'
template:
type: string
required:
- interface_uuid
- template
- type
- url_name
InterfaceRequest:
type: object
description: Interface serializer
properties:
url_name:
type: string
minLength: 1
maxLength: 50
pattern: ^[-a-zA-Z0-9_]+$
type:
$ref: '#/components/schemas/InterfaceTypeEnum'
template:
type: string
minLength: 1
required:
- template
- type
- url_name
InterfaceTypeEnum:
enum:
- user
- admin
- flow
type: string
description: |-
* `user` - User
* `admin` - Admin
* `flow` - Flow
InvalidResponseActionEnum: InvalidResponseActionEnum:
enum: enum:
- retry - retry
@ -33051,6 +33411,41 @@ components:
required: required:
- pagination - pagination
- results - results
PaginatedInterfaceList:
type: object
properties:
pagination:
type: object
properties:
next:
type: number
previous:
type: number
count:
type: number
current:
type: number
total_pages:
type: number
start_index:
type: number
end_index:
type: number
required:
- next
- previous
- count
- current
- total_pages
- start_index
- end_index
results:
type: array
items:
$ref: '#/components/schemas/Interface'
required:
- pagination
- results
PaginatedInvitationList: PaginatedInvitationList:
type: object type: object
properties: properties:
@ -35870,6 +36265,7 @@ components:
* `authentik.admin` - authentik Admin * `authentik.admin` - authentik Admin
* `authentik.api` - authentik API * `authentik.api` - authentik API
* `authentik.crypto` - authentik Crypto * `authentik.crypto` - authentik Crypto
* `authentik.interfaces` - authentik Interfaces
* `authentik.events` - authentik Events * `authentik.events` - authentik Events
* `authentik.flows` - authentik Flows * `authentik.flows` - authentik Flows
* `authentik.lib` - authentik lib * `authentik.lib` - authentik lib
@ -36121,6 +36517,20 @@ components:
description: Specify which sources should be shown. description: Specify which sources should be shown.
show_source_labels: show_source_labels:
type: boolean type: boolean
PatchedInterfaceRequest:
type: object
description: Interface serializer
properties:
url_name:
type: string
minLength: 1
maxLength: 50
pattern: ^[-a-zA-Z0-9_]+$
type:
$ref: '#/components/schemas/InterfaceTypeEnum'
template:
type: string
minLength: 1
PatchedInvitationRequest: PatchedInvitationRequest:
type: object type: object
description: Invitation Serializer description: Invitation Serializer
@ -37405,6 +37815,18 @@ components:
type: string type: string
format: uuid format: uuid
nullable: true nullable: true
interface_admin:
type: string
format: uuid
nullable: true
interface_user:
type: string
format: uuid
nullable: true
interface_flow:
type: string
format: uuid
nullable: true
event_retention: event_retention:
type: string type: string
minLength: 1 minLength: 1
@ -40576,6 +40998,18 @@ components:
type: string type: string
format: uuid format: uuid
nullable: true nullable: true
interface_admin:
type: string
format: uuid
nullable: true
interface_user:
type: string
format: uuid
nullable: true
interface_flow:
type: string
format: uuid
nullable: true
event_retention: event_retention:
type: string type: string
description: 'Events will be deleted after this duration.(Format: weeks=3;days=2;hours=3,seconds=2).' description: 'Events will be deleted after this duration.(Format: weeks=3;days=2;hours=3,seconds=2).'
@ -40634,6 +41068,18 @@ components:
type: string type: string
format: uuid format: uuid
nullable: true nullable: true
interface_admin:
type: string
format: uuid
nullable: true
interface_user:
type: string
format: uuid
nullable: true
interface_flow:
type: string
format: uuid
nullable: true
event_retention: event_retention:
type: string type: string
minLength: 1 minLength: 1