Merge remote-tracking branch 'origin/core/app-transactional' into application-wizard-2-with-api

* origin/core/app-transactional:
  add new "must_created" state to blueprints to prevent overwriting objects
  cleanup
  separate blueprint importer from yaml parsing
  initial api and schema
This commit is contained in:
Ken Sternberg 2023-08-21 12:26:56 -07:00
commit cbdca55e57
20 changed files with 515 additions and 166 deletions

View File

@ -12,7 +12,7 @@ from rest_framework.viewsets import ModelViewSet
from authentik.api.decorators import permission_required from authentik.api.decorators import permission_required
from authentik.blueprints.models import BlueprintInstance from authentik.blueprints.models import BlueprintInstance
from authentik.blueprints.v1.importer import Importer from authentik.blueprints.v1.importer import StringImporter
from authentik.blueprints.v1.oci import OCI_PREFIX from authentik.blueprints.v1.oci import OCI_PREFIX
from authentik.blueprints.v1.tasks import apply_blueprint, blueprints_find_dict from authentik.blueprints.v1.tasks import apply_blueprint, blueprints_find_dict
from authentik.core.api.used_by import UsedByMixin from authentik.core.api.used_by import UsedByMixin
@ -49,7 +49,7 @@ class BlueprintInstanceSerializer(ModelSerializer):
if content == "": if content == "":
return content return content
context = self.instance.context if self.instance else {} context = self.instance.context if self.instance else {}
valid, logs = Importer(content, context).validate() valid, logs = StringImporter(content, context).validate()
if not valid: if not valid:
text_logs = "\n".join([x["event"] for x in logs]) text_logs = "\n".join([x["event"] for x in logs])
raise ValidationError(_("Failed to validate blueprint: %(logs)s" % {"logs": text_logs})) raise ValidationError(_("Failed to validate blueprint: %(logs)s" % {"logs": text_logs}))

View File

@ -5,7 +5,7 @@ from django.core.management.base import BaseCommand, no_translations
from structlog.stdlib import get_logger from structlog.stdlib import get_logger
from authentik.blueprints.models import BlueprintInstance from authentik.blueprints.models import BlueprintInstance
from authentik.blueprints.v1.importer import Importer from authentik.blueprints.v1.importer import StringImporter
LOGGER = get_logger() LOGGER = get_logger()
@ -18,7 +18,7 @@ class Command(BaseCommand):
"""Apply all blueprints in order, abort when one fails to import""" """Apply all blueprints in order, abort when one fails to import"""
for blueprint_path in options.get("blueprints", []): for blueprint_path in options.get("blueprints", []):
content = BlueprintInstance(path=blueprint_path).retrieve() content = BlueprintInstance(path=blueprint_path).retrieve()
importer = Importer(content) importer = StringImporter(content)
valid, _ = importer.validate() valid, _ = importer.validate()
if not valid: if not valid:
self.stderr.write("blueprint invalid") self.stderr.write("blueprint invalid")

View File

@ -9,6 +9,7 @@ from rest_framework.fields import Field, JSONField, UUIDField
from rest_framework.serializers import Serializer from rest_framework.serializers import Serializer
from structlog.stdlib import get_logger from structlog.stdlib import get_logger
from authentik.blueprints.v1.common import BlueprintEntryDesiredState
from authentik.blueprints.v1.importer import SERIALIZER_CONTEXT_BLUEPRINT, is_model_allowed from authentik.blueprints.v1.importer import SERIALIZER_CONTEXT_BLUEPRINT, is_model_allowed
from authentik.blueprints.v1.meta.registry import BaseMetaModel, registry from authentik.blueprints.v1.meta.registry import BaseMetaModel, registry
from authentik.lib.models import SerializerModel from authentik.lib.models import SerializerModel
@ -110,7 +111,7 @@ class Command(BaseCommand):
"id": {"type": "string"}, "id": {"type": "string"},
"state": { "state": {
"type": "string", "type": "string",
"enum": ["absent", "present", "created"], "enum": [s.value for s in BlueprintEntryDesiredState],
"default": "present", "default": "present",
}, },
"conditions": {"type": "array", "items": {"type": "boolean"}}, "conditions": {"type": "array", "items": {"type": "boolean"}},

View File

@ -11,7 +11,7 @@ from authentik.blueprints.models import BlueprintInstance
def apply_blueprint(*files: str): def apply_blueprint(*files: str):
"""Apply blueprint before test""" """Apply blueprint before test"""
from authentik.blueprints.v1.importer import Importer from authentik.blueprints.v1.importer import StringImporter
def wrapper_outer(func: Callable): def wrapper_outer(func: Callable):
"""Apply blueprint before test""" """Apply blueprint before test"""
@ -20,7 +20,7 @@ def apply_blueprint(*files: str):
def wrapper(*args, **kwargs): def wrapper(*args, **kwargs):
for file in files: for file in files:
content = BlueprintInstance(path=file).retrieve() content = BlueprintInstance(path=file).retrieve()
Importer(content).apply() StringImporter(content).apply()
return func(*args, **kwargs) return func(*args, **kwargs)
return wrapper return wrapper

View File

@ -6,7 +6,7 @@ from django.test import TransactionTestCase
from authentik.blueprints.models import BlueprintInstance from authentik.blueprints.models import BlueprintInstance
from authentik.blueprints.tests import apply_blueprint from authentik.blueprints.tests import apply_blueprint
from authentik.blueprints.v1.importer import Importer from authentik.blueprints.v1.importer import StringImporter
from authentik.tenants.models import Tenant from authentik.tenants.models import Tenant
@ -25,7 +25,7 @@ def blueprint_tester(file_name: Path) -> Callable:
def tester(self: TestPackaged): def tester(self: TestPackaged):
base = Path("blueprints/") base = Path("blueprints/")
rel_path = Path(file_name).relative_to(base) rel_path = Path(file_name).relative_to(base)
importer = Importer(BlueprintInstance(path=str(rel_path)).retrieve()) importer = StringImporter(BlueprintInstance(path=str(rel_path)).retrieve())
self.assertTrue(importer.validate()[0]) self.assertTrue(importer.validate()[0])
self.assertTrue(importer.apply()) self.assertTrue(importer.apply())

View File

