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:
parent
58e3ca28be
commit
54ba3e9616
|
@ -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.
|
||||
|
|
|
@ -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 }}
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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)
|
|
@ -1,4 +1,4 @@
|
|||
"""Managed Object models"""
|
||||
"""blueprint models"""
|
||||
from pathlib import Path
|
||||
from urllib.parse import urlparse
|
||||
from uuid import uuid4
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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")
|
|
@ -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):
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
"""Authentik reputation_policy app config"""
|
||||
from authentik.blueprints.manager import ManagedAppConfig
|
||||
from authentik.blueprints.apps import ManagedAppConfig
|
||||
|
||||
|
||||
class AuthentikPolicyReputationConfig(ManagedAppConfig):
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
"""authentik Proxy app"""
|
||||
from authentik.blueprints.manager import ManagedAppConfig
|
||||
from authentik.blueprints.apps import ManagedAppConfig
|
||||
|
||||
|
||||
class AuthentikProviderProxyConfig(ManagedAppConfig):
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
"""authentik ldap source config"""
|
||||
from authentik.blueprints.manager import ManagedAppConfig
|
||||
from authentik.blueprints.apps import ManagedAppConfig
|
||||
|
||||
|
||||
class AuthentikSourceLDAPConfig(ManagedAppConfig):
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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"""
|
||||
|
||||
|
|
|
@ -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"""
|
||||
|
||||
|
|
|
@ -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"""
|
||||
|
||||
|
|
|
@ -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"""
|
||||
|
||||
|
|
|
@ -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"""
|
||||
|
||||
|
|
|
@ -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"""
|
||||
|
||||
|
|
|
@ -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"""
|
||||
|
||||
|
|
|
@ -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"""
|
||||
|
||||
|
|
|
@ -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"""
|
||||
|
||||
|
|
|
@ -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"""
|
||||
|
||||
|
|
|
@ -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()
|
|
@ -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"""
|
||||
|
||||
|
|
|
@ -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 = [
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
"""Authentik SAML app config"""
|
||||
from authentik.blueprints.manager import ManagedAppConfig
|
||||
from authentik.blueprints.apps import ManagedAppConfig
|
||||
|
||||
|
||||
class AuthentikSourceSAMLConfig(ManagedAppConfig):
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
"""Authenticator Static stage"""
|
||||
from authentik.blueprints.manager import ManagedAppConfig
|
||||
from authentik.blueprints.apps import ManagedAppConfig
|
||||
|
||||
|
||||
class AuthentikStageAuthenticatorStaticConfig(ManagedAppConfig):
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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!
|
||||
|
|
|
@ -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]]
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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"""
|
||||
|
||||
|
|
Reference in New Issue