blueprints: add meta model to apply blueprint within blueprint for dependencies (#3486)

* add meta model to apply blueprint within blueprint for dependencies

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* fix tests

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* use custom registry

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* fix again

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* move ManagedAppConfig to apps.py

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* rename manager to registry

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* ci: use full tag in comment

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
This commit is contained in:
Jens L 2022-08-29 21:20:58 +02:00 committed by GitHub
parent 58e3ca28be
commit 54ba3e9616
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
48 changed files with 269 additions and 133 deletions

View File

@ -83,8 +83,6 @@ runs:
image: image:
repository: ghcr.io/goauthentik/dev-server repository: ghcr.io/goauthentik/dev-server
tag: ${{ inputs.tag }} tag: ${{ inputs.tag }}
# pullPolicy: Always to ensure you always get the latest version
pullPolicy: Always
``` ```
Afterwards, run the upgrade commands from the latest release notes. Afterwards, run the upgrade commands from the latest release notes.

View File

@ -240,4 +240,4 @@ jobs:
continue-on-error: true continue-on-error: true
uses: ./.github/actions/comment-pr-instructions uses: ./.github/actions/comment-pr-instructions
with: with:
tag: gh-${{ steps.ev.outputs.branchNameContainer }} tag: gh-${{ steps.ev.outputs.branchNameContainer }}-${{ steps.ev.outputs.timestamp }}-${{ steps.ev.outputs.sha }}

View File

@ -1,7 +1,7 @@
"""authentik admin app config""" """authentik admin app config"""
from prometheus_client import Gauge, Info from prometheus_client import Gauge, Info
from authentik.blueprints.manager import ManagedAppConfig from authentik.blueprints.apps import ManagedAppConfig
PROM_INFO = Info("authentik_version", "Currently running authentik version") PROM_INFO = Info("authentik_version", "Currently running authentik version")
GAUGE_WORKERS = Gauge("authentik_admin_workers", "Currently connected workers") GAUGE_WORKERS = Gauge("authentik_admin_workers", "Currently connected workers")

View File

@ -1,6 +1,46 @@
"""authentik Blueprints app""" """authentik Blueprints app"""
from authentik.blueprints.manager import ManagedAppConfig from importlib import import_module
from inspect import ismethod
from django.apps import AppConfig
from django.db import DatabaseError, InternalError, ProgrammingError
from structlog.stdlib import BoundLogger, get_logger
class ManagedAppConfig(AppConfig):
"""Basic reconciliation logic for apps"""
_logger: BoundLogger
def __init__(self, app_name: str, *args, **kwargs) -> None:
super().__init__(app_name, *args, **kwargs)
self._logger = get_logger().bind(app_name=app_name)
def ready(self) -> None:
self.reconcile()
return super().ready()
def import_module(self, path: str):
"""Load module"""
import_module(path)
def reconcile(self) -> None:
"""reconcile ourselves"""
prefix = "reconcile_"
for meth_name in dir(self):
meth = getattr(self, meth_name)
if not ismethod(meth):
continue
if not meth_name.startswith(prefix):
continue
name = meth_name.replace(prefix, "")
try:
self._logger.debug("Starting reconciler", name=name)
meth()
self._logger.debug("Successfully reconciled", name=name)
except (DatabaseError, ProgrammingError, InternalError) as exc:
self._logger.debug("Failed to run reconcile", name=name, exc=exc)
class AuthentikBlueprintsConfig(ManagedAppConfig): class AuthentikBlueprintsConfig(ManagedAppConfig):
@ -15,6 +55,10 @@ class AuthentikBlueprintsConfig(ManagedAppConfig):
"""Load v1 tasks""" """Load v1 tasks"""
self.import_module("authentik.blueprints.v1.tasks") self.import_module("authentik.blueprints.v1.tasks")
def reconcile_load_blueprints_v1_meta(self):
"""Load v1 meta models"""
self.import_module("authentik.blueprints.v1.meta.apply_blueprint")
def reconcile_blueprints_discover(self): def reconcile_blueprints_discover(self):
"""Run blueprint discovery""" """Run blueprint discovery"""
from authentik.blueprints.v1.tasks import blueprints_discover from authentik.blueprints.v1.tasks import blueprints_discover

View File

@ -2,11 +2,11 @@
from json import dumps, loads from json import dumps, loads
from pathlib import Path from pathlib import Path
from django.apps import apps
from django.core.management.base import BaseCommand, no_translations from django.core.management.base import BaseCommand, no_translations
from structlog.stdlib import get_logger from structlog.stdlib import get_logger
from authentik.blueprints.v1.importer import is_model_allowed from authentik.blueprints.v1.importer import is_model_allowed
from authentik.blueprints.v1.meta.registry import registry
from authentik.lib.models import SerializerModel from authentik.lib.models import SerializerModel
LOGGER = get_logger() LOGGER = get_logger()
@ -29,10 +29,11 @@ class Command(BaseCommand):
def set_model_allowed(self): def set_model_allowed(self):
"""Set model enum""" """Set model enum"""
model_names = [] model_names = []
for model in apps.get_models(): for model in registry.get_models():
if not is_model_allowed(model): if not is_model_allowed(model):
continue continue
if SerializerModel not in model.__mro__: if SerializerModel not in model.__mro__:
continue continue
model_names.append(f"{model._meta.app_label}.{model._meta.model_name}") model_names.append(f"{model._meta.app_label}.{model._meta.model_name}")
model_names.sort()
self.schema["properties"]["entries"]["items"]["properties"]["model"]["enum"] = model_names self.schema["properties"]["entries"]["items"]["properties"]["model"]["enum"] = model_names

View File

@ -41,8 +41,7 @@
"$id": "#entry", "$id": "#entry",
"type": "object", "type": "object",
"required": [ "required": [
"model", "model"
"identifiers"
], ],
"properties": { "properties": {
"model": { "model": {
@ -67,6 +66,7 @@
}, },
"identifiers": { "identifiers": {
"type": "object", "type": "object",
"default": {},
"properties": { "properties": {
"pk": { "pk": {
"description": "Commonly available field, may not exist on all models", "description": "Commonly available field, may not exist on all models",

View File

@ -1,44 +0,0 @@
"""Managed objects manager"""
from importlib import import_module
from inspect import ismethod
from django.apps import AppConfig
from django.db import DatabaseError, InternalError, ProgrammingError
from structlog.stdlib import BoundLogger, get_logger
LOGGER = get_logger()
class ManagedAppConfig(AppConfig):
"""Basic reconciliation logic for apps"""
_logger: BoundLogger
def __init__(self, app_name: str, *args, **kwargs) -> None:
super().__init__(app_name, *args, **kwargs)
self._logger = get_logger().bind(app_name=app_name)
def ready(self) -> None:
self.reconcile()
return super().ready()
def import_module(self, path: str):
"""Load module"""
import_module(path)
def reconcile(self) -> None:
"""reconcile ourselves"""
prefix = "reconcile_"
for meth_name in dir(self):
meth = getattr(self, meth_name)
if not ismethod(meth):
continue
if not meth_name.startswith(prefix):
continue
name = meth_name.replace(prefix, "")
try:
self._logger.debug("Starting reconciler", name=name)
meth()
self._logger.debug("Successfully reconciled", name=name)
except (DatabaseError, ProgrammingError, InternalError) as exc:
self._logger.debug("Failed to run reconcile", name=name, exc=exc)

View File

@ -1,4 +1,4 @@
"""Managed Object models""" """blueprint models"""
from pathlib import Path from pathlib import Path
from urllib.parse import urlparse from urllib.parse import urlparse
from uuid import uuid4 from uuid import uuid4

View File

@ -5,7 +5,7 @@ from typing import Callable
from django.apps import apps from django.apps import apps
from authentik.blueprints.manager import ManagedAppConfig from authentik.blueprints.apps import ManagedAppConfig
from authentik.blueprints.models import BlueprintInstance from authentik.blueprints.models import BlueprintInstance
from authentik.lib.config import CONFIG from authentik.lib.config import CONFIG

View File

@ -45,8 +45,8 @@ class BlueprintEntryState:
class BlueprintEntry: class BlueprintEntry:
"""Single entry of a blueprint""" """Single entry of a blueprint"""
identifiers: dict[str, Any]
model: str model: str
identifiers: dict[str, Any] = field(default_factory=dict)
attrs: Optional[dict[str, Any]] = field(default_factory=dict) attrs: Optional[dict[str, Any]] = field(default_factory=dict)
# pylint: disable=invalid-name # pylint: disable=invalid-name

View File

@ -6,7 +6,6 @@ from typing import Any, Optional
from dacite import from_dict from dacite import from_dict
from dacite.exceptions import DaciteError from dacite.exceptions import DaciteError
from deepmerge import always_merger from deepmerge import always_merger
from django.apps import apps
from django.db import transaction from django.db import transaction
from django.db.models import Model from django.db.models import Model
from django.db.models.query_utils import Q from django.db.models.query_utils import Q
@ -25,6 +24,7 @@ from authentik.blueprints.v1.common import (
BlueprintLoader, BlueprintLoader,
EntryInvalidError, EntryInvalidError,
) )
from authentik.blueprints.v1.meta.registry import BaseMetaModel, registry
from authentik.core.models import ( from authentik.core.models import (
AuthenticatedSession, AuthenticatedSession,
PropertyMapping, PropertyMapping,
@ -138,10 +138,17 @@ class Importer:
def _validate_single(self, entry: BlueprintEntry) -> BaseSerializer: def _validate_single(self, entry: BlueprintEntry) -> BaseSerializer:
"""Validate a single entry""" """Validate a single entry"""
model_app_label, model_name = entry.model.split(".") model_app_label, model_name = entry.model.split(".")
model: type[SerializerModel] = apps.get_model(model_app_label, model_name) model: type[SerializerModel] = registry.get_model(model_app_label, model_name)
# Don't use isinstance since we don't want to check for inheritance # Don't use isinstance since we don't want to check for inheritance
if not is_model_allowed(model): if not is_model_allowed(model):
raise EntryInvalidError(f"Model {model} not allowed") raise EntryInvalidError(f"Model {model} not allowed")
if issubclass(model, BaseMetaModel):
serializer = model.serializer()(data=entry.get_attrs(self.__import))
try:
serializer.is_valid(raise_exception=True)
except ValidationError as exc:
raise EntryInvalidError(f"Serializer errors {serializer.errors}") from exc
return serializer
if entry.identifiers == {}: if entry.identifiers == {}:
raise EntryInvalidError("No identifiers") raise EntryInvalidError("No identifiers")
@ -158,7 +165,7 @@ class Importer:
existing_models = model.objects.filter(self.__query_from_identifier(updated_identifiers)) existing_models = model.objects.filter(self.__query_from_identifier(updated_identifiers))
serializer_kwargs = {} serializer_kwargs = {}
if existing_models.exists(): if not isinstance(model(), BaseMetaModel) and existing_models.exists():
model_instance = existing_models.first() model_instance = existing_models.first()
self.logger.debug( self.logger.debug(
"initialise serializer with instance", "initialise serializer with instance",
@ -207,7 +214,7 @@ class Importer:
for entry in self.__import.entries: for entry in self.__import.entries:
model_app_label, model_name = entry.model.split(".") model_app_label, model_name = entry.model.split(".")
try: try:
model: SerializerModel = apps.get_model(model_app_label, model_name) model: type[SerializerModel] = registry.get_model(model_app_label, model_name)
except LookupError: except LookupError:
self.logger.warning( self.logger.warning(
"app or model does not exist", app=model_app_label, model=model_name "app or model does not exist", app=model_app_label, model=model_name
@ -224,7 +231,7 @@ class Importer:
if "pk" in entry.identifiers: if "pk" in entry.identifiers:
self.__pk_map[entry.identifiers["pk"]] = model.pk self.__pk_map[entry.identifiers["pk"]] = model.pk
entry._state = BlueprintEntryState(model) entry._state = BlueprintEntryState(model)
self.logger.debug("updated model", model=model, pk=model.pk) self.logger.debug("updated model", model=model)
return True return True
def validate(self) -> tuple[bool, list[EventDict]]: def validate(self) -> tuple[bool, list[EventDict]]:
@ -243,6 +250,6 @@ class Importer:
if not successful: if not successful:
self.logger.debug("Blueprint validation failed") self.logger.debug("Blueprint validation failed")
for log in logs: for log in logs:
self.logger.debug(**log) getattr(self.logger, log.get("log_level"))(**log)
self.__import = orig_import self.__import = orig_import
return successful, logs return successful, logs

View File

View File

@ -0,0 +1,46 @@
"""Apply Blueprint meta model"""
from rest_framework.exceptions import ValidationError
from rest_framework.fields import BooleanField, JSONField
from structlog.stdlib import get_logger
from authentik.blueprints.v1.meta.registry import BaseMetaModel, MetaResult, registry
from authentik.core.api.utils import PassiveSerializer, is_dict
LOGGER = get_logger()
class ApplyBlueprintMetaSerializer(PassiveSerializer):
"""Serializer for meta apply blueprint model"""
identifiers = JSONField(validators=[is_dict])
required = BooleanField(default=True)
def create(self, validated_data: dict) -> MetaResult:
from authentik.blueprints.models import BlueprintInstance
from authentik.blueprints.v1.tasks import apply_blueprint
identifiers = validated_data["identifiers"]
required = validated_data["required"]
instance = BlueprintInstance.objects.filter(**identifiers).first()
if not instance:
if required:
raise ValidationError("Required blueprint does not exist")
LOGGER.info("Blueprint does not exist, but not required")
return MetaResult()
LOGGER.debug("Applying blueprint from meta model", blueprint=instance)
# pylint: disable=no-value-for-parameter
apply_blueprint(str(instance.pk))
return MetaResult()
@registry.register("metaapplyblueprint")
class MetaApplyBlueprint(BaseMetaModel):
"""Meta model to apply another blueprint"""
@staticmethod
def serializer() -> ApplyBlueprintMetaSerializer:
return ApplyBlueprintMetaSerializer
class Meta:
abstract = True

View File

@ -0,0 +1,63 @@
"""Base models"""
from typing import Optional
from django.apps import apps
from django.db.models import Model
from rest_framework.serializers import Serializer
class BaseMetaModel(Model):
"""Base models"""
@staticmethod
def serializer() -> Serializer:
"""Serializer similar to SerializerModel, but as a static method since
this is an abstract model"""
raise NotImplementedError
class Meta:
abstract = True
class MetaResult:
"""Result returned by Meta Models' serializers. Empty class but we can't return none as
the framework doesn't allow that"""
class MetaModelRegistry:
"""Registry for pseudo meta models"""
models: dict[str, BaseMetaModel]
virtual_prefix: str
def __init__(self, prefix: str) -> None:
self.models = {}
self.virtual_prefix = prefix
def register(self, model_id: str):
"""Register model class under `model_id`"""
def inner_wrapper(cls):
self.models[model_id] = cls
return cls
return inner_wrapper
def get_models(self):
"""Wrapper for django's `get_models` to list all models"""
models = apps.get_models()
for _, value in self.models.items():
models.append(value)
return models
def get_model(self, app_label: str, model_id: str) -> Optional[type[Model]]:
"""Get model checks if any virtual models are registered, and falls back
to actual django models"""
if app_label == self.virtual_prefix:
if model_id in self.models:
return self.models[model_id]
return apps.get_model(app_label, model_id)
registry = MetaModelRegistry("authentik_blueprints")

View File

@ -1,7 +1,7 @@
"""authentik core app config""" """authentik core app config"""
from django.conf import settings from django.conf import settings
from authentik.blueprints.manager import ManagedAppConfig from authentik.blueprints.apps import ManagedAppConfig
class AuthentikCoreConfig(ManagedAppConfig): class AuthentikCoreConfig(ManagedAppConfig):

View File

@ -2,7 +2,7 @@
from datetime import datetime from datetime import datetime
from typing import TYPE_CHECKING, Optional from typing import TYPE_CHECKING, Optional
from authentik.blueprints.manager import ManagedAppConfig from authentik.blueprints.apps import ManagedAppConfig
from authentik.lib.generators import generate_id from authentik.lib.generators import generate_id
if TYPE_CHECKING: if TYPE_CHECKING:

View File

@ -1,7 +1,7 @@
"""authentik events app""" """authentik events app"""
from prometheus_client import Gauge from prometheus_client import Gauge
from authentik.blueprints.manager import ManagedAppConfig from authentik.blueprints.apps import ManagedAppConfig
GAUGE_TASKS = Gauge( GAUGE_TASKS = Gauge(
"authentik_system_tasks", "authentik_system_tasks",

View File

@ -1,7 +1,7 @@
"""authentik flows app config""" """authentik flows app config"""
from prometheus_client import Gauge, Histogram from prometheus_client import Gauge, Histogram
from authentik.blueprints.manager import ManagedAppConfig from authentik.blueprints.apps import ManagedAppConfig
from authentik.lib.utils.reflection import all_subclasses from authentik.lib.utils.reflection import all_subclasses
GAUGE_FLOWS_CACHED = Gauge( GAUGE_FLOWS_CACHED = Gauge(

View File

@ -20,9 +20,8 @@ def model_tester_factory(test_model: type[Stage]) -> Callable:
try: try:
model_class = None model_class = None
if test_model._meta.abstract: if test_model._meta.abstract:
model_class = test_model.__bases__[0]() return
else: model_class = test_model()
model_class = test_model()
self.assertTrue(issubclass(model_class.serializer, BaseSerializer)) self.assertTrue(issubclass(model_class.serializer, BaseSerializer))
except NotImplementedError: except NotImplementedError:
pass pass

View File

@ -2,7 +2,7 @@
from prometheus_client import Gauge from prometheus_client import Gauge
from structlog.stdlib import get_logger from structlog.stdlib import get_logger
from authentik.blueprints.manager import ManagedAppConfig from authentik.blueprints.apps import ManagedAppConfig
LOGGER = get_logger() LOGGER = get_logger()

View File

@ -1,7 +1,7 @@
"""authentik policies app config""" """authentik policies app config"""
from prometheus_client import Gauge, Histogram from prometheus_client import Gauge, Histogram
from authentik.blueprints.manager import ManagedAppConfig from authentik.blueprints.apps import ManagedAppConfig
GAUGE_POLICIES_CACHED = Gauge( GAUGE_POLICIES_CACHED = Gauge(
"authentik_policies_cached", "authentik_policies_cached",

View File

@ -1,5 +1,5 @@
"""Authentik reputation_policy app config""" """Authentik reputation_policy app config"""
from authentik.blueprints.manager import ManagedAppConfig from authentik.blueprints.apps import ManagedAppConfig
class AuthentikPolicyReputationConfig(ManagedAppConfig): class AuthentikPolicyReputationConfig(ManagedAppConfig):

View File

@ -1,5 +1,5 @@
"""authentik Proxy app""" """authentik Proxy app"""
from authentik.blueprints.manager import ManagedAppConfig from authentik.blueprints.apps import ManagedAppConfig
class AuthentikProviderProxyConfig(ManagedAppConfig): class AuthentikProviderProxyConfig(ManagedAppConfig):

View File

@ -1,5 +1,5 @@
"""authentik ldap source config""" """authentik ldap source config"""
from authentik.blueprints.manager import ManagedAppConfig from authentik.blueprints.apps import ManagedAppConfig
class AuthentikSourceLDAPConfig(ManagedAppConfig): class AuthentikSourceLDAPConfig(ManagedAppConfig):

View File

@ -15,7 +15,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.utils.http import get_http_session from authentik.lib.utils.http import get_http_session
from authentik.sources.oauth.models import OAuthSource from authentik.sources.oauth.models import OAuthSource
from authentik.sources.oauth.types.manager import MANAGER, SourceType from authentik.sources.oauth.types.registry import SourceType, registry
class SourceTypeSerializer(PassiveSerializer): class SourceTypeSerializer(PassiveSerializer):
@ -33,7 +33,7 @@ class SourceTypeSerializer(PassiveSerializer):
class OAuthSourceSerializer(SourceSerializer): class OAuthSourceSerializer(SourceSerializer):
"""OAuth Source Serializer""" """OAuth Source Serializer"""
provider_type = ChoiceField(choices=MANAGER.get_name_tuple()) provider_type = ChoiceField(choices=registry.get_name_tuple())
callback_url = SerializerMethodField() callback_url = SerializerMethodField()
def get_callback_url(self, instance: OAuthSource) -> str: def get_callback_url(self, instance: OAuthSource) -> str:
@ -81,7 +81,7 @@ class OAuthSourceSerializer(SourceSerializer):
config = jwks_config.json() config = jwks_config.json()
attrs["oidc_jwks"] = config attrs["oidc_jwks"] = config
provider_type = MANAGER.find_type(attrs.get("provider_type", "")) provider_type = registry.find_type(attrs.get("provider_type", ""))
for url in [ for url in [
"authorization_url", "authorization_url",
"access_token_url", "access_token_url",
@ -153,10 +153,10 @@ class OAuthSourceViewSet(UsedByMixin, ModelViewSet):
If <name> isn't found, returns the default type.""" If <name> isn't found, returns the default type."""
data = [] data = []
if "name" in request.query_params: if "name" in request.query_params:
source_type = MANAGER.find_type(request.query_params.get("name")) source_type = registry.find_type(request.query_params.get("name"))
if source_type.__class__ != SourceType: if source_type.__class__ != SourceType:
data.append(SourceTypeSerializer(source_type).data) data.append(SourceTypeSerializer(source_type).data)
else: else:
for source_type in MANAGER.get(): for source_type in registry.get():
data.append(SourceTypeSerializer(source_type).data) data.append(SourceTypeSerializer(source_type).data)
return Response(data) return Response(data)

View File

@ -1,7 +1,7 @@
"""authentik oauth_client config""" """authentik oauth_client config"""
from structlog.stdlib import get_logger from structlog.stdlib import get_logger
from authentik.blueprints.manager import ManagedAppConfig from authentik.blueprints.apps import ManagedAppConfig
LOGGER = get_logger() LOGGER = get_logger()

View File

@ -11,7 +11,7 @@ from authentik.core.models import Source, UserSourceConnection
from authentik.core.types import UILoginButton, UserSettingSerializer from authentik.core.types import UILoginButton, UserSettingSerializer
if TYPE_CHECKING: if TYPE_CHECKING:
from authentik.sources.oauth.types.manager import SourceType from authentik.sources.oauth.types.registry import SourceType
class OAuthSource(Source): class OAuthSource(Source):
@ -57,9 +57,9 @@ class OAuthSource(Source):
@property @property
def type(self) -> type["SourceType"]: def type(self) -> type["SourceType"]:
"""Return the provider instance for this source""" """Return the provider instance for this source"""
from authentik.sources.oauth.types.manager import MANAGER from authentik.sources.oauth.types.registry import registry
return MANAGER.find_type(self.provider_type) return registry.find_type(self.provider_type)
@property @property
def component(self) -> str: def component(self) -> str:

View File

@ -11,7 +11,7 @@ from structlog.stdlib import get_logger
from authentik.flows.challenge import Challenge, ChallengeResponse, ChallengeTypes from authentik.flows.challenge import Challenge, ChallengeResponse, ChallengeTypes
from authentik.sources.oauth.clients.oauth2 import OAuth2Client from authentik.sources.oauth.clients.oauth2 import OAuth2Client
from authentik.sources.oauth.models import OAuthSource from authentik.sources.oauth.models import OAuthSource
from authentik.sources.oauth.types.manager import MANAGER, SourceType from authentik.sources.oauth.types.registry import SourceType, registry
from authentik.sources.oauth.views.callback import OAuthCallback from authentik.sources.oauth.views.callback import OAuthCallback
from authentik.sources.oauth.views.redirect import OAuthRedirect from authentik.sources.oauth.views.redirect import OAuthRedirect
@ -101,7 +101,7 @@ class AppleOAuth2Callback(OAuthCallback):
} }
@MANAGER.type() @registry.register()
class AppleType(SourceType): class AppleType(SourceType):
"""Apple Type definition""" """Apple Type definition"""

View File

@ -4,7 +4,7 @@ from typing import Any
from structlog.stdlib import get_logger from structlog.stdlib import get_logger
from authentik.sources.oauth.clients.oauth2 import UserprofileHeaderAuthClient from authentik.sources.oauth.clients.oauth2 import UserprofileHeaderAuthClient
from authentik.sources.oauth.types.manager import MANAGER, SourceType from authentik.sources.oauth.types.registry import SourceType, registry
from authentik.sources.oauth.views.callback import OAuthCallback from authentik.sources.oauth.views.callback import OAuthCallback
from authentik.sources.oauth.views.redirect import OAuthRedirect from authentik.sources.oauth.views.redirect import OAuthRedirect
@ -37,7 +37,7 @@ class AzureADOAuthCallback(OAuthCallback):
} }
@MANAGER.type() @registry.register()
class AzureADType(SourceType): class AzureADType(SourceType):
"""Azure AD Type definition""" """Azure AD Type definition"""

View File

@ -1,7 +1,7 @@
"""Discord OAuth Views""" """Discord OAuth Views"""
from typing import Any from typing import Any
from authentik.sources.oauth.types.manager import MANAGER, SourceType from authentik.sources.oauth.types.registry import SourceType, registry
from authentik.sources.oauth.views.callback import OAuthCallback from authentik.sources.oauth.views.callback import OAuthCallback
from authentik.sources.oauth.views.redirect import OAuthRedirect from authentik.sources.oauth.views.redirect import OAuthRedirect
@ -30,7 +30,7 @@ class DiscordOAuth2Callback(OAuthCallback):
} }
@MANAGER.type() @registry.register()
class DiscordType(SourceType): class DiscordType(SourceType):
"""Discord Type definition""" """Discord Type definition"""

View File

@ -4,7 +4,7 @@ from typing import Any, Optional
from facebook import GraphAPI from facebook import GraphAPI
from authentik.sources.oauth.clients.oauth2 import OAuth2Client from authentik.sources.oauth.clients.oauth2 import OAuth2Client
from authentik.sources.oauth.types.manager import MANAGER, SourceType from authentik.sources.oauth.types.registry import SourceType, registry
from authentik.sources.oauth.views.callback import OAuthCallback from authentik.sources.oauth.views.callback import OAuthCallback
from authentik.sources.oauth.views.redirect import OAuthRedirect from authentik.sources.oauth.views.redirect import OAuthRedirect
@ -42,7 +42,7 @@ class FacebookOAuth2Callback(OAuthCallback):
} }
@MANAGER.type() @registry.register()
class FacebookType(SourceType): class FacebookType(SourceType):
"""Facebook Type definition""" """Facebook Type definition"""

View File

@ -4,7 +4,7 @@ from typing import Any, Optional
from requests.exceptions import RequestException from requests.exceptions import RequestException
from authentik.sources.oauth.clients.oauth2 import OAuth2Client from authentik.sources.oauth.clients.oauth2 import OAuth2Client
from authentik.sources.oauth.types.manager import MANAGER, SourceType from authentik.sources.oauth.types.registry import SourceType, registry
from authentik.sources.oauth.views.callback import OAuthCallback from authentik.sources.oauth.views.callback import OAuthCallback
from authentik.sources.oauth.views.redirect import OAuthRedirect from authentik.sources.oauth.views.redirect import OAuthRedirect
@ -63,7 +63,7 @@ class GitHubOAuth2Callback(OAuthCallback):
} }
@MANAGER.type() @registry.register()
class GitHubType(SourceType): class GitHubType(SourceType):
"""GitHub Type definition""" """GitHub Type definition"""

View File

@ -1,7 +1,7 @@
"""Google OAuth Views""" """Google OAuth Views"""
from typing import Any from typing import Any
from authentik.sources.oauth.types.manager import MANAGER, SourceType from authentik.sources.oauth.types.registry import SourceType, registry
from authentik.sources.oauth.views.callback import OAuthCallback from authentik.sources.oauth.views.callback import OAuthCallback
from authentik.sources.oauth.views.redirect import OAuthRedirect from authentik.sources.oauth.views.redirect import OAuthRedirect
@ -28,7 +28,7 @@ class GoogleOAuth2Callback(OAuthCallback):
} }
@MANAGER.type() @registry.register()
class GoogleType(SourceType): class GoogleType(SourceType):
"""Google Type definition""" """Google Type definition"""

View File

@ -5,7 +5,7 @@ from requests.exceptions import RequestException
from structlog.stdlib import get_logger from structlog.stdlib import get_logger
from authentik.sources.oauth.clients.oauth2 import OAuth2Client from authentik.sources.oauth.clients.oauth2 import OAuth2Client
from authentik.sources.oauth.types.manager import MANAGER, SourceType from authentik.sources.oauth.types.registry import SourceType, registry
from authentik.sources.oauth.views.callback import OAuthCallback from authentik.sources.oauth.views.callback import OAuthCallback
from authentik.sources.oauth.views.redirect import OAuthRedirect from authentik.sources.oauth.views.redirect import OAuthRedirect
@ -58,7 +58,7 @@ class MailcowOAuth2Callback(OAuthCallback):
} }
@MANAGER.type() @registry.register()
class MailcowType(SourceType): class MailcowType(SourceType):
"""Mailcow Type definition""" """Mailcow Type definition"""

View File

@ -3,7 +3,7 @@ from typing import Any
from authentik.sources.oauth.clients.oauth2 import UserprofileHeaderAuthClient from authentik.sources.oauth.clients.oauth2 import UserprofileHeaderAuthClient
from authentik.sources.oauth.models import OAuthSource from authentik.sources.oauth.models import OAuthSource
from authentik.sources.oauth.types.manager import MANAGER, SourceType from authentik.sources.oauth.types.registry import SourceType, registry
from authentik.sources.oauth.views.callback import OAuthCallback from authentik.sources.oauth.views.callback import OAuthCallback
from authentik.sources.oauth.views.redirect import OAuthRedirect from authentik.sources.oauth.views.redirect import OAuthRedirect
@ -36,7 +36,7 @@ class OpenIDConnectOAuth2Callback(OAuthCallback):
} }
@MANAGER.type() @registry.register()
class OpenIDConnectType(SourceType): class OpenIDConnectType(SourceType):
"""OpenIDConnect Type definition""" """OpenIDConnect Type definition"""

View File

@ -3,7 +3,7 @@ from typing import Any
from authentik.sources.oauth.clients.oauth2 import UserprofileHeaderAuthClient from authentik.sources.oauth.clients.oauth2 import UserprofileHeaderAuthClient
from authentik.sources.oauth.models import OAuthSource from authentik.sources.oauth.models import OAuthSource
from authentik.sources.oauth.types.manager import MANAGER, SourceType from authentik.sources.oauth.types.registry import SourceType, registry
from authentik.sources.oauth.views.callback import OAuthCallback from authentik.sources.oauth.views.callback import OAuthCallback
from authentik.sources.oauth.views.redirect import OAuthRedirect from authentik.sources.oauth.views.redirect import OAuthRedirect
@ -39,7 +39,7 @@ class OktaOAuth2Callback(OAuthCallback):
} }
@MANAGER.type() @registry.register()
class OktaType(SourceType): class OktaType(SourceType):
"""Okta Type definition""" """Okta Type definition"""

View File

@ -4,7 +4,7 @@ from typing import Any
from requests.auth import HTTPBasicAuth from requests.auth import HTTPBasicAuth
from authentik.sources.oauth.clients.oauth2 import OAuth2Client from authentik.sources.oauth.clients.oauth2 import OAuth2Client
from authentik.sources.oauth.types.manager import MANAGER, SourceType from authentik.sources.oauth.types.registry import SourceType, registry
from authentik.sources.oauth.views.callback import OAuthCallback from authentik.sources.oauth.views.callback import OAuthCallback
from authentik.sources.oauth.views.redirect import OAuthRedirect from authentik.sources.oauth.views.redirect import OAuthRedirect
@ -45,7 +45,7 @@ class RedditOAuth2Callback(OAuthCallback):
} }
@MANAGER.type() @registry.register()
class RedditType(SourceType): class RedditType(SourceType):
"""Reddit Type definition""" """Reddit Type definition"""

View File

@ -55,13 +55,13 @@ class SourceType:
) )
class SourceTypeManager: class SourceTypeRegistry:
"""Manager to hold all Source types.""" """Registry to hold all Source types."""
def __init__(self) -> None: def __init__(self) -> None:
self.__sources: list[type[SourceType]] = [] self.__sources: list[type[SourceType]] = []
def type(self): def register(self):
"""Class decorator to register classes inline.""" """Class decorator to register classes inline."""
def inner_wrapper(cls): def inner_wrapper(cls):
@ -103,4 +103,4 @@ class SourceTypeManager:
raise ValueError raise ValueError
MANAGER = SourceTypeManager() registry = SourceTypeRegistry()

View File

@ -6,7 +6,7 @@ from authentik.sources.oauth.clients.oauth2 import (
SESSION_KEY_OAUTH_PKCE, SESSION_KEY_OAUTH_PKCE,
UserprofileHeaderAuthClient, UserprofileHeaderAuthClient,
) )
from authentik.sources.oauth.types.manager import MANAGER, SourceType from authentik.sources.oauth.types.registry import SourceType, registry
from authentik.sources.oauth.views.callback import OAuthCallback from authentik.sources.oauth.views.callback import OAuthCallback
from authentik.sources.oauth.views.redirect import OAuthRedirect from authentik.sources.oauth.views.redirect import OAuthRedirect
@ -60,7 +60,7 @@ class TwitterOAuthCallback(OAuthCallback):
} }
@MANAGER.type() @registry.register()
class TwitterType(SourceType): class TwitterType(SourceType):
"""Twitter Type definition""" """Twitter Type definition"""

View File

@ -2,7 +2,7 @@
from django.urls import path from django.urls import path
from authentik.sources.oauth.types.manager import RequestKind from authentik.sources.oauth.types.registry import RequestKind
from authentik.sources.oauth.views.dispatcher import DispatcherView from authentik.sources.oauth.views.dispatcher import DispatcherView
urlpatterns = [ urlpatterns = [

View File

@ -6,7 +6,7 @@ from django.views.decorators.csrf import csrf_exempt
from structlog.stdlib import get_logger from structlog.stdlib import get_logger
from authentik.sources.oauth.models import OAuthSource from authentik.sources.oauth.models import OAuthSource
from authentik.sources.oauth.types.manager import MANAGER, RequestKind from authentik.sources.oauth.types.registry import RequestKind, registry
LOGGER = get_logger() LOGGER = get_logger()
@ -20,6 +20,6 @@ class DispatcherView(View):
def dispatch(self, *args, source_slug: str, **kwargs): def dispatch(self, *args, source_slug: str, **kwargs):
"""Find Source by slug and forward request""" """Find Source by slug and forward request"""
source = get_object_or_404(OAuthSource, slug=source_slug) source = get_object_or_404(OAuthSource, slug=source_slug)
view = MANAGER.find(source.provider_type, kind=RequestKind(self.kind)) view = registry.find(source.provider_type, kind=RequestKind(self.kind))
LOGGER.debug("dispatching OAuth2 request to", view=view, kind=self.kind) LOGGER.debug("dispatching OAuth2 request to", view=view, kind=self.kind)
return view.as_view()(*args, source_slug=source_slug, **kwargs) return view.as_view()(*args, source_slug=source_slug, **kwargs)

View File

@ -1,5 +1,5 @@
"""Authentik SAML app config""" """Authentik SAML app config"""
from authentik.blueprints.manager import ManagedAppConfig from authentik.blueprints.apps import ManagedAppConfig
class AuthentikSourceSAMLConfig(ManagedAppConfig): class AuthentikSourceSAMLConfig(ManagedAppConfig):

View File

@ -1,5 +1,5 @@
"""Authenticator Static stage""" """Authenticator Static stage"""
from authentik.blueprints.manager import ManagedAppConfig from authentik.blueprints.apps import ManagedAppConfig
class AuthentikStageAuthenticatorStaticConfig(ManagedAppConfig): class AuthentikStageAuthenticatorStaticConfig(ManagedAppConfig):

View File

@ -3,7 +3,7 @@ from django.template.exceptions import TemplateDoesNotExist
from django.template.loader import get_template from django.template.loader import get_template
from structlog.stdlib import get_logger from structlog.stdlib import get_logger
from authentik.blueprints.manager import ManagedAppConfig from authentik.blueprints.apps import ManagedAppConfig
LOGGER = get_logger() LOGGER = get_logger()

View File

@ -2,6 +2,11 @@ version: 1
metadata: metadata:
name: Default - Authentication flow name: Default - Authentication flow
entries: entries:
- model: authentik_blueprints.metaapplyblueprint
attrs:
identifiers:
name: Default - Password change flow
required: false
- attrs: - attrs:
designation: authentication designation: authentication
name: Welcome to authentik! name: Welcome to authentik!

View File

@ -2,6 +2,21 @@ metadata:
name: Default - Tenant name: Default - Tenant
version: 1 version: 1
entries: 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: - attrs:
flow_authentication: !Find [authentik_flows.flow, [slug, default-authentication-flow]] flow_authentication: !Find [authentik_flows.flow, [slug, default-authentication-flow]]
flow_invalidation: !Find [authentik_flows.flow, [slug, default-invalidation-flow]] flow_invalidation: !Find [authentik_flows.flow, [slug, default-invalidation-flow]]

View File

@ -41,18 +41,24 @@
"$id": "#entry", "$id": "#entry",
"type": "object", "type": "object",
"required": [ "required": [
"model", "model"
"identifiers"
], ],
"properties": { "properties": {
"model": { "model": {
"type": "string", "type": "string",
"enum": [ "enum": [
"authentik_blueprints.blueprintinstance",
"authentik_blueprints.metaapplyblueprint",
"authentik_blueprints.metaapplyblueprint",
"authentik_core.application",
"authentik_core.group",
"authentik_core.token",
"authentik_core.user",
"authentik_crypto.certificatekeypair", "authentik_crypto.certificatekeypair",
"authentik_events.event", "authentik_events.event",
"authentik_events.notificationtransport",
"authentik_events.notification", "authentik_events.notification",
"authentik_events.notificationrule", "authentik_events.notificationrule",
"authentik_events.notificationtransport",
"authentik_events.notificationwebhookmapping", "authentik_events.notificationwebhookmapping",
"authentik_flows.flow", "authentik_flows.flow",
"authentik_flows.flowstagebinding", "authentik_flows.flowstagebinding",
@ -60,25 +66,25 @@
"authentik_outposts.dockerserviceconnection", "authentik_outposts.dockerserviceconnection",
"authentik_outposts.kubernetesserviceconnection", "authentik_outposts.kubernetesserviceconnection",
"authentik_outposts.outpost", "authentik_outposts.outpost",
"authentik_policies.policybinding",
"authentik_policies_dummy.dummypolicy", "authentik_policies_dummy.dummypolicy",
"authentik_policies_event_matcher.eventmatcherpolicy", "authentik_policies_event_matcher.eventmatcherpolicy",
"authentik_policies_expiry.passwordexpirypolicy", "authentik_policies_expiry.passwordexpirypolicy",
"authentik_policies_expression.expressionpolicy", "authentik_policies_expression.expressionpolicy",
"authentik_policies_hibp.haveibeenpwendpolicy", "authentik_policies_hibp.haveibeenpwendpolicy",
"authentik_policies_password.passwordpolicy", "authentik_policies_password.passwordpolicy",
"authentik_policies_reputation.reputationpolicy",
"authentik_policies_reputation.reputation", "authentik_policies_reputation.reputation",
"authentik_policies.policybinding", "authentik_policies_reputation.reputationpolicy",
"authentik_providers_ldap.ldapprovider", "authentik_providers_ldap.ldapprovider",
"authentik_providers_oauth2.scopemapping",
"authentik_providers_oauth2.oauth2provider",
"authentik_providers_oauth2.authorizationcode", "authentik_providers_oauth2.authorizationcode",
"authentik_providers_oauth2.oauth2provider",
"authentik_providers_oauth2.refreshtoken", "authentik_providers_oauth2.refreshtoken",
"authentik_providers_oauth2.scopemapping",
"authentik_providers_proxy.proxyprovider", "authentik_providers_proxy.proxyprovider",
"authentik_providers_saml.samlprovider",
"authentik_providers_saml.samlpropertymapping", "authentik_providers_saml.samlpropertymapping",
"authentik_sources_ldap.ldapsource", "authentik_providers_saml.samlprovider",
"authentik_sources_ldap.ldappropertymapping", "authentik_sources_ldap.ldappropertymapping",
"authentik_sources_ldap.ldapsource",
"authentik_sources_oauth.oauthsource", "authentik_sources_oauth.oauthsource",
"authentik_sources_oauth.useroauthsourceconnection", "authentik_sources_oauth.useroauthsourceconnection",
"authentik_sources_plex.plexsource", "authentik_sources_plex.plexsource",
@ -100,8 +106,8 @@
"authentik_stages_dummy.dummystage", "authentik_stages_dummy.dummystage",
"authentik_stages_email.emailstage", "authentik_stages_email.emailstage",
"authentik_stages_identification.identificationstage", "authentik_stages_identification.identificationstage",
"authentik_stages_invitation.invitationstage",
"authentik_stages_invitation.invitation", "authentik_stages_invitation.invitation",
"authentik_stages_invitation.invitationstage",
"authentik_stages_password.passwordstage", "authentik_stages_password.passwordstage",
"authentik_stages_prompt.prompt", "authentik_stages_prompt.prompt",
"authentik_stages_prompt.promptstage", "authentik_stages_prompt.promptstage",
@ -109,12 +115,7 @@
"authentik_stages_user_login.userloginstage", "authentik_stages_user_login.userloginstage",
"authentik_stages_user_logout.userlogoutstage", "authentik_stages_user_logout.userlogoutstage",
"authentik_stages_user_write.userwritestage", "authentik_stages_user_write.userwritestage",
"authentik_tenants.tenant", "authentik_tenants.tenant"
"authentik_blueprints.blueprintinstance",
"authentik_core.group",
"authentik_core.user",
"authentik_core.application",
"authentik_core.token"
] ]
}, },
"id": { "id": {
@ -133,6 +134,7 @@
}, },
"identifiers": { "identifiers": {
"type": "object", "type": "object",
"default": {},
"properties": { "properties": {
"pk": { "pk": {
"description": "Commonly available field, may not exist on all models", "description": "Commonly available field, may not exist on all models",

View File

@ -18,7 +18,7 @@ from authentik.core.models import User
from authentik.flows.models import Flow from authentik.flows.models import Flow
from authentik.lib.generators import generate_id, generate_key from authentik.lib.generators import generate_id, generate_key
from authentik.sources.oauth.models import OAuthSource from authentik.sources.oauth.models import OAuthSource
from authentik.sources.oauth.types.manager import MANAGER, SourceType from authentik.sources.oauth.types.registry import SourceType, registry
from authentik.sources.oauth.views.callback import OAuthCallback from authentik.sources.oauth.views.callback import OAuthCallback
from authentik.stages.identification.models import IdentificationStage from authentik.stages.identification.models import IdentificationStage
from tests.e2e.utils import SeleniumTestCase, retry from tests.e2e.utils import SeleniumTestCase, retry
@ -43,7 +43,7 @@ class OAUth1Callback(OAuthCallback):
} }
@MANAGER.type() @registry.register()
class OAUth1Type(SourceType): class OAUth1Type(SourceType):
"""OAuth1 Type definition""" """OAuth1 Type definition"""