@ -4,7 +4,7 @@ from os import environ
from django.test import TransactionTestCase from django.test import TransactionTestCase
from authentik.blueprints.v1.exporter import FlowExporter from authentik.blueprints.v1.exporter import FlowExporter
from authentik.blueprints.v1.importer import Importer, transaction_rollback from authentik.blueprints.v1.importer import StringImporter, transaction_rollback
from authentik.core.models import Group from authentik.core.models import Group
from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding
from authentik.lib.generators import generate_id from authentik.lib.generators import generate_id
@ -21,14 +21,14 @@ class TestBlueprintsV1(TransactionTestCase):
def test_blueprint_invalid_format(self): def test_blueprint_invalid_format(self):
"""Test blueprint with invalid format""" """Test blueprint with invalid format"""
importer = Importer('{"version": 3}') importer = StringImporter('{"version": 3}')
self.assertFalse(importer.validate()[0]) self.assertFalse(importer.validate()[0])
importer = Importer( importer = StringImporter(
'{"version": 1,"entries":[{"identifiers":{},"attrs":{},' '{"version": 1,"entries":[{"identifiers":{},"attrs":{},'
'"model": "authentik_core.User"}]}' '"model": "authentik_core.User"}]}'
) )
self.assertFalse(importer.validate()[0]) self.assertFalse(importer.validate()[0])
importer = Importer( importer = StringImporter(
'{"version": 1, "entries": [{"attrs": {"name": "test"}, ' '{"version": 1, "entries": [{"attrs": {"name": "test"}, '
'"identifiers": {}, ' '"identifiers": {}, '
'"model": "authentik_core.Group"}]}' '"model": "authentik_core.Group"}]}'
@ -54,7 +54,7 @@ class TestBlueprintsV1(TransactionTestCase):
}, },
) )
importer = Importer( importer = StringImporter(
'{"version": 1, "entries": [{"attrs": {"name": "test999", "attributes": ' '{"version": 1, "entries": [{"attrs": {"name": "test999", "attributes": '
'{"key": ["updated_value"]}}, "identifiers": {"attributes": {"other_key": ' '{"key": ["updated_value"]}}, "identifiers": {"attributes": {"other_key": '
'["other_value"]}}, "model": "authentik_core.Group"}]}' '["other_value"]}}, "model": "authentik_core.Group"}]}'
@ -103,7 +103,7 @@ class TestBlueprintsV1(TransactionTestCase):
self.assertEqual(len(export.entries), 3) self.assertEqual(len(export.entries), 3)
export_yaml = exporter.export_to_string() export_yaml = exporter.export_to_string()
importer = Importer(export_yaml) importer = StringImporter(export_yaml)
self.assertTrue(importer.validate()[0]) self.assertTrue(importer.validate()[0])
self.assertTrue(importer.apply()) self.assertTrue(importer.apply())
@ -113,14 +113,14 @@ class TestBlueprintsV1(TransactionTestCase):
"""Test export and import it twice""" """Test export and import it twice"""
count_initial = Prompt.objects.filter(field_key="username").count() count_initial = Prompt.objects.filter(field_key="username").count()
importer = Importer(load_fixture("fixtures/static_prompt_export.yaml")) importer = StringImporter(load_fixture("fixtures/static_prompt_export.yaml"))
self.assertTrue(importer.validate()[0]) self.assertTrue(importer.validate()[0])
self.assertTrue(importer.apply()) self.assertTrue(importer.apply())
count_before = Prompt.objects.filter(field_key="username").count() count_before = Prompt.objects.filter(field_key="username").count()
self.assertEqual(count_initial + 1, count_before) self.assertEqual(count_initial + 1, count_before)
importer = Importer(load_fixture("fixtures/static_prompt_export.yaml")) importer = StringImporter(load_fixture("fixtures/static_prompt_export.yaml"))
self.assertTrue(importer.apply()) self.assertTrue(importer.apply())
self.assertEqual(Prompt.objects.filter(field_key="username").count(), count_before) self.assertEqual(Prompt.objects.filter(field_key="username").count(), count_before)
@ -130,7 +130,7 @@ class TestBlueprintsV1(TransactionTestCase):
ExpressionPolicy.objects.filter(name="foo-bar-baz-qux").delete() ExpressionPolicy.objects.filter(name="foo-bar-baz-qux").delete()
Group.objects.filter(name="test").delete() Group.objects.filter(name="test").delete()
environ["foo"] = generate_id() environ["foo"] = generate_id()
importer = Importer(load_fixture("fixtures/tags.yaml"), {"bar": "baz"}) importer = StringImporter(load_fixture("fixtures/tags.yaml"), {"bar": "baz"})
self.assertTrue(importer.validate()[0]) self.assertTrue(importer.validate()[0])
self.assertTrue(importer.apply()) self.assertTrue(importer.apply())
policy = ExpressionPolicy.objects.filter(name="foo-bar-baz-qux").first() policy = ExpressionPolicy.objects.filter(name="foo-bar-baz-qux").first()
@ -247,7 +247,7 @@ class TestBlueprintsV1(TransactionTestCase):
exporter = FlowExporter(flow) exporter = FlowExporter(flow)
export_yaml = exporter.export_to_string() export_yaml = exporter.export_to_string()
importer = Importer(export_yaml) importer = StringImporter(export_yaml)
self.assertTrue(importer.validate()[0]) self.assertTrue(importer.validate()[0])
self.assertTrue(importer.apply()) self.assertTrue(importer.apply())
self.assertTrue(UserLoginStage.objects.filter(name=stage_name).exists()) self.assertTrue(UserLoginStage.objects.filter(name=stage_name).exists())
@ -296,7 +296,7 @@ class TestBlueprintsV1(TransactionTestCase):
exporter = FlowExporter(flow) exporter = FlowExporter(flow)
export_yaml = exporter.export_to_string() export_yaml = exporter.export_to_string()
importer = Importer(export_yaml) importer = StringImporter(export_yaml)
self.assertTrue(importer.validate()[0]) self.assertTrue(importer.validate()[0])
self.assertTrue(importer.apply()) self.assertTrue(importer.apply())

View File

@ -1,7 +1,7 @@
"""Test blueprints v1""" """Test blueprints v1"""
from django.test import TransactionTestCase from django.test import TransactionTestCase
from authentik.blueprints.v1.importer import Importer from authentik.blueprints.v1.importer import StringImporter
from authentik.core.models import Application, Token, User from authentik.core.models import Application, Token, User
from authentik.core.tests.utils import create_test_admin_user from authentik.core.tests.utils import create_test_admin_user
from authentik.flows.models import Flow from authentik.flows.models import Flow
@ -18,7 +18,7 @@ class TestBlueprintsV1ConditionalFields(TransactionTestCase):
self.uid = generate_id() self.uid = generate_id()
import_yaml = load_fixture("fixtures/conditional_fields.yaml", uid=self.uid, user=user.pk) import_yaml = load_fixture("fixtures/conditional_fields.yaml", uid=self.uid, user=user.pk)
importer = Importer(import_yaml) importer = StringImporter(import_yaml)
self.assertTrue(importer.validate()[0]) self.assertTrue(importer.validate()[0])
self.assertTrue(importer.apply()) self.assertTrue(importer.apply())

View File

@ -1,7 +1,7 @@
"""Test blueprints v1""" """Test blueprints v1"""
from django.test import TransactionTestCase from django.test import TransactionTestCase
from authentik.blueprints.v1.importer import Importer from authentik.blueprints.v1.importer import StringImporter
from authentik.flows.models import Flow from authentik.flows.models import Flow
from authentik.lib.generators import generate_id from authentik.lib.generators import generate_id
from authentik.lib.tests.utils import load_fixture from authentik.lib.tests.utils import load_fixture
@ -18,7 +18,7 @@ class TestBlueprintsV1Conditions(TransactionTestCase):
"fixtures/conditions_fulfilled.yaml", id1=flow_slug1, id2=flow_slug2 "fixtures/conditions_fulfilled.yaml", id1=flow_slug1, id2=flow_slug2
) )
importer = Importer(import_yaml) importer = StringImporter(import_yaml)
self.assertTrue(importer.validate()[0]) self.assertTrue(importer.validate()[0])
self.assertTrue(importer.apply()) self.assertTrue(importer.apply())
# Ensure objects exist # Ensure objects exist
@ -35,7 +35,7 @@ class TestBlueprintsV1Conditions(TransactionTestCase):
"fixtures/conditions_not_fulfilled.yaml", id1=flow_slug1, id2=flow_slug2 "fixtures/conditions_not_fulfilled.yaml", id1=flow_slug1, id2=flow_slug2
) )
importer = Importer(import_yaml) importer = StringImporter(import_yaml)
self.assertTrue(importer.validate()[0]) self.assertTrue(importer.validate()[0])
self.assertTrue(importer.apply()) self.assertTrue(importer.apply())
# Ensure objects do not exist # Ensure objects do not exist

