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.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.tasks import apply_blueprint, blueprints_find_dict
from authentik.core.api.used_by import UsedByMixin
@ -49,7 +49,7 @@ class BlueprintInstanceSerializer(ModelSerializer):
if content == "":
return content
context = self.instance.context if self.instance else {}
valid, logs = Importer(content, context).validate()
valid, logs = StringImporter(content, context).validate()
if not valid:
text_logs = "\n".join([x["event"] for x in 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 authentik.blueprints.models import BlueprintInstance
from authentik.blueprints.v1.importer import Importer
from authentik.blueprints.v1.importer import StringImporter
LOGGER = get_logger()
@ -18,7 +18,7 @@ class Command(BaseCommand):
"""Apply all blueprints in order, abort when one fails to import"""
for blueprint_path in options.get("blueprints", []):
content = BlueprintInstance(path=blueprint_path).retrieve()
importer = Importer(content)
importer = StringImporter(content)
valid, _ = importer.validate()
if not valid:
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 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.meta.registry import BaseMetaModel, registry
from authentik.lib.models import SerializerModel
@ -110,7 +111,7 @@ class Command(BaseCommand):
"id": {"type": "string"},
"state": {
"type": "string",
"enum": ["absent", "present", "created"],
"enum": [s.value for s in BlueprintEntryDesiredState],
"default": "present",
},
"conditions": {"type": "array", "items": {"type": "boolean"}},

View File

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

View File

@ -6,7 +6,7 @@ from django.test import TransactionTestCase
from authentik.blueprints.models import BlueprintInstance
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
@ -25,7 +25,7 @@ def blueprint_tester(file_name: Path) -> Callable:
def tester(self: TestPackaged):
base = Path("blueprints/")
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.apply())

View File

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

View File

@ -1,7 +1,7 @@
"""Test blueprints v1"""
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.tests.utils import create_test_admin_user
from authentik.flows.models import Flow
@ -18,7 +18,7 @@ class TestBlueprintsV1ConditionalFields(TransactionTestCase):
self.uid = generate_id()
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.apply())

View File

@ -1,7 +1,7 @@
"""Test blueprints v1"""
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.lib.generators import generate_id
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
)
importer = Importer(import_yaml)
importer = StringImporter(import_yaml)
self.assertTrue(importer.validate()[0])
self.assertTrue(importer.apply())
# Ensure objects exist
@ -35,7 +35,7 @@ class TestBlueprintsV1Conditions(TransactionTestCase):
"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.apply())
# Ensure objects do not exist

View File

@ -1,7 +1,7 @@
"""Test blueprints v1"""
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.lib.generators import generate_id
from authentik.lib.tests.utils import load_fixture
@ -15,7 +15,7 @@ class TestBlueprintsV1State(TransactionTestCase):
flow_slug = generate_id()
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.apply())
# Ensure object exists
@ -30,7 +30,7 @@ class TestBlueprintsV1State(TransactionTestCase):
self.assertEqual(flow.title, "bar")
# Ensure importer updates it
importer = Importer(import_yaml)
importer = StringImporter(import_yaml)
self.assertTrue(importer.validate()[0])
self.assertTrue(importer.apply())
flow: Flow = Flow.objects.filter(slug=flow_slug).first()
@ -41,7 +41,7 @@ class TestBlueprintsV1State(TransactionTestCase):
flow_slug = generate_id()
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.apply())
# Ensure object exists
@ -56,7 +56,7 @@ class TestBlueprintsV1State(TransactionTestCase):
self.assertEqual(flow.title, "bar")
# Ensure importer doesn't update it
importer = Importer(import_yaml)
importer = StringImporter(import_yaml)
self.assertTrue(importer.validate()[0])
self.assertTrue(importer.apply())
flow: Flow = Flow.objects.filter(slug=flow_slug).first()
@ -67,7 +67,7 @@ class TestBlueprintsV1State(TransactionTestCase):
flow_slug = generate_id()
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.apply())
# Ensure object exists
@ -75,7 +75,7 @@ class TestBlueprintsV1State(TransactionTestCase):
self.assertEqual(flow.slug, 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.apply())
flow: Flow = Flow.objects.filter(slug=flow_slug).first()

View File

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

View File

