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:
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.

View file

@ -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 }}

View file

@ -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")

View file

@ -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

View file

@ -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

View file

@ -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",

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 urllib.parse import urlparse
from uuid import uuid4

View file

@ -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

View file

@ -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

View file

@ -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

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"""
from django.conf import settings
from authentik.blueprints.manager import ManagedAppConfig
from authentik.blueprints.apps import ManagedAppConfig
class AuthentikCoreConfig(ManagedAppConfig):

View file

@ -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:

View file

@ -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",

View file

@ -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(

View file

@ -20,8 +20,7 @@ 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:
return
model_class = test_model()
self.assertTrue(issubclass(model_class.serializer, BaseSerializer))
except NotImplementedError:

View file

@ -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()

View file

@ -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",

View file

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

View file

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

View file

@ -1,5 +1,5 @@
"""authentik ldap source config"""
from authentik.blueprints.manager import ManagedAppConfig
from authentik.blueprints.apps import 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.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 <name> 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)

View file

@ -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()

View file

@ -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:

View file

@ -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"""

View file

@ -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"""

View file

@ -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"""

View file

@ -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"""

View file

@ -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"""

View file

@ -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"""

View file

@ -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"""

View file

@ -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"""

View file

@ -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"""

View file

@ -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"""

View file

@ -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()

View file

@ -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"""

View file

@ -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 = [

View file

@ -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)

View file

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

View file

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

View file

@ -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()

View file

@ -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!

View file

@ -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]]

View file

@ -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",

View file

@ -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"""