View File

@ -1,7 +1,7 @@
"""Test blueprints v1""" """Test blueprints v1"""
from django.test import TransactionTestCase from django.test import TransactionTestCase
from authentik.blueprints.v1.importer import Importer from authentik.blueprints.v1.importer import StringImporter
from authentik.flows.models import Flow from authentik.flows.models import Flow
from authentik.lib.generators import generate_id from authentik.lib.generators import generate_id
from authentik.lib.tests.utils import load_fixture from authentik.lib.tests.utils import load_fixture
@ -15,7 +15,7 @@ class TestBlueprintsV1State(TransactionTestCase):
flow_slug = generate_id() flow_slug = generate_id()
import_yaml = load_fixture("fixtures/state_present.yaml", id=flow_slug) import_yaml = load_fixture("fixtures/state_present.yaml", id=flow_slug)
importer = Importer(import_yaml) importer = StringImporter(import_yaml)
self.assertTrue(importer.validate()[0]) self.assertTrue(importer.validate()[0])
self.assertTrue(importer.apply()) self.assertTrue(importer.apply())
# Ensure object exists # Ensure object exists
@ -30,7 +30,7 @@ class TestBlueprintsV1State(TransactionTestCase):
self.assertEqual(flow.title, "bar") self.assertEqual(flow.title, "bar")
# Ensure importer updates it # Ensure importer updates it
importer = Importer(import_yaml) importer = StringImporter(import_yaml)
self.assertTrue(importer.validate()[0]) self.assertTrue(importer.validate()[0])
self.assertTrue(importer.apply()) self.assertTrue(importer.apply())
flow: Flow = Flow.objects.filter(slug=flow_slug).first() flow: Flow = Flow.objects.filter(slug=flow_slug).first()
@ -41,7 +41,7 @@ class TestBlueprintsV1State(TransactionTestCase):
flow_slug = generate_id() flow_slug = generate_id()
import_yaml = load_fixture("fixtures/state_created.yaml", id=flow_slug) import_yaml = load_fixture("fixtures/state_created.yaml", id=flow_slug)
importer = Importer(import_yaml) importer = StringImporter(import_yaml)
self.assertTrue(importer.validate()[0]) self.assertTrue(importer.validate()[0])
self.assertTrue(importer.apply()) self.assertTrue(importer.apply())
# Ensure object exists # Ensure object exists
@ -56,7 +56,7 @@ class TestBlueprintsV1State(TransactionTestCase):
self.assertEqual(flow.title, "bar") self.assertEqual(flow.title, "bar")
# Ensure importer doesn't update it # Ensure importer doesn't update it
importer = Importer(import_yaml) importer = StringImporter(import_yaml)
self.assertTrue(importer.validate()[0]) self.assertTrue(importer.validate()[0])
self.assertTrue(importer.apply()) self.assertTrue(importer.apply())
flow: Flow = Flow.objects.filter(slug=flow_slug).first() flow: Flow = Flow.objects.filter(slug=flow_slug).first()
@ -67,7 +67,7 @@ class TestBlueprintsV1State(TransactionTestCase):
flow_slug = generate_id() flow_slug = generate_id()
import_yaml = load_fixture("fixtures/state_created.yaml", id=flow_slug) import_yaml = load_fixture("fixtures/state_created.yaml", id=flow_slug)
importer = Importer(import_yaml) importer = StringImporter(import_yaml)
self.assertTrue(importer.validate()[0]) self.assertTrue(importer.validate()[0])
self.assertTrue(importer.apply()) self.assertTrue(importer.apply())
# Ensure object exists # Ensure object exists
@ -75,7 +75,7 @@ class TestBlueprintsV1State(TransactionTestCase):
self.assertEqual(flow.slug, flow_slug) self.assertEqual(flow.slug, flow_slug)
import_yaml = load_fixture("fixtures/state_absent.yaml", id=flow_slug) import_yaml = load_fixture("fixtures/state_absent.yaml", id=flow_slug)
importer = Importer(import_yaml) importer = StringImporter(import_yaml)
self.assertTrue(importer.validate()[0]) self.assertTrue(importer.validate()[0])
self.assertTrue(importer.apply()) self.assertTrue(importer.apply())
flow: Flow = Flow.objects.filter(slug=flow_slug).first() flow: Flow = Flow.objects.filter(slug=flow_slug).first()

View File

@ -52,6 +52,7 @@ class BlueprintEntryDesiredState(Enum):
ABSENT = "absent" ABSENT = "absent"
PRESENT = "present" PRESENT = "present"
CREATED = "created" CREATED = "created"
MUST_CREATED = "must_created"
@dataclass @dataclass

View File

