From 54ba3e96166c2d2edb42a6cb59104db4ba5ef35d Mon Sep 17 00:00:00 2001 From: Jens L Date: Mon, 29 Aug 2022 21:20:58 +0200 Subject: [PATCH] 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 * fix tests Signed-off-by: Jens Langhammer * use custom registry Signed-off-by: Jens Langhammer * fix again Signed-off-by: Jens Langhammer * move ManagedAppConfig to apps.py Signed-off-by: Jens Langhammer * rename manager to registry Signed-off-by: Jens Langhammer * ci: use full tag in comment Signed-off-by: Jens Langhammer Signed-off-by: Jens Langhammer --- .../comment-pr-instructions/action.yml | 2 - .github/workflows/ci-main.yml | 2 +- authentik/admin/apps.py | 2 +- authentik/blueprints/apps.py | 46 +++++++++++++- .../commands/make_blueprint_schema.py | 5 +- .../management/commands/schema_template.json | 4 +- authentik/blueprints/manager.py | 44 ------------- authentik/blueprints/models.py | 2 +- authentik/blueprints/tests/__init__.py | 2 +- authentik/blueprints/v1/common.py | 2 +- authentik/blueprints/v1/importer.py | 19 ++++-- authentik/blueprints/v1/meta/__init__.py | 0 .../blueprints/v1/meta/apply_blueprint.py | 46 ++++++++++++++ authentik/blueprints/v1/meta/registry.py | 63 +++++++++++++++++++ authentik/core/apps.py | 2 +- authentik/crypto/apps.py | 2 +- authentik/events/apps.py | 2 +- authentik/flows/apps.py | 2 +- authentik/lib/tests/test_serializer_model.py | 5 +- authentik/outposts/apps.py | 2 +- authentik/policies/apps.py | 2 +- authentik/policies/reputation/apps.py | 2 +- authentik/providers/proxy/apps.py | 2 +- authentik/sources/ldap/apps.py | 2 +- authentik/sources/oauth/api/source.py | 10 +-- authentik/sources/oauth/apps.py | 2 +- authentik/sources/oauth/models.py | 6 +- authentik/sources/oauth/types/apple.py | 4 +- authentik/sources/oauth/types/azure_ad.py | 4 +- authentik/sources/oauth/types/discord.py | 4 +- authentik/sources/oauth/types/facebook.py | 4 +- authentik/sources/oauth/types/github.py | 4 +- authentik/sources/oauth/types/google.py | 4 +- authentik/sources/oauth/types/mailcow.py | 4 +- authentik/sources/oauth/types/oidc.py | 4 +- authentik/sources/oauth/types/okta.py | 4 +- authentik/sources/oauth/types/reddit.py | 4 +- .../oauth/types/{manager.py => registry.py} | 8 +-- authentik/sources/oauth/types/twitter.py | 4 +- authentik/sources/oauth/urls.py | 2 +- authentik/sources/oauth/views/dispatcher.py | 4 +- authentik/sources/saml/apps.py | 2 +- authentik/stages/authenticator_static/apps.py | 2 +- authentik/stages/email/apps.py | 2 +- .../10-flow-default-authentication-flow.yaml | 5 ++ blueprints/default/90-default-tenant.yaml | 15 +++++ blueprints/schema.json | 34 +++++----- tests/e2e/test_source_oauth.py | 4 +- 48 files changed, 269 insertions(+), 133 deletions(-) delete mode 100644 authentik/blueprints/manager.py create mode 100644 authentik/blueprints/v1/meta/__init__.py create mode 100644 authentik/blueprints/v1/meta/apply_blueprint.py create mode 100644 authentik/blueprints/v1/meta/registry.py rename authentik/sources/oauth/types/{manager.py => registry.py} (96%) diff --git a/.github/actions/comment-pr-instructions/action.yml b/.github/actions/comment-pr-instructions/action.yml index 63c2c4066..187d71f8e 100644 --- a/.github/actions/comment-pr-instructions/action.yml +++ b/.github/actions/comment-pr-instructions/action.yml @@ -83,8 +83,6 @@ runs: image: repository: ghcr.io/goauthentik/dev-server 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. diff --git a/.github/workflows/ci-main.yml b/.github/workflows/ci-main.yml index 1fd301500..a0934e631 100644 --- a/.github/workflows/ci-main.yml +++ b/.github/workflows/ci-main.yml @@ -240,4 +240,4 @@ jobs: continue-on-error: true uses: ./.github/actions/comment-pr-instructions with: - tag: gh-${{ steps.ev.outputs.branchNameContainer }} + tag: gh-${{ steps.ev.outputs.branchNameContainer }}-${{ steps.ev.outputs.timestamp }}-${{ steps.ev.outputs.sha }} diff --git a/authentik/admin/apps.py b/authentik/admin/apps.py index ea1818622..a80e21d89 100644 --- a/authentik/admin/apps.py +++ b/authentik/admin/apps.py @@ -1,7 +1,7 @@ """authentik admin app config""" 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") GAUGE_WORKERS = Gauge("authentik_admin_workers", "Currently connected workers") diff --git a/authentik/blueprints/apps.py b/authentik/blueprints/apps.py index 1ce185089..c1e2b6cee 100644 --- a/authentik/blueprints/apps.py +++ b/authentik/blueprints/apps.py @@ -1,6 +1,46 @@ """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): @@ -15,6 +55,10 @@ class AuthentikBlueprintsConfig(ManagedAppConfig): """Load 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): """Run blueprint discovery""" from authentik.blueprints.v1.tasks import blueprints_discover diff --git a/authentik/blueprints/management/commands/make_blueprint_schema.py b/authentik/blueprints/management/commands/make_blueprint_schema.py index 30a1a64ce..d65e5b0ae 100644 --- a/authentik/blueprints/management/commands/make_blueprint_schema.py +++ b/authentik/blueprints/management/commands/make_blueprint_schema.py @@ -2,11 +2,11 @@ from json import dumps, loads from pathlib import Path -from django.apps import apps from django.core.management.base import BaseCommand, no_translations from structlog.stdlib import get_logger from authentik.blueprints.v1.importer import is_model_allowed +from authentik.blueprints.v1.meta.registry import registry from authentik.lib.models import SerializerModel LOGGER = get_logger() @@ -29,10 +29,11 @@ class Command(BaseCommand): def set_model_allowed(self): """Set model enum""" model_names = [] - for model in apps.get_models(): + for model in registry.get_models(): if not is_model_allowed(model): continue if SerializerModel not in model.__mro__: continue 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 diff --git a/authentik/blueprints/management/commands/schema_template.json b/authentik/blueprints/management/commands/schema_template.json index 7e3f740be..e264e0a8e 100644 --- a/authentik/blueprints/management/commands/schema_template.json +++ b/authentik/blueprints/management/commands/schema_template.json @@ -41,8 +41,7 @@ "$id": "#entry", "type": "object", "required": [ - "model", - "identifiers" + "model" ], "properties": { "model": { @@ -67,6 +66,7 @@ }, "identifiers": { "type": "object", + "default": {}, "properties": { "pk": { "description": "Commonly available field, may not exist on all models", diff --git a/authentik/blueprints/manager.py b/authentik/blueprints/manager.py deleted file mode 100644 index 82bcd22fd..000000000 --- a/authentik/blueprints/manager.py +++ /dev/null @@ -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) diff --git a/authentik/blueprints/models.py b/authentik/blueprints/models.py index 74f2fcb76..c14021b6d 100644 --- a/authentik/blueprints/models.py +++ b/authentik/blueprints/models.py @@ -1,4 +1,4 @@ -"""Managed Object models""" +"""blueprint models""" from pathlib import Path from urllib.parse import urlparse from uuid import uuid4 diff --git a/authentik/blueprints/tests/__init__.py b/authentik/blueprints/tests/__init__.py index e7ee2597e..6ceb7c631 100644 --- a/authentik/blueprints/tests/__init__.py +++ b/authentik/blueprints/tests/__init__.py @@ -5,7 +5,7 @@ from typing import Callable 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.lib.config import CONFIG diff --git a/authentik/blueprints/v1/common.py b/authentik/blueprints/v1/common.py index 1f505e29b..df36e3e4e 100644 --- a/authentik/blueprints/v1/common.py +++ b/authentik/blueprints/v1/common.py @@ -45,8 +45,8 @@ class BlueprintEntryState: class BlueprintEntry: """Single entry of a blueprint""" - identifiers: dict[str, Any] model: str + identifiers: dict[str, Any] = field(default_factory=dict) attrs: Optional[dict[str, Any]] = field(default_factory=dict) # pylint: disable=invalid-name diff --git a/authentik/blueprints/v1/importer.py b/authentik/blueprints/v1/importer.py index 6792b2bc0..7038cdc12 100644 --- a/authentik/blueprints/v1/importer.py +++ b/authentik/blueprints/v1/importer.py @@ -6,7 +6,6 @@ from typing import Any, Optional from dacite import from_dict from dacite.exceptions import DaciteError from deepmerge import always_merger -from django.apps import apps from django.db import transaction from django.db.models import Model from django.db.models.query_utils import Q @@ -25,6 +24,7 @@ from authentik.blueprints.v1.common import ( BlueprintLoader, EntryInvalidError, ) +from authentik.blueprints.v1.meta.registry import BaseMetaModel, registry from authentik.core.models import ( AuthenticatedSession, PropertyMapping, @@ -138,10 +138,17 @@ class Importer: def _validate_single(self, entry: BlueprintEntry) -> BaseSerializer: """Validate a single entry""" 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 if not is_model_allowed(model): 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 == {}: raise EntryInvalidError("No identifiers") @@ -158,7 +165,7 @@ class Importer: existing_models = model.objects.filter(self.__query_from_identifier(updated_identifiers)) serializer_kwargs = {} - if existing_models.exists(): + if not isinstance(model(), BaseMetaModel) and existing_models.exists(): model_instance = existing_models.first() self.logger.debug( "initialise serializer with instance", @@ -207,7 +214,7 @@ class Importer: for entry in self.__import.entries: model_app_label, model_name = entry.model.split(".") try: - model: SerializerModel = apps.get_model(model_app_label, model_name) + model: type[SerializerModel] = registry.get_model(model_app_label, model_name) except LookupError: self.logger.warning( "app or model does not exist", app=model_app_label, model=model_name @@ -224,7 +231,7 @@ class Importer: if "pk" in entry.identifiers: self.__pk_map[entry.identifiers["pk"]] = model.pk entry._state = BlueprintEntryState(model) - self.logger.debug("updated model", model=model, pk=model.pk) + self.logger.debug("updated model", model=model) return True def validate(self) -> tuple[bool, list[EventDict]]: @@ -243,6 +250,6 @@ class Importer: if not successful: self.logger.debug("Blueprint validation failed") for log in logs: - self.logger.debug(**log) + getattr(self.logger, log.get("log_level"))(**log) self.__import = orig_import return successful, logs diff --git a/authentik/blueprints/v1/meta/__init__.py b/authentik/blueprints/v1/meta/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/authentik/blueprints/v1/meta/apply_blueprint.py b/authentik/blueprints/v1/meta/apply_blueprint.py new file mode 100644 index 000000000..03824aeb3 --- /dev/null +++ b/authentik/blueprints/v1/meta/apply_blueprint.py @@ -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 diff --git a/authentik/blueprints/v1/meta/registry.py b/authentik/blueprints/v1/meta/registry.py new file mode 100644 index 000000000..5830b5c39 --- /dev/null +++ b/authentik/blueprints/v1/meta/registry.py @@ -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") diff --git a/authentik/core/apps.py b/authentik/core/apps.py index 4d456033f..719b2abb1 100644 --- a/authentik/core/apps.py +++ b/authentik/core/apps.py @@ -1,7 +1,7 @@ """authentik core app config""" from django.conf import settings -from authentik.blueprints.manager import ManagedAppConfig +from authentik.blueprints.apps import ManagedAppConfig class AuthentikCoreConfig(ManagedAppConfig): diff --git a/authentik/crypto/apps.py b/authentik/crypto/apps.py index 3da92b464..44449773d 100644 --- a/authentik/crypto/apps.py +++ b/authentik/crypto/apps.py @@ -2,7 +2,7 @@ from datetime import datetime 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 if TYPE_CHECKING: diff --git a/authentik/events/apps.py b/authentik/events/apps.py index 79cf35097..34368856d 100644 --- a/authentik/events/apps.py +++ b/authentik/events/apps.py @@ -1,7 +1,7 @@ """authentik events app""" from prometheus_client import Gauge -from authentik.blueprints.manager import ManagedAppConfig +from authentik.blueprints.apps import ManagedAppConfig GAUGE_TASKS = Gauge( "authentik_system_tasks", diff --git a/authentik/flows/apps.py b/authentik/flows/apps.py index 5908a3aee..e01640bfc 100644 --- a/authentik/flows/apps.py +++ b/authentik/flows/apps.py @@ -1,7 +1,7 @@ """authentik flows app config""" 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 GAUGE_FLOWS_CACHED = Gauge( diff --git a/authentik/lib/tests/test_serializer_model.py b/authentik/lib/tests/test_serializer_model.py index ba5cd1ec0..28b1db718 100644 --- a/authentik/lib/tests/test_serializer_model.py +++ b/authentik/lib/tests/test_serializer_model.py @@ -20,9 +20,8 @@ def model_tester_factory(test_model: type[Stage]) -> Callable: try: model_class = None if test_model._meta.abstract: - model_class = test_model.__bases__[0]() - else: - model_class = test_model() + return + model_class = test_model() self.assertTrue(issubclass(model_class.serializer, BaseSerializer)) except NotImplementedError: pass diff --git a/authentik/outposts/apps.py b/authentik/outposts/apps.py index 54ab3141d..6898a170a 100644 --- a/authentik/outposts/apps.py +++ b/authentik/outposts/apps.py @@ -2,7 +2,7 @@ from prometheus_client import Gauge from structlog.stdlib import get_logger -from authentik.blueprints.manager import ManagedAppConfig +from authentik.blueprints.apps import ManagedAppConfig LOGGER = get_logger() diff --git a/authentik/policies/apps.py b/authentik/policies/apps.py index 025c13c54..f3bea51b5 100644 --- a/authentik/policies/apps.py +++ b/authentik/policies/apps.py @@ -1,7 +1,7 @@ """authentik policies app config""" from prometheus_client import Gauge, Histogram -from authentik.blueprints.manager import ManagedAppConfig +from authentik.blueprints.apps import ManagedAppConfig GAUGE_POLICIES_CACHED = Gauge( "authentik_policies_cached", diff --git a/authentik/policies/reputation/apps.py b/authentik/policies/reputation/apps.py index b92c721bf..14e7d3a0c 100644 --- a/authentik/policies/reputation/apps.py +++ b/authentik/policies/reputation/apps.py @@ -1,5 +1,5 @@ """Authentik reputation_policy app config""" -from authentik.blueprints.manager import ManagedAppConfig +from authentik.blueprints.apps import ManagedAppConfig class AuthentikPolicyReputationConfig(ManagedAppConfig): diff --git a/authentik/providers/proxy/apps.py b/authentik/providers/proxy/apps.py index afe1e242c..5e49fe181 100644 --- a/authentik/providers/proxy/apps.py +++ b/authentik/providers/proxy/apps.py @@ -1,5 +1,5 @@ """authentik Proxy app""" -from authentik.blueprints.manager import ManagedAppConfig +from authentik.blueprints.apps import ManagedAppConfig class AuthentikProviderProxyConfig(ManagedAppConfig): diff --git a/authentik/sources/ldap/apps.py b/authentik/sources/ldap/apps.py index 886f71862..4b55ef4ab 100644 --- a/authentik/sources/ldap/apps.py +++ b/authentik/sources/ldap/apps.py @@ -1,5 +1,5 @@ """authentik ldap source config""" -from authentik.blueprints.manager import ManagedAppConfig +from authentik.blueprints.apps import ManagedAppConfig class AuthentikSourceLDAPConfig(ManagedAppConfig): diff --git a/authentik/sources/oauth/api/source.py b/authentik/sources/oauth/api/source.py index f43e9ce1c..f35aa46ab 100644 --- a/authentik/sources/oauth/api/source.py +++ b/authentik/sources/oauth/api/source.py @@ -15,7 +15,7 @@ from authentik.core.api.used_by import UsedByMixin from authentik.core.api.utils import PassiveSerializer from authentik.lib.utils.http import get_http_session 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): @@ -33,7 +33,7 @@ class SourceTypeSerializer(PassiveSerializer): class OAuthSourceSerializer(SourceSerializer): """OAuth Source Serializer""" - provider_type = ChoiceField(choices=MANAGER.get_name_tuple()) + provider_type = ChoiceField(choices=registry.get_name_tuple()) callback_url = SerializerMethodField() def get_callback_url(self, instance: OAuthSource) -> str: @@ -81,7 +81,7 @@ class OAuthSourceSerializer(SourceSerializer): config = jwks_config.json() 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 [ "authorization_url", "access_token_url", @@ -153,10 +153,10 @@ class OAuthSourceViewSet(UsedByMixin, ModelViewSet): If isn't found, returns the default type.""" data = [] 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: data.append(SourceTypeSerializer(source_type).data) else: - for source_type in MANAGER.get(): + for source_type in registry.get(): data.append(SourceTypeSerializer(source_type).data) return Response(data) diff --git a/authentik/sources/oauth/apps.py b/authentik/sources/oauth/apps.py index b22655734..b4812f85d 100644 --- a/authentik/sources/oauth/apps.py +++ b/authentik/sources/oauth/apps.py @@ -1,7 +1,7 @@ """authentik oauth_client config""" from structlog.stdlib import get_logger -from authentik.blueprints.manager import ManagedAppConfig +from authentik.blueprints.apps import ManagedAppConfig LOGGER = get_logger() diff --git a/authentik/sources/oauth/models.py b/authentik/sources/oauth/models.py index 947c943b2..c4a84a8c6 100644 --- a/authentik/sources/oauth/models.py +++ b/authentik/sources/oauth/models.py @@ -11,7 +11,7 @@ from authentik.core.models import Source, UserSourceConnection from authentik.core.types import UILoginButton, UserSettingSerializer if TYPE_CHECKING: - from authentik.sources.oauth.types.manager import SourceType + from authentik.sources.oauth.types.registry import SourceType class OAuthSource(Source): @@ -57,9 +57,9 @@ class OAuthSource(Source): @property def type(self) -> type["SourceType"]: """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 def component(self) -> str: diff --git a/authentik/sources/oauth/types/apple.py b/authentik/sources/oauth/types/apple.py index 452531828..30d141f97 100644 --- a/authentik/sources/oauth/types/apple.py +++ b/authentik/sources/oauth/types/apple.py @@ -11,7 +11,7 @@ from structlog.stdlib import get_logger from authentik.flows.challenge import Challenge, ChallengeResponse, ChallengeTypes from authentik.sources.oauth.clients.oauth2 import OAuth2Client 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.redirect import OAuthRedirect @@ -101,7 +101,7 @@ class AppleOAuth2Callback(OAuthCallback): } -@MANAGER.type() +@registry.register() class AppleType(SourceType): """Apple Type definition""" diff --git a/authentik/sources/oauth/types/azure_ad.py b/authentik/sources/oauth/types/azure_ad.py index d7c5dc49c..6e2f3730a 100644 --- a/authentik/sources/oauth/types/azure_ad.py +++ b/authentik/sources/oauth/types/azure_ad.py @@ -4,7 +4,7 @@ from typing import Any from structlog.stdlib import get_logger 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.redirect import OAuthRedirect @@ -37,7 +37,7 @@ class AzureADOAuthCallback(OAuthCallback): } -@MANAGER.type() +@registry.register() class AzureADType(SourceType): """Azure AD Type definition""" diff --git a/authentik/sources/oauth/types/discord.py b/authentik/sources/oauth/types/discord.py index 5bf1a47b1..10c461f5a 100644 --- a/authentik/sources/oauth/types/discord.py +++ b/authentik/sources/oauth/types/discord.py @@ -1,7 +1,7 @@ """Discord OAuth Views""" 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.redirect import OAuthRedirect @@ -30,7 +30,7 @@ class DiscordOAuth2Callback(OAuthCallback): } -@MANAGER.type() +@registry.register() class DiscordType(SourceType): """Discord Type definition""" diff --git a/authentik/sources/oauth/types/facebook.py b/authentik/sources/oauth/types/facebook.py index 2297b69ff..9176afeb5 100644 --- a/authentik/sources/oauth/types/facebook.py +++ b/authentik/sources/oauth/types/facebook.py @@ -4,7 +4,7 @@ from typing import Any, Optional from facebook import GraphAPI 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.redirect import OAuthRedirect @@ -42,7 +42,7 @@ class FacebookOAuth2Callback(OAuthCallback): } -@MANAGER.type() +@registry.register() class FacebookType(SourceType): """Facebook Type definition""" diff --git a/authentik/sources/oauth/types/github.py b/authentik/sources/oauth/types/github.py index f800edb4e..df970e115 100644 --- a/authentik/sources/oauth/types/github.py +++ b/authentik/sources/oauth/types/github.py @@ -4,7 +4,7 @@ from typing import Any, Optional from requests.exceptions import RequestException 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.redirect import OAuthRedirect @@ -63,7 +63,7 @@ class GitHubOAuth2Callback(OAuthCallback): } -@MANAGER.type() +@registry.register() class GitHubType(SourceType): """GitHub Type definition""" diff --git a/authentik/sources/oauth/types/google.py b/authentik/sources/oauth/types/google.py index 5d6865e70..0e0b3ce67 100644 --- a/authentik/sources/oauth/types/google.py +++ b/authentik/sources/oauth/types/google.py @@ -1,7 +1,7 @@ """Google OAuth Views""" 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.redirect import OAuthRedirect @@ -28,7 +28,7 @@ class GoogleOAuth2Callback(OAuthCallback): } -@MANAGER.type() +@registry.register() class GoogleType(SourceType): """Google Type definition""" diff --git a/authentik/sources/oauth/types/mailcow.py b/authentik/sources/oauth/types/mailcow.py index 93999f5d2..30be8a8fb 100644 --- a/authentik/sources/oauth/types/mailcow.py +++ b/authentik/sources/oauth/types/mailcow.py @@ -5,7 +5,7 @@ from requests.exceptions import RequestException from structlog.stdlib import get_logger 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.redirect import OAuthRedirect @@ -58,7 +58,7 @@ class MailcowOAuth2Callback(OAuthCallback): } -@MANAGER.type() +@registry.register() class MailcowType(SourceType): """Mailcow Type definition""" diff --git a/authentik/sources/oauth/types/oidc.py b/authentik/sources/oauth/types/oidc.py index 4dfdf1678..189209c43 100644 --- a/authentik/sources/oauth/types/oidc.py +++ b/authentik/sources/oauth/types/oidc.py @@ -3,7 +3,7 @@ from typing import Any from authentik.sources.oauth.clients.oauth2 import UserprofileHeaderAuthClient 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.redirect import OAuthRedirect @@ -36,7 +36,7 @@ class OpenIDConnectOAuth2Callback(OAuthCallback): } -@MANAGER.type() +@registry.register() class OpenIDConnectType(SourceType): """OpenIDConnect Type definition""" diff --git a/authentik/sources/oauth/types/okta.py b/authentik/sources/oauth/types/okta.py index c7f43cbbf..c1aa3880a 100644 --- a/authentik/sources/oauth/types/okta.py +++ b/authentik/sources/oauth/types/okta.py @@ -3,7 +3,7 @@ from typing import Any from authentik.sources.oauth.clients.oauth2 import UserprofileHeaderAuthClient 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.redirect import OAuthRedirect @@ -39,7 +39,7 @@ class OktaOAuth2Callback(OAuthCallback): } -@MANAGER.type() +@registry.register() class OktaType(SourceType): """Okta Type definition""" diff --git a/authentik/sources/oauth/types/reddit.py b/authentik/sources/oauth/types/reddit.py index ff58d3a3c..7ad2a2754 100644 --- a/authentik/sources/oauth/types/reddit.py +++ b/authentik/sources/oauth/types/reddit.py @@ -4,7 +4,7 @@ from typing import Any from requests.auth import HTTPBasicAuth 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.redirect import OAuthRedirect @@ -45,7 +45,7 @@ class RedditOAuth2Callback(OAuthCallback): } -@MANAGER.type() +@registry.register() class RedditType(SourceType): """Reddit Type definition""" diff --git a/authentik/sources/oauth/types/manager.py b/authentik/sources/oauth/types/registry.py similarity index 96% rename from authentik/sources/oauth/types/manager.py rename to authentik/sources/oauth/types/registry.py index a87013190..450564670 100644 --- a/authentik/sources/oauth/types/manager.py +++ b/authentik/sources/oauth/types/registry.py @@ -55,13 +55,13 @@ class SourceType: ) -class SourceTypeManager: - """Manager to hold all Source types.""" +class SourceTypeRegistry: + """Registry to hold all Source types.""" def __init__(self) -> None: self.__sources: list[type[SourceType]] = [] - def type(self): + def register(self): """Class decorator to register classes inline.""" def inner_wrapper(cls): @@ -103,4 +103,4 @@ class SourceTypeManager: raise ValueError -MANAGER = SourceTypeManager() +registry = SourceTypeRegistry() diff --git a/authentik/sources/oauth/types/twitter.py b/authentik/sources/oauth/types/twitter.py index 3e8a6e71b..da1d306da 100644 --- a/authentik/sources/oauth/types/twitter.py +++ b/authentik/sources/oauth/types/twitter.py @@ -6,7 +6,7 @@ from authentik.sources.oauth.clients.oauth2 import ( SESSION_KEY_OAUTH_PKCE, 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.redirect import OAuthRedirect @@ -60,7 +60,7 @@ class TwitterOAuthCallback(OAuthCallback): } -@MANAGER.type() +@registry.register() class TwitterType(SourceType): """Twitter Type definition""" diff --git a/authentik/sources/oauth/urls.py b/authentik/sources/oauth/urls.py index 7b38afe53..ecc32a14c 100644 --- a/authentik/sources/oauth/urls.py +++ b/authentik/sources/oauth/urls.py @@ -2,7 +2,7 @@ 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 urlpatterns = [ diff --git a/authentik/sources/oauth/views/dispatcher.py b/authentik/sources/oauth/views/dispatcher.py index 557153fe7..234b936b3 100644 --- a/authentik/sources/oauth/views/dispatcher.py +++ b/authentik/sources/oauth/views/dispatcher.py @@ -6,7 +6,7 @@ from django.views.decorators.csrf import csrf_exempt from structlog.stdlib import get_logger 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() @@ -20,6 +20,6 @@ class DispatcherView(View): def dispatch(self, *args, source_slug: str, **kwargs): """Find Source by slug and forward request""" 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) return view.as_view()(*args, source_slug=source_slug, **kwargs) diff --git a/authentik/sources/saml/apps.py b/authentik/sources/saml/apps.py index a9453817f..727b5bbc4 100644 --- a/authentik/sources/saml/apps.py +++ b/authentik/sources/saml/apps.py @@ -1,5 +1,5 @@ """Authentik SAML app config""" -from authentik.blueprints.manager import ManagedAppConfig +from authentik.blueprints.apps import ManagedAppConfig class AuthentikSourceSAMLConfig(ManagedAppConfig): diff --git a/authentik/stages/authenticator_static/apps.py b/authentik/stages/authenticator_static/apps.py index 9865cb20b..301ad9983 100644 --- a/authentik/stages/authenticator_static/apps.py +++ b/authentik/stages/authenticator_static/apps.py @@ -1,5 +1,5 @@ """Authenticator Static stage""" -from authentik.blueprints.manager import ManagedAppConfig +from authentik.blueprints.apps import ManagedAppConfig class AuthentikStageAuthenticatorStaticConfig(ManagedAppConfig): diff --git a/authentik/stages/email/apps.py b/authentik/stages/email/apps.py index 8fd2d8876..f69bbb728 100644 --- a/authentik/stages/email/apps.py +++ b/authentik/stages/email/apps.py @@ -3,7 +3,7 @@ from django.template.exceptions import TemplateDoesNotExist from django.template.loader import get_template from structlog.stdlib import get_logger -from authentik.blueprints.manager import ManagedAppConfig +from authentik.blueprints.apps import ManagedAppConfig LOGGER = get_logger() diff --git a/blueprints/default/10-flow-default-authentication-flow.yaml b/blueprints/default/10-flow-default-authentication-flow.yaml index 231420733..9afbf6605 100644 --- a/blueprints/default/10-flow-default-authentication-flow.yaml +++ b/blueprints/default/10-flow-default-authentication-flow.yaml @@ -2,6 +2,11 @@ version: 1 metadata: name: Default - Authentication flow entries: +- model: authentik_blueprints.metaapplyblueprint + attrs: + identifiers: + name: Default - Password change flow + required: false - attrs: designation: authentication name: Welcome to authentik! diff --git a/blueprints/default/90-default-tenant.yaml b/blueprints/default/90-default-tenant.yaml index 6914293e8..a029fd349 100644 --- a/blueprints/default/90-default-tenant.yaml +++ b/blueprints/default/90-default-tenant.yaml @@ -2,6 +2,21 @@ 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]] diff --git a/blueprints/schema.json b/blueprints/schema.json index f1cc431ed..ab13a672d 100644 --- a/blueprints/schema.json +++ b/blueprints/schema.json @@ -41,18 +41,24 @@ "$id": "#entry", "type": "object", "required": [ - "model", - "identifiers" + "model" ], "properties": { "model": { "type": "string", "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_events.event", - "authentik_events.notificationtransport", "authentik_events.notification", "authentik_events.notificationrule", + "authentik_events.notificationtransport", "authentik_events.notificationwebhookmapping", "authentik_flows.flow", "authentik_flows.flowstagebinding", @@ -60,25 +66,25 @@ "authentik_outposts.dockerserviceconnection", "authentik_outposts.kubernetesserviceconnection", "authentik_outposts.outpost", + "authentik_policies.policybinding", "authentik_policies_dummy.dummypolicy", "authentik_policies_event_matcher.eventmatcherpolicy", "authentik_policies_expiry.passwordexpirypolicy", "authentik_policies_expression.expressionpolicy", "authentik_policies_hibp.haveibeenpwendpolicy", "authentik_policies_password.passwordpolicy", - "authentik_policies_reputation.reputationpolicy", "authentik_policies_reputation.reputation", - "authentik_policies.policybinding", + "authentik_policies_reputation.reputationpolicy", "authentik_providers_ldap.ldapprovider", - "authentik_providers_oauth2.scopemapping", - "authentik_providers_oauth2.oauth2provider", "authentik_providers_oauth2.authorizationcode", + "authentik_providers_oauth2.oauth2provider", "authentik_providers_oauth2.refreshtoken", + "authentik_providers_oauth2.scopemapping", "authentik_providers_proxy.proxyprovider", - "authentik_providers_saml.samlprovider", "authentik_providers_saml.samlpropertymapping", - "authentik_sources_ldap.ldapsource", + "authentik_providers_saml.samlprovider", "authentik_sources_ldap.ldappropertymapping", + "authentik_sources_ldap.ldapsource", "authentik_sources_oauth.oauthsource", "authentik_sources_oauth.useroauthsourceconnection", "authentik_sources_plex.plexsource", @@ -100,8 +106,8 @@ "authentik_stages_dummy.dummystage", "authentik_stages_email.emailstage", "authentik_stages_identification.identificationstage", - "authentik_stages_invitation.invitationstage", "authentik_stages_invitation.invitation", + "authentik_stages_invitation.invitationstage", "authentik_stages_password.passwordstage", "authentik_stages_prompt.prompt", "authentik_stages_prompt.promptstage", @@ -109,12 +115,7 @@ "authentik_stages_user_login.userloginstage", "authentik_stages_user_logout.userlogoutstage", "authentik_stages_user_write.userwritestage", - "authentik_tenants.tenant", - "authentik_blueprints.blueprintinstance", - "authentik_core.group", - "authentik_core.user", - "authentik_core.application", - "authentik_core.token" + "authentik_tenants.tenant" ] }, "id": { @@ -133,6 +134,7 @@ }, "identifiers": { "type": "object", + "default": {}, "properties": { "pk": { "description": "Commonly available field, may not exist on all models", diff --git a/tests/e2e/test_source_oauth.py b/tests/e2e/test_source_oauth.py index a7b77a14b..5848eb0b1 100644 --- a/tests/e2e/test_source_oauth.py +++ b/tests/e2e/test_source_oauth.py @@ -18,7 +18,7 @@ from authentik.core.models import User from authentik.flows.models import Flow from authentik.lib.generators import generate_id, generate_key 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.stages.identification.models import IdentificationStage from tests.e2e.utils import SeleniumTestCase, retry @@ -43,7 +43,7 @@ class OAUth1Callback(OAuthCallback): } -@MANAGER.type() +@registry.register() class OAUth1Type(SourceType): """OAuth1 Type definition"""