@ -86,27 +86,22 @@ class Importer:
"""Import Blueprint from YAML"""
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._import = blueprint
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 = {}
always_merger.merge(ctx, self.__import.context)
always_merger.merge(ctx, self._import.context)
if context:
always_merger.merge(ctx, context)
self.__import.context = ctx
self._import.context = ctx
@property
def blueprint(self) -> Blueprint:
"""Get imported blueprint"""
return self.__import
return self._import
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"""
@ -152,11 +147,11 @@ class Importer:
# pylint: disable-msg=too-many-locals
def _validate_single(self, entry: BlueprintEntry) -> Optional[BaseSerializer]:
"""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")
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)
# Don't use isinstance since we don't want to check for inheritance
if not is_model_allowed(model):
@ -164,7 +159,7 @@ class Importer:
if issubclass(model, BaseMetaModel):
serializer_class: type[Serializer] = model.serializer()
serializer = serializer_class(
data=entry.get_attrs(self.__import),
data=entry.get_attrs(self._import),
context={
SERIALIZER_CONTEXT_BLUEPRINT: entry,
},
@ -182,7 +177,7 @@ class Importer:
# the full serializer for later usage
# Because a model might have multiple unique columns, we chain all identifiers together
# 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()):
if isinstance(value, dict) and "pk" in value:
del updated_identifiers[key]
@ -208,6 +203,13 @@ class Importer:
)
serializer_kwargs["instance"] = model_instance
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:
self.logger.debug(
"initialised new serializer instance",
@ -220,7 +222,7 @@ class Importer:
model_instance.pk = updated_identifiers["pk"]
serializer_kwargs["instance"] = model_instance
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:
raise EntryInvalidError(exc) from exc
always_merger.merge(full_data, updated_identifiers)
@ -255,8 +257,8 @@ class Importer:
def _apply_models(self) -> bool:
"""Apply (create/update) models yaml"""
self.__pk_map = {}
for entry in self.__import.entries:
model_app_label, model_name = entry.get_model(self.__import).split(".")
for entry in self._import.entries:
model_app_label, model_name = entry.get_model(self._import).split(".")
try:
model: type[SerializerModel] = registry.get_model(model_app_label, model_name)
except LookupError:
@ -269,15 +271,19 @@ class Importer:
serializer = self._validate_single(entry)
except EntryInvalidError as exc:
# 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
self.logger.warning(f"entry invalid: {exc}", entry=entry, error=exc)
return False
if not serializer:
continue
state = entry.get_state(self.__import)
if state in [BlueprintEntryDesiredState.PRESENT, BlueprintEntryDesiredState.CREATED]:
state = entry.get_state(self._import)
if state in [
BlueprintEntryDesiredState.PRESENT,
BlueprintEntryDesiredState.CREATED,
BlueprintEntryDesiredState.MUST_CREATED,
]:
instance = serializer.instance
if (
instance
@ -309,8 +315,8 @@ class Importer:
"""Validate loaded blueprint export, ensure all models are allowed
and serializers have no errors"""
self.logger.debug("Starting blueprint import validation")
orig_import = deepcopy(self.__import)
if self.__import.version != 1:
orig_import = deepcopy(self._import)
if self._import.version != 1:
self.logger.warning("Invalid blueprint version")
return False, [{"event": "Invalid blueprint version"}]
with (
@ -323,5 +329,19 @@ class Importer:
for log in logs:
getattr(self.logger, log.get("log_level"))(**log)
self.logger.debug("Finished blueprint import validation")
self.__import = orig_import
self._import = orig_import
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,
)
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.oci import OCI_PREFIX
from authentik.events.monitored_tasks import (
@ -190,7 +190,7 @@ def apply_blueprint(self: MonitoredTask, instance_pk: str):
self.set_uid(slugify(instance.name))
blueprint_content = instance.retrieve()
file_hash = sha512(blueprint_content.encode()).hexdigest()
importer = Importer(blueprint_content, instance.context)
importer = StringImporter(blueprint_content, instance.context)
if importer.blueprint.metadata:
instance.metadata = asdict(importer.blueprint.metadata)
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.sources import SourceViewSet, UserSourceConnectionViewSet
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.views import apps
from authentik.core.views.debug import AccessDeniedView
@ -70,6 +71,11 @@ urlpatterns = [
api_urlpatterns = [
("core/authenticated_sessions", AuthenticatedSessionViewSet),
("core/applications", ApplicationViewSet),
path(
"core/transactional/applications/",
TransactionalApplicationView.as_view(),
name="core-transactional-application",
),
("core/groups", GroupViewSet),
("core/users", UserViewSet),
("core/tokens", TokenViewSet),

View File

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

View File

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

View File

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

View File

@ -4349,6 +4349,39 @@ paths:
schema:
$ref: '#/components/schemas/GenericError'
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/:
get:
operationId: core_user_consent_list
@ -37518,6 +37551,22 @@ components:
description: |-
* `twilio` - Twilio
* `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:
type: object
description: Provider Serializer
@ -39929,6 +39978,36 @@ components:
readOnly: true
required:
- 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:
type: object
description: Types of an object that can be created
@ -40856,6 +40935,23 @@ components:
type: integer
required:
- 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:
authentik:
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:
- [**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 behind the feature or functionality.
- [**Conceptual**](./conceptual.md): these docs provide the WHY information, and explain when to use a feature (or when not to!), and general concepts
- **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!