@ -86,27 +86,22 @@ class Importer:
"""Import Blueprint from YAML""" """Import Blueprint from YAML"""
logger: BoundLogger logger: BoundLogger
_import: Blueprint
def __init__(self, yaml_input: str, context: Optional[dict] = None): def __init__(self, blueprint: Blueprint, context: Optional[dict] = None):
self.__pk_map: dict[Any, Model] = {} self.__pk_map: dict[Any, Model] = {}
self._import = blueprint
self.logger = get_logger() self.logger = get_logger()
import_dict = load(yaml_input, BlueprintLoader)
try:
self.__import = from_dict(
Blueprint, import_dict, config=Config(cast=[BlueprintEntryDesiredState])
)
except DaciteError as exc:
raise EntryInvalidError from exc
ctx = {} ctx = {}
always_merger.merge(ctx, self.__import.context) always_merger.merge(ctx, self._import.context)
if context: if context:
always_merger.merge(ctx, context) always_merger.merge(ctx, context)
self.__import.context = ctx self._import.context = ctx
@property @property
def blueprint(self) -> Blueprint: def blueprint(self) -> Blueprint:
"""Get imported blueprint""" """Get imported blueprint"""
return self.__import return self._import
def __update_pks_for_attrs(self, attrs: dict[str, Any]) -> dict[str, Any]: def __update_pks_for_attrs(self, attrs: dict[str, Any]) -> dict[str, Any]:
"""Replace any value if it is a known primary key of an other object""" """Replace any value if it is a known primary key of an other object"""
@ -152,11 +147,11 @@ class Importer:
# pylint: disable-msg=too-many-locals # pylint: disable-msg=too-many-locals
def _validate_single(self, entry: BlueprintEntry) -> Optional[BaseSerializer]: def _validate_single(self, entry: BlueprintEntry) -> Optional[BaseSerializer]:
"""Validate a single entry""" """Validate a single entry"""
if not entry.check_all_conditions_match(self.__import): if not entry.check_all_conditions_match(self._import):
self.logger.debug("One or more conditions of this entry are not fulfilled, skipping") self.logger.debug("One or more conditions of this entry are not fulfilled, skipping")
return None return None
model_app_label, model_name = entry.get_model(self.__import).split(".") model_app_label, model_name = entry.get_model(self._import).split(".")
model: type[SerializerModel] = registry.get_model(model_app_label, model_name) model: type[SerializerModel] = registry.get_model(model_app_label, model_name)
# Don't use isinstance since we don't want to check for inheritance # Don't use isinstance since we don't want to check for inheritance
if not is_model_allowed(model): if not is_model_allowed(model):
@ -164,7 +159,7 @@ class Importer:
if issubclass(model, BaseMetaModel): if issubclass(model, BaseMetaModel):
serializer_class: type[Serializer] = model.serializer() serializer_class: type[Serializer] = model.serializer()
serializer = serializer_class( serializer = serializer_class(
data=entry.get_attrs(self.__import), data=entry.get_attrs(self._import),
context={ context={
SERIALIZER_CONTEXT_BLUEPRINT: entry, SERIALIZER_CONTEXT_BLUEPRINT: entry,
}, },
@ -182,7 +177,7 @@ class Importer:
# the full serializer for later usage # the full serializer for later usage
# Because a model might have multiple unique columns, we chain all identifiers together # Because a model might have multiple unique columns, we chain all identifiers together
# to create an OR query. # to create an OR query.
updated_identifiers = self.__update_pks_for_attrs(entry.get_identifiers(self.__import)) updated_identifiers = self.__update_pks_for_attrs(entry.get_identifiers(self._import))
for key, value in list(updated_identifiers.items()): for key, value in list(updated_identifiers.items()):
if isinstance(value, dict) and "pk" in value: if isinstance(value, dict) and "pk" in value:
del updated_identifiers[key] del updated_identifiers[key]
@ -208,6 +203,13 @@ class Importer:
) )
serializer_kwargs["instance"] = model_instance serializer_kwargs["instance"] = model_instance
serializer_kwargs["partial"] = True serializer_kwargs["partial"] = True
elif model_instance and entry.state == BlueprintEntryDesiredState.MUST_CREATED:
raise EntryInvalidError(
(
f"state is set to {BlueprintEntryDesiredState.MUST_CREATED}"
" and object exists already"
)
)
else: else:
self.logger.debug( self.logger.debug(
"initialised new serializer instance", "initialised new serializer instance",
@ -220,7 +222,7 @@ class Importer:
model_instance.pk = updated_identifiers["pk"] model_instance.pk = updated_identifiers["pk"]
serializer_kwargs["instance"] = model_instance serializer_kwargs["instance"] = model_instance
try: try:
full_data = self.__update_pks_for_attrs(entry.get_attrs(self.__import)) full_data = self.__update_pks_for_attrs(entry.get_attrs(self._import))
except ValueError as exc: except ValueError as exc:
raise EntryInvalidError(exc) from exc raise EntryInvalidError(exc) from exc
always_merger.merge(full_data, updated_identifiers) always_merger.merge(full_data, updated_identifiers)
@ -255,8 +257,8 @@ class Importer:
def _apply_models(self) -> bool: def _apply_models(self) -> bool:
"""Apply (create/update) models yaml""" """Apply (create/update) models yaml"""
self.__pk_map = {} self.__pk_map = {}
for entry in self.__import.entries: for entry in self._import.entries:
model_app_label, model_name = entry.get_model(self.__import).split(".") model_app_label, model_name = entry.get_model(self._import).split(".")
try: try:
model: type[SerializerModel] = registry.get_model(model_app_label, model_name) model: type[SerializerModel] = registry.get_model(model_app_label, model_name)
except LookupError: except LookupError:
@ -269,15 +271,19 @@ class Importer:
serializer = self._validate_single(entry) serializer = self._validate_single(entry)
except EntryInvalidError as exc: except EntryInvalidError as exc:
# For deleting objects we don't need the serializer to be valid # For deleting objects we don't need the serializer to be valid
if entry.get_state(self.__import) == BlueprintEntryDesiredState.ABSENT: if entry.get_state(self._import) == BlueprintEntryDesiredState.ABSENT:
continue continue
self.logger.warning(f"entry invalid: {exc}", entry=entry, error=exc) self.logger.warning(f"entry invalid: {exc}", entry=entry, error=exc)
return False return False
if not serializer: if not serializer:
continue continue
state = entry.get_state(self.__import) state = entry.get_state(self._import)
if state in [BlueprintEntryDesiredState.PRESENT, BlueprintEntryDesiredState.CREATED]: if state in [
BlueprintEntryDesiredState.PRESENT,
BlueprintEntryDesiredState.CREATED,
BlueprintEntryDesiredState.MUST_CREATED,
]:
instance = serializer.instance instance = serializer.instance
if ( if (
instance instance
@ -309,8 +315,8 @@ class Importer:
"""Validate loaded blueprint export, ensure all models are allowed """Validate loaded blueprint export, ensure all models are allowed
and serializers have no errors""" and serializers have no errors"""
self.logger.debug("Starting blueprint import validation") self.logger.debug("Starting blueprint import validation")
orig_import = deepcopy(self.__import) orig_import = deepcopy(self._import)
if self.__import.version != 1: if self._import.version != 1:
self.logger.warning("Invalid blueprint version") self.logger.warning("Invalid blueprint version")
return False, [{"event": "Invalid blueprint version"}] return False, [{"event": "Invalid blueprint version"}]
with ( with (
@ -323,5 +329,19 @@ class Importer:
for log in logs: for log in logs:
getattr(self.logger, log.get("log_level"))(**log) getattr(self.logger, log.get("log_level"))(**log)
self.logger.debug("Finished blueprint import validation") self.logger.debug("Finished blueprint import validation")
self.__import = orig_import self._import = orig_import
return successful, logs return successful, logs
class StringImporter(Importer):
"""Importer that also parses from string"""
def __init__(self, yaml_input: str, context: dict | None = None):
import_dict = load(yaml_input, BlueprintLoader)
try:
_import = from_dict(
Blueprint, import_dict, config=Config(cast=[BlueprintEntryDesiredState])
)
except DaciteError as exc:
raise EntryInvalidError from exc
super().__init__(_import, context)

View File

@ -26,7 +26,7 @@ from authentik.blueprints.models import (
BlueprintRetrievalFailed, BlueprintRetrievalFailed,
) )
from authentik.blueprints.v1.common import BlueprintLoader, BlueprintMetadata, EntryInvalidError from authentik.blueprints.v1.common import BlueprintLoader, BlueprintMetadata, EntryInvalidError
from authentik.blueprints.v1.importer import Importer from authentik.blueprints.v1.importer import StringImporter
from authentik.blueprints.v1.labels import LABEL_AUTHENTIK_INSTANTIATE from authentik.blueprints.v1.labels import LABEL_AUTHENTIK_INSTANTIATE
from authentik.blueprints.v1.oci import OCI_PREFIX from authentik.blueprints.v1.oci import OCI_PREFIX
from authentik.events.monitored_tasks import ( from authentik.events.monitored_tasks import (
@ -190,7 +190,7 @@ def apply_blueprint(self: MonitoredTask, instance_pk: str):
self.set_uid(slugify(instance.name)) self.set_uid(slugify(instance.name))
blueprint_content = instance.retrieve() blueprint_content = instance.retrieve()
file_hash = sha512(blueprint_content.encode()).hexdigest() file_hash = sha512(blueprint_content.encode()).hexdigest()
importer = Importer(blueprint_content, instance.context) importer = StringImporter(blueprint_content, instance.context)
if importer.blueprint.metadata: if importer.blueprint.metadata:
instance.metadata = asdict(importer.blueprint.metadata) instance.metadata = asdict(importer.blueprint.metadata)
valid, logs = importer.validate() valid, logs = importer.validate()

View File

@ -0,0 +1,127 @@
"""transactional application and provider creation"""
from django.apps import apps
from drf_spectacular.utils import PolymorphicProxySerializer, extend_schema, extend_schema_field
from rest_framework.exceptions import ValidationError
from rest_framework.fields import BooleanField, CharField, ChoiceField, DictField, ListField
from rest_framework.permissions import IsAdminUser
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.views import APIView
from yaml import ScalarNode
from authentik.blueprints.v1.common import (
Blueprint,
BlueprintEntry,
BlueprintEntryDesiredState,
KeyOf,
)
from authentik.blueprints.v1.importer import Importer
from authentik.core.api.applications import ApplicationSerializer
from authentik.core.api.utils import PassiveSerializer
from authentik.core.models import Provider
from authentik.lib.utils.reflection import all_subclasses
def get_provider_serializer_mapping():
"""Get a mapping of all providers' model names and their serializers"""
mapping = {}
for model in all_subclasses(Provider):
if model._meta.abstract:
continue
mapping[f"{model._meta.app_label}.{model._meta.model_name}"] = model().serializer
return mapping
@extend_schema_field(
PolymorphicProxySerializer(
component_name="model",
serializers=get_provider_serializer_mapping,
resource_type_field_name="provider_model",
)
)
class TransactionProviderField(DictField):
"""Dictionary field which can hold provider creation data"""
class TransactionApplicationSerializer(PassiveSerializer):
"""Serializer for creating a provider and an application in one transaction"""
app = ApplicationSerializer()
provider_model = ChoiceField(choices=list(get_provider_serializer_mapping().keys()))
provider = TransactionProviderField()
_provider_model: type[Provider] = None
def validate_provider_model(self, fq_model_name: str) -> str:
"""Validate that the model exists and is a provider"""
if "." not in fq_model_name:
raise ValidationError("Invalid provider model")
try:
app, _, model_name = fq_model_name.partition(".")
model = apps.get_model(app, model_name)
if not issubclass(model, Provider):
raise ValidationError("Invalid provider model")
self._provider_model = model
except LookupError:
raise ValidationError("Invalid provider model")
return fq_model_name
class TransactionApplicationResponseSerializer(PassiveSerializer):
"""Transactional creation response"""
valid = BooleanField()
applied = BooleanField()
logs = ListField(child=CharField())
class TransactionalApplicationView(APIView):
"""Create provider and application and attach them in a single transaction"""
permission_classes = [IsAdminUser]
@extend_schema(
request=TransactionApplicationSerializer(),
responses={
200: TransactionApplicationResponseSerializer(),
},
)
def put(self, request: Request) -> Response:
"""Convert data into a blueprint, validate it and apply it"""
data = TransactionApplicationSerializer(data=request.data)
data.is_valid(raise_exception=True)
print(data.validated_data)
blueprint = Blueprint()
blueprint.entries.append(
BlueprintEntry(
model=data.validated_data["provider_model"],
state=BlueprintEntryDesiredState.MUST_CREATED,
identifiers={
"name": data.validated_data["provider"]["name"],
},
id="provider",
attrs=data.validated_data["provider"],
)
)
app_data = data.validated_data["app"]
app_data["provider"] = KeyOf(None, ScalarNode(tag="", value="provider"))
blueprint.entries.append(
BlueprintEntry(
model="authentik_core.application",
state=BlueprintEntryDesiredState.MUST_CREATED,
identifiers={
"slug": data.validated_data["app"]["slug"],
},
attrs=app_data,
)
)
importer = Importer(blueprint, {})
response = {"valid": False, "applied": False, "logs": []}
valid, logs = importer.validate()
response["logs"] = [x["event"] for x in logs]
response["valid"] = valid
if valid:
applied = importer.apply()
response["applied"] = applied
return Response(response, status=200)

View File

@ -0,0 +1,45 @@
"""Test Transactional API"""
from json import loads
from django.urls import reverse
from rest_framework.test import APITestCase
from authentik.core.models import Application
from authentik.core.tests.utils import create_test_admin_user, create_test_flow
from authentik.lib.generators import generate_id
from authentik.providers.oauth2.models import OAuth2Provider
class TestTransactionalApplicationsAPI(APITestCase):
"""Test Transactional API"""
def setUp(self) -> None:
self.user = create_test_admin_user()
def test_create_transactional(self):
"""Test transactional Application + provider creation"""
self.client.force_login(self.user)
uid = generate_id()
authorization_flow = create_test_flow()
response = self.client.put(
reverse("authentik_api:core-transactional-application"),
data={
"app": {
"name": uid,
"slug": uid,
},
"provider_model": "authentik_providers_oauth2.oauth2provider",
"provider": {
"name": uid,
"authorization_flow": str(authorization_flow.pk),
},
},
)
response_body = loads(response.content.decode())
self.assertTrue(response_body["valid"])
self.assertTrue(response_body["applied"])
provider = OAuth2Provider.objects.filter(name=uid).first()
self.assertIsNotNone(provider)
app = Application.objects.filter(slug=uid).first()
self.assertIsNotNone(app)
self.assertEqual(app.provider.pk, provider.pk)

View File

@ -15,6 +15,7 @@ from authentik.core.api.propertymappings import PropertyMappingViewSet
from authentik.core.api.providers import ProviderViewSet from authentik.core.api.providers import ProviderViewSet
from authentik.core.api.sources import SourceViewSet, UserSourceConnectionViewSet from authentik.core.api.sources import SourceViewSet, UserSourceConnectionViewSet
from authentik.core.api.tokens import TokenViewSet from authentik.core.api.tokens import TokenViewSet
from authentik.core.api.transactional_applications import TransactionalApplicationView
from authentik.core.api.users import UserViewSet from authentik.core.api.users import UserViewSet
from authentik.core.views import apps from authentik.core.views import apps
from authentik.core.views.debug import AccessDeniedView from authentik.core.views.debug import AccessDeniedView
@ -70,6 +71,11 @@ urlpatterns = [
api_urlpatterns = [ api_urlpatterns = [
("core/authenticated_sessions", AuthenticatedSessionViewSet), ("core/authenticated_sessions", AuthenticatedSessionViewSet),
("core/applications", ApplicationViewSet), ("core/applications", ApplicationViewSet),
path(
"core/transactional/applications/",
TransactionalApplicationView.as_view(),
name="core-transactional-application",
),
("core/groups", GroupViewSet), ("core/groups", GroupViewSet),
("core/users", UserViewSet), ("core/users", UserViewSet),
("core/tokens", TokenViewSet), ("core/tokens", TokenViewSet),

View File

@ -16,7 +16,7 @@ from structlog.stdlib import get_logger
from authentik.api.decorators import permission_required from authentik.api.decorators import permission_required
from authentik.blueprints.v1.exporter import FlowExporter from authentik.blueprints.v1.exporter import FlowExporter
from authentik.blueprints.v1.importer import SERIALIZER_CONTEXT_BLUEPRINT, Importer from authentik.blueprints.v1.importer import SERIALIZER_CONTEXT_BLUEPRINT, StringImporter
from authentik.core.api.used_by import UsedByMixin from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import CacheSerializer, LinkSerializer, PassiveSerializer from authentik.core.api.utils import CacheSerializer, LinkSerializer, PassiveSerializer
from authentik.events.utils import sanitize_dict from authentik.events.utils import sanitize_dict
@ -181,7 +181,7 @@ class FlowViewSet(UsedByMixin, ModelViewSet):
if not file: if not file:
return Response(data=import_response.initial_data, status=400) return Response(data=import_response.initial_data, status=400)
importer = Importer(file.read().decode()) importer = StringImporter(file.read().decode())
valid, logs = importer.validate() valid, logs = importer.validate()
import_response.initial_data["logs"] = [sanitize_dict(log) for log in logs] import_response.initial_data["logs"] = [sanitize_dict(log) for log in logs]
import_response.initial_data["success"] = valid import_response.initial_data["success"] = valid

View File

@ -73,40 +73,24 @@ QS_QUERY = "query"
def challenge_types(): def challenge_types():
"""This is a workaround for PolymorphicProxySerializer not accepting a callable for """This function returns a class which is an iterator, which returns the
`serializers`. This function returns a class which is an iterator, which returns the
subclasses of Challenge, and Challenge itself.""" subclasses of Challenge, and Challenge itself."""
class Inner(dict):
"""dummy class with custom callback on .items()"""
def items(self):
mapping = {} mapping = {}
classes = all_subclasses(Challenge) classes = all_subclasses(Challenge)
classes.remove(WithUserInfoChallenge) classes.remove(WithUserInfoChallenge)
for cls in classes: for cls in classes:
mapping[cls().fields["component"].default] = cls mapping[cls().fields["component"].default] = cls
return mapping.items() return mapping
return Inner()
def challenge_response_types(): def challenge_response_types():
"""This is a workaround for PolymorphicProxySerializer not accepting a callable for """This function returns a class which is an iterator, which returns the
`serializers`. This function returns a class which is an iterator, which returns the
subclasses of Challenge, and Challenge itself.""" subclasses of Challenge, and Challenge itself."""
class Inner(dict):
"""dummy class with custom callback on .items()"""
def items(self):
mapping = {} mapping = {}
classes = all_subclasses(ChallengeResponse) classes = all_subclasses(ChallengeResponse)
for cls in classes: for cls in classes:
mapping[cls(stage=None).fields["component"].default] = cls mapping[cls(stage=None).fields["component"].default] = cls
return mapping.items() return mapping
return Inner()
class InvalidStageError(SentryIgnoredException): class InvalidStageError(SentryIgnoredException):
@ -264,7 +248,7 @@ class FlowExecutorView(APIView):
responses={ responses={
200: PolymorphicProxySerializer( 200: PolymorphicProxySerializer(
component_name="ChallengeTypes", component_name="ChallengeTypes",
serializers=challenge_types(), serializers=challenge_types,
resource_type_field_name="component", resource_type_field_name="component",
), ),
}, },
@ -304,13 +288,13 @@ class FlowExecutorView(APIView):
responses={ responses={
200: PolymorphicProxySerializer( 200: PolymorphicProxySerializer(
component_name="ChallengeTypes", component_name="ChallengeTypes",
serializers=challenge_types(), serializers=challenge_types,
resource_type_field_name="component", resource_type_field_name="component",
), ),
}, },
request=PolymorphicProxySerializer( request=PolymorphicProxySerializer(
component_name="FlowChallengeResponse", component_name="FlowChallengeResponse",
serializers=challenge_response_types(), serializers=challenge_response_types,
resource_type_field_name="component", resource_type_field_name="component",
), ),
parameters=[ parameters=[

View File

@ -59,7 +59,8 @@
"enum": [ "enum": [
"absent", "absent",
"present", "present",
"created" "created",
"must_created"
], ],
"default": "present" "default": "present"
}, },
@ -95,7 +96,8 @@
"enum": [ "enum": [
"absent", "absent",
"present", "present",
"created" "created",
"must_created"
], ],
"default": "present" "default": "present"
}, },
@ -131,7 +133,8 @@
"enum": [ "enum": [
"absent", "absent",
"present", "present",
"created" "created",
"must_created"
], ],
"default": "present" "default": "present"
}, },
@ -167,7 +170,8 @@
"enum": [ "enum": [
"absent", "absent",
"present", "present",
"created" "created",
"must_created"
], ],
"default": "present" "default": "present"
}, },
@ -203,7 +207,8 @@
"enum": [ "enum": [
"absent", "absent",
"present", "present",
"created" "created",
"must_created"
], ],
"default": "present" "default": "present"
}, },
@ -239,7 +244,8 @@
"enum": [ "enum": [
"absent", "absent",
"present", "present",
"created" "created",
"must_created"
], ],
"default": "present" "default": "present"
}, },
@ -275,7 +281,8 @@
"enum": [ "enum": [
"absent", "absent",
"present", "present",
"created" "created",
"must_created"
], ],
"default": "present" "default": "present"
}, },
@ -311,7 +318,8 @@
"enum": [ "enum": [
"absent", "absent",
"present", "present",
"created" "created",
"must_created"
], ],
"default": "present" "default": "present"
}, },
@ -347,7 +355,8 @@
"enum": [ "enum": [
"absent", "absent",
"present", "present",
"created" "created",
"must_created"
], ],
"default": "present" "default": "present"
}, },
@ -383,7 +392,8 @@
"enum": [ "enum": [
"absent", "absent",
"present", "present",
"created" "created",
"must_created"
], ],
"default": "present" "default": "present"
}, },
@ -419,7 +429,8 @@
"enum": [ "enum": [
"absent", "absent",
"present", "present",
"created" "created",
"must_created"
], ],
"default": "present" "default": "present"
}, },
@ -455,7 +466,8 @@
"enum": [ "enum": [
"absent", "absent",
"present", "present",
"created" "created",
"must_created"
], ],
"default": "present" "default": "present"
}, },
@ -491,7 +503,8 @@
"enum": [ "enum": [
"absent", "absent",
"present", "present",
"created" "created",
"must_created"
], ],
"default": "present" "default": "present"
}, },
@ -527,7 +540,8 @@
"enum": [ "enum": [
"absent", "absent",
"present", "present",
"created" "created",
"must_created"
], ],
"default": "present" "default": "present"
}, },
@ -563,7 +577,8 @@
"enum": [ "enum": [
"absent", "absent",
"present", "present",
"created" "created",
"must_created"
], ],
"default": "present" "default": "present"
}, },
@ -599,7 +614,8 @@
"enum": [ "enum": [
"absent", "absent",
"present", "present",
"created" "created",
"must_created"
], ],
"default": "present" "default": "present"
}, },
@ -635,7 +651,8 @@
"enum": [ "enum": [
"absent", "absent",
"present", "present",
"created" "created",
"must_created"
], ],
"default": "present" "default": "present"
}, },
@ -671,7 +688,8 @@
"enum": [ "enum": [
"absent", "absent",
"present", "present",
"created" "created",
"must_created"
], ],
"default": "present" "default": "present"
}, },
@ -707,7 +725,8 @@
"enum": [ "enum": [
"absent", "absent",
"present", "present",
"created" "created",
"must_created"
], ],
"default": "present" "default": "present"
}, },
@ -743,7 +762,8 @@
"enum": [ "enum": [
"absent", "absent",
"present", "present",
"created" "created",
"must_created"
], ],
"default": "present" "default": "present"
}, },
@ -779,7 +799,8 @@
"enum": [ "enum": [
"absent", "absent",
"present", "present",
"created" "created",
"must_created"
], ],
"default": "present" "default": "present"
}, },
@ -815,7 +836,8 @@
"enum": [ "enum": [
"absent", "absent",
"present", "present",
"created" "created",
"must_created"
], ],
"default": "present" "default": "present"
}, },
@ -851,7 +873,8 @@
"enum": [ "enum": [
"absent", "absent",
"present", "present",
"created" "created",
"must_created"
], ],
"default": "present" "default": "present"
}, },
@ -887,7 +910,8 @@
"enum": [ "enum": [
"absent", "absent",
"present", "present",
"created" "created",
"must_created"
], ],
"default": "present" "default": "present"
}, },
@ -923,7 +947,8 @@
"enum": [ "enum": [
"absent", "absent",
"present", "present",
"created" "created",
"must_created"
], ],
"default": "present" "default": "present"
}, },
@ -959,7 +984,8 @@
"enum": [ "enum": [
"absent", "absent",
"present", "present",
"created" "created",
"must_created"
], ],
"default": "present" "default": "present"
}, },
@ -995,7 +1021,8 @@
"enum": [ "enum": [
"absent", "absent",
"present", "present",
"created" "created",
"must_created"
], ],
"default": "present" "default": "present"
}, },
@ -1031,7 +1058,8 @@
"enum": [ "enum": [
"absent", "absent",
"present", "present",
"created" "created",
"must_created"
], ],
"default": "present" "default": "present"
}, },
@ -1067,7 +1095,8 @@
"enum": [ "enum": [
"absent", "absent",
"present", "present",
"created" "created",
"must_created"
], ],
"default": "present" "default": "present"
}, },
@ -1103,7 +1132,8 @@
"enum": [ "enum": [
"absent", "absent",
"present", "present",
"created" "created",
"must_created"
], ],
"default": "present" "default": "present"
}, },
@ -1139,7 +1169,8 @@
"enum": [ "enum": [
"absent", "absent",
"present", "present",
"created" "created",
"must_created"
], ],
"default": "present" "default": "present"
}, },
@ -1175,7 +1206,8 @@
"enum": [ "enum": [
"absent", "absent",
"present", "present",
"created" "created",
"must_created"
], ],
"default": "present" "default": "present"
}, },
@ -1211,7 +1243,8 @@
"enum": [ "enum": [
"absent", "absent",
"present", "present",
"created" "created",
"must_created"
], ],
"default": "present" "default": "present"
}, },
@ -1247,7 +1280,8 @@
"enum": [ "enum": [
"absent", "absent",
"present", "present",
"created" "created",
"must_created"
], ],
"default": "present" "default": "present"
}, },
@ -1283,7 +1317,8 @@
"enum": [ "enum": [
"absent", "absent",
"present", "present",
"created" "created",
"must_created"
], ],
"default": "present" "default": "present"
}, },
@ -1319,7 +1354,8 @@
"enum": [ "enum": [
"absent", "absent",
"present", "present",
"created" "created",
"must_created"
], ],
"default": "present" "default": "present"
}, },
@ -1355,7 +1391,8 @@
"enum": [ "enum": [
"absent", "absent",
"present", "present",
"created" "created",
"must_created"
], ],
"default": "present" "default": "present"
}, },
@ -1391,7 +1428,8 @@
"enum": [ "enum": [
"absent", "absent",
"present", "present",
"created" "created",
"must_created"
], ],
"default": "present" "default": "present"
}, },
@ -1427,7 +1465,8 @@
"enum": [ "enum": [
"absent", "absent",
"present", "present",
"created" "created",
"must_created"
], ],
"default": "present" "default": "present"
}, },
@ -1463,7 +1502,8 @@
"enum": [ "enum": [
"absent", "absent",
"present", "present",
"created" "created",
"must_created"
], ],
"default": "present" "default": "present"
}, },
@ -1499,7 +1539,8 @@
"enum": [ "enum": [
"absent", "absent",
"present", "present",
"created" "created",
"must_created"
], ],
"default": "present" "default": "present"
}, },
@ -1535,7 +1576,8 @@
"enum": [ "enum": [
"absent", "absent",
"present", "present",
"created" "created",
"must_created"
], ],
"default": "present" "default": "present"
}, },
@ -1571,7 +1613,8 @@
"enum": [ "enum": [
"absent", "absent",
"present", "present",
"created" "created",
"must_created"
], ],
"default": "present" "default": "present"
}, },
@ -1607,7 +1650,8 @@
"enum": [ "enum": [
"absent", "absent",
"present", "present",
"created" "created",
"must_created"
], ],
"default": "present" "default": "present"
}, },
@ -1643,7 +1687,8 @@
"enum": [ "enum": [
"absent", "absent",
"present", "present",
"created" "created",
"must_created"
], ],
"default": "present" "default": "present"
}, },
@ -1679,7 +1724,8 @@
"enum": [ "enum": [
"absent", "absent",
"present", "present",
"created" "created",
"must_created"
], ],
"default": "present" "default": "present"
}, },
@ -1715,7 +1761,8 @@
"enum": [ "enum": [
"absent", "absent",
"present", "present",
"created" "created",
"must_created"
], ],
"default": "present" "default": "present"
}, },
@ -1751,7 +1798,8 @@
"enum": [ "enum": [
"absent", "absent",
"present", "present",
"created" "created",
"must_created"
], ],
"default": "present" "default": "present"
}, },
@ -1787,7 +1835,8 @@
"enum": [ "enum": [
"absent", "absent",
"present", "present",
"created" "created",
"must_created"
], ],
"default": "present" "default": "present"
}, },
@ -1823,7 +1872,8 @@
"enum": [ "enum": [
"absent", "absent",
"present", "present",
"created" "created",
"must_created"
], ],
"default": "present" "default": "present"
}, },
@ -1859,7 +1909,8 @@
"enum": [ "enum": [
"absent", "absent",
"present", "present",
"created" "created",
"must_created"
], ],
"default": "present" "default": "present"
}, },
@ -1895,7 +1946,8 @@
"enum": [ "enum": [
"absent", "absent",
"present", "present",
"created" "created",
"must_created"
], ],
"default": "present" "default": "present"
}, },
@ -1931,7 +1983,8 @@
"enum": [ "enum": [
"absent", "absent",
"present", "present",
"created" "created",
"must_created"
], ],
"default": "present" "default": "present"
}, },
@ -1967,7 +2020,8 @@
"enum": [ "enum": [
"absent", "absent",
"present", "present",
"created" "created",
"must_created"
], ],
"default": "present" "default": "present"
}, },
@ -2003,7 +2057,8 @@
"enum": [ "enum": [
"absent", "absent",
"present", "present",
"created" "created",
"must_created"
], ],
"default": "present" "default": "present"
}, },
@ -2039,7 +2094,8 @@
"enum": [ "enum": [
"absent", "absent",
"present", "present",
"created" "created",
"must_created"
], ],
"default": "present" "default": "present"
}, },
@ -2075,7 +2131,8 @@
"enum": [ "enum": [
"absent", "absent",
"present", "present",
"created" "created",
"must_created"
], ],
"default": "present" "default": "present"
}, },
@ -2111,7 +2168,8 @@
"enum": [ "enum": [
"absent", "absent",
"present", "present",
"created" "created",
"must_created"
], ],
"default": "present" "default": "present"
}, },
@ -2147,7 +2205,8 @@
"enum": [ "enum": [
"absent", "absent",
"present", "present",
"created" "created",
"must_created"
], ],
"default": "present" "default": "present"
}, },
@ -2183,7 +2242,8 @@
"enum": [ "enum": [
"absent", "absent",
"present", "present",
"created" "created",
"must_created"
], ],
"default": "present" "default": "present"
}, },
@ -2219,7 +2279,8 @@
"enum": [ "enum": [
"absent", "absent",
"present", "present",
"created" "created",
"must_created"
], ],
"default": "present" "default": "present"
}, },
@ -2255,7 +2316,8 @@
"enum": [ "enum": [
"absent", "absent",
"present", "present",
"created" "created",
"must_created"
], ],
"default": "present" "default": "present"
}, },
@ -2291,7 +2353,8 @@
"enum": [ "enum": [
"absent", "absent",
"present", "present",
"created" "created",
"must_created"
], ],
"default": "present" "default": "present"
}, },
@ -2327,7 +2390,8 @@
"enum": [ "enum": [
"absent", "absent",
"present", "present",
"created" "created",
"must_created"
], ],
"default": "present" "default": "present"
}, },
@ -2363,7 +2427,8 @@
"enum": [ "enum": [
"absent", "absent",
"present", "present",
"created" "created",
"must_created"
], ],
"default": "present" "default": "present"
}, },
@ -2399,7 +2464,8 @@
"enum": [ "enum": [
"absent", "absent",
"present", "present",
"created" "created",
"must_created"
], ],
"default": "present" "default": "present"
}, },
@ -2435,7 +2501,8 @@
"enum": [ "enum": [
"absent", "absent",
"present", "present",
"created" "created",
"must_created"
], ],
"default": "present" "default": "present"
}, },
@ -2471,7 +2538,8 @@
"enum": [ "enum": [
"absent", "absent",
"present", "present",
"created" "created",
"must_created"
], ],
"default": "present" "default": "present"
}, },
@ -2507,7 +2575,8 @@
"enum": [ "enum": [
"absent", "absent",
"present", "present",
"created" "created",
"must_created"
], ],
"default": "present" "default": "present"
}, },
@ -2543,7 +2612,8 @@
"enum": [ "enum": [
"absent", "absent",
"present", "present",
"created" "created",
"must_created"
], ],
"default": "present" "default": "present"
}, },
@ -2579,7 +2649,8 @@
"enum": [ "enum": [
"absent", "absent",
"present", "present",
"created" "created",
"must_created"
], ],
"default": "present" "default": "present"
}, },

View File

@ -4349,6 +4349,39 @@ paths:
schema: schema:
$ref: '#/components/schemas/GenericError' $ref: '#/components/schemas/GenericError'
description: '' description: ''
/core/transactional/applications/:
put:
operationId: core_transactional_applications_update
description: Convert data into a blueprint, validate it and apply it
tags:
- core
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/TransactionApplicationRequest'
required: true
security:
- authentik: []
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/TransactionApplicationResponse'
description: ''
'400':
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
description: ''
'403':
content:
application/json:
schema:
$ref: '#/components/schemas/GenericError'
description: ''
/core/user_consent/: /core/user_consent/:
get: get:
operationId: core_user_consent_list operationId: core_user_consent_list
@ -37518,6 +37551,22 @@ components:
description: |- description: |-
* `twilio` - Twilio * `twilio` - Twilio
* `generic` - Generic * `generic` - Generic
ProviderModelEnum:
enum:
- authentik_providers_ldap.ldapprovider
- authentik_providers_oauth2.oauth2provider
- authentik_providers_proxy.proxyprovider
- authentik_providers_radius.radiusprovider
- authentik_providers_saml.samlprovider
- authentik_providers_scim.scimprovider
type: string
description: |-
* `authentik_providers_ldap.ldapprovider` - authentik_providers_ldap.ldapprovider
* `authentik_providers_oauth2.oauth2provider` - authentik_providers_oauth2.oauth2provider
* `authentik_providers_proxy.proxyprovider` - authentik_providers_proxy.proxyprovider
* `authentik_providers_radius.radiusprovider` - authentik_providers_radius.radiusprovider
* `authentik_providers_saml.samlprovider` - authentik_providers_saml.samlprovider
* `authentik_providers_scim.scimprovider` - authentik_providers_scim.scimprovider
ProviderRequest: ProviderRequest:
type: object type: object
description: Provider Serializer description: Provider Serializer
@ -39929,6 +39978,36 @@ components:
readOnly: true readOnly: true
required: required:
- key - key
TransactionApplicationRequest:
type: object
description: Serializer for creating a provider and an application in one transaction
properties:
app:
$ref: '#/components/schemas/ApplicationRequest'
provider_model:
$ref: '#/components/schemas/ProviderModelEnum'
provider:
$ref: '#/components/schemas/modelRequest'
required:
- app
- provider
- provider_model
TransactionApplicationResponse:
type: object
description: Transactional creation response
properties:
valid:
type: boolean
applied:
type: boolean
logs:
type: array
items:
type: string
required:
- applied
- logs
- valid
TypeCreate: TypeCreate:
type: object type: object
description: Types of an object that can be created description: Types of an object that can be created
@ -40856,6 +40935,23 @@ components:
type: integer type: integer
required: required:
- count - count
modelRequest:
oneOf:
- $ref: '#/components/schemas/LDAPProviderRequest'
- $ref: '#/components/schemas/OAuth2ProviderRequest'
- $ref: '#/components/schemas/ProxyProviderRequest'
- $ref: '#/components/schemas/RadiusProviderRequest'
- $ref: '#/components/schemas/SAMLProviderRequest'
- $ref: '#/components/schemas/SCIMProviderRequest'
discriminator:
propertyName: provider_model
mapping:
authentik_providers_ldap.ldapprovider: '#/components/schemas/LDAPProviderRequest'
authentik_providers_oauth2.oauth2provider: '#/components/schemas/OAuth2ProviderRequest'
authentik_providers_proxy.proxyprovider: '#/components/schemas/ProxyProviderRequest'
authentik_providers_radius.radiusprovider: '#/components/schemas/RadiusProviderRequest'
authentik_providers_saml.samlprovider: '#/components/schemas/SAMLProviderRequest'
authentik_providers_scim.scimprovider: '#/components/schemas/SCIMProviderRequest'
securitySchemes: securitySchemes:
authentik: authentik:
type: apiKey type: apiKey

View File

@ -7,9 +7,7 @@ In technical docuemntation, there are document "types" (similar to how there are
The most common types are: The most common types are:
- [**Procedural**](./procedural.md): these are How To docs, the HOW information, with step-by-step instructions for accomplishing a task. This is what most people are looking for when they open the docs... and best practice is to separate the procedural docs from long, lengthy conceptual or reference docs. - [**Procedural**](./procedural.md): these are How To docs, the HOW information, with step-by-step instructions for accomplishing a task. This is what most people are looking for when they open the docs... and best practice is to separate the procedural docs from long, lengthy conceptual or reference docs.
- [**Conceptual**](./conceptual.md): these docs provide the WHY information, and explain when to use a feature (or when not to!), and general concepts
- [**Conceptual**](./conceptual.md): these docs provide the WHY information, and explain when to use a feature (or when not to!), and general concepts behind the feature or functionality.
- **Reference**: this is typically tables or lists of reference information, such as configuration values, or functions, or most commmonly APIs. - **Reference**: this is typically tables or lists of reference information, such as configuration values, or functions, or most commmonly APIs.
We have templates for the different types, to make it super-easy for whomever wants to contribute some documentation! We have templates for the different types, to make it super-easy for whomever wants to contribute some documentation!