Compare commits

...
This repository has been archived on 2024-05-31. You can view files and clone it, but cannot push or open issues or pull requests.

12 Commits

Author SHA1 Message Date
Jens Langhammer a331affd42
actually make API work
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-08-07 21:31:37 +02:00
Jens Langhammer 9d3bd8418d
remove applications api
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-08-07 20:34:09 +02:00
Jens Langhammer 9ee77993a9
base parser off of yaml parser
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-08-07 20:31:39 +02:00
Jens Langhammer 42e2eb1529
rename importer once again to make room for a JSON importer
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-08-07 20:31:39 +02:00
Jens Langhammer e2d18f6011
start json parser
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-08-07 20:31:39 +02:00
Jens Langhammer d811aabd38
blueprints: initial
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-08-07 20:31:39 +02:00
Jens Langhammer c592599633
root: fix baseAPI path having trailing slash
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-08-07 20:31:39 +02:00
Jens Langhammer 8b13da354f
stages/invitation: fix incorrect serializer
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-08-07 20:31:39 +02:00
Jens Langhammer 79175266cc
add new "must_created" state to blueprints to prevent overwriting objects
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-08-07 20:31:39 +02:00
Jens Langhammer 629af26742
cleanup
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-08-07 20:31:39 +02:00
Jens Langhammer edc7f2fdb0
separate blueprint importer from yaml parsing
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-08-07 20:31:39 +02:00
Jens Langhammer a95c33f1ca
initial api and schema
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-08-07 20:31:39 +02:00
22 changed files with 626 additions and 313 deletions

View File

@ -31,5 +31,5 @@ class AuthentikAPIConfig(AppConfig):
"type": "apiKey", "type": "apiKey",
"in": "header", "in": "header",
"name": "Authorization", "name": "Authorization",
"scheme": "bearer", "scheme": "Bearer",
} }

View File

@ -1,9 +1,10 @@
"""Serializer mixin for managed models""" """Serializer mixin for managed models"""
from django.apps import apps
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from drf_spectacular.utils import extend_schema, inline_serializer from drf_spectacular.utils import OpenApiParameter, extend_schema, inline_serializer
from rest_framework.decorators import action from rest_framework.decorators import action
from rest_framework.exceptions import ValidationError from rest_framework.exceptions import ValidationError
from rest_framework.fields import CharField, DateTimeField, JSONField from rest_framework.fields import BooleanField, CharField, DateTimeField, DictField, JSONField
from rest_framework.permissions import IsAdminUser from rest_framework.permissions import IsAdminUser
from rest_framework.request import Request from rest_framework.request import Request
from rest_framework.response import Response from rest_framework.response import Response
@ -12,7 +13,9 @@ 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.common import Blueprint, BlueprintEntry, BlueprintEntryDesiredState
from authentik.blueprints.v1.importer import Importer, YAMLStringImporter, is_model_allowed
from authentik.blueprints.v1.json_parser import BlueprintJSONParser
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 +52,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 = YAMLStringImporter(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}))
@ -84,6 +87,43 @@ class BlueprintInstanceSerializer(ModelSerializer):
} }
class BlueprintEntrySerializer(PassiveSerializer):
"""Validate a single blueprint entry, similar to a subset of regular blueprints"""
model = CharField()
id = CharField(required=False, allow_blank=True)
identifiers = DictField()
attrs = DictField()
def validate_model(self, fq_model: str) -> str:
"""Validate model is allowed"""
if "." not in fq_model:
raise ValidationError("Invalid model")
app, model_name = fq_model.split(".")
try:
model = apps.get_model(app, model_name)
if not is_model_allowed(model):
raise ValidationError("Invalid model")
except LookupError:
raise ValidationError("Invalid model")
return fq_model
class BlueprintSerializer(PassiveSerializer):
"""Validate a procedural blueprint, which is a subset of a regular blueprint"""
entries = ListSerializer(child=BlueprintEntrySerializer())
context = DictField(required=False)
class BlueprintProceduralResultSerializer(PassiveSerializer):
"""Result of applying a procedural blueprint"""
valid = BooleanField()
applied = BooleanField()
logs = ListSerializer(child=CharField())
class BlueprintInstanceViewSet(UsedByMixin, ModelViewSet): class BlueprintInstanceViewSet(UsedByMixin, ModelViewSet):
"""Blueprint instances""" """Blueprint instances"""
@ -127,3 +167,55 @@ class BlueprintInstanceViewSet(UsedByMixin, ModelViewSet):
blueprint = self.get_object() blueprint = self.get_object()
apply_blueprint.delay(str(blueprint.pk)).get() apply_blueprint.delay(str(blueprint.pk)).get()
return self.retrieve(request, *args, **kwargs) return self.retrieve(request, *args, **kwargs)
@extend_schema(
request=BlueprintSerializer,
responses=BlueprintProceduralResultSerializer,
parameters=[
OpenApiParameter("validate_only", bool),
],
)
@action(
detail=False,
pagination_class=None,
filter_backends=[],
methods=["PUT"],
permission_classes=[IsAdminUser],
parser_classes=[BlueprintJSONParser],
)
def procedural(self, request: Request) -> Response:
"""Run a client-provided blueprint once, as-is. Blueprint is not kept in memory/database
and will not be continuously applied"""
blueprint = Blueprint()
data = BlueprintSerializer(data=request.data)
data.is_valid(raise_exception=True)
blueprint.context = data.validated_data.get("context", {})
for raw_entry in data.validated_data["entries"]:
entry = BlueprintEntrySerializer(data=raw_entry)
entry.is_valid(raise_exception=True)
blueprint.entries.append(
BlueprintEntry(
model=entry.data["model"],
state=BlueprintEntryDesiredState.MUST_CREATED,
identifiers=entry.data["identifiers"],
attrs=entry.data["attrs"],
id=entry.data.get("id", None),
)
)
importer = Importer(blueprint)
valid, logs = importer.validate()
result = {
"valid": valid,
"applied": False,
# TODO: Better way to handle logs
"logs": [x["event"] for x in logs],
}
response = BlueprintProceduralResultSerializer(data=result)
response.is_valid()
if request.query_params.get("validate_only", False):
return Response(response.validated_data)
applied = importer.apply()
result["applied"] = applied
response = BlueprintProceduralResultSerializer(data=result)
response.is_valid()
return Response(response.validated_data)

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 YAMLStringImporter
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 = YAMLStringImporter(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 YAMLStringImporter
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() YAMLStringImporter(content).apply()
return func(*args, **kwargs) return func(*args, **kwargs)
return wrapper return wrapper

View File

@ -0,0 +1,44 @@
{
"$schema": "https://goauthentik.io/blueprints/schema.json",
"version": 1,
"metadata": {
"name": "test-json"
},
"entries": [
{
"model": "authentik_providers_oauth2.oauth2provider",
"id": "provider",
"identifiers": {
"name": "grafana-json"
},
"attrs": {
"authorization_flow": {
"goauthentik.io/yaml-key": "!Find",
"args": [
"authentik_flows.flow",
[
"pk",
{
"goauthentik.io/yaml-key": "!Context",
"args": "flow"
}
]
]
}
}
},
{
"model": "authentik_core.application",
"identifiers": {
"slug": "test-json"
},
"attrs": {
"name": "test-json",
"provider": {
"goauthentik.io/yaml-key": "!KeyOf",
"args": "provider"
}
}
}
]
}

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 YAMLStringImporter
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 = YAMLStringImporter(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 YAMLStringImporter, 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 = YAMLStringImporter('{"version": 3}')
self.assertFalse(importer.validate()[0]) self.assertFalse(importer.validate()[0])
importer = Importer( importer = YAMLStringImporter(
'{"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 = YAMLStringImporter(
'{"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 = YAMLStringImporter(
'{"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 = YAMLStringImporter(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 = YAMLStringImporter(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 = YAMLStringImporter(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 = YAMLStringImporter(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 = YAMLStringImporter(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 = YAMLStringImporter(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 YAMLStringImporter
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 = YAMLStringImporter(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 YAMLStringImporter
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 = YAMLStringImporter(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 = YAMLStringImporter(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

@ -0,0 +1,22 @@
"""Test blueprints v1 JSON"""
from django.test import TransactionTestCase
from authentik.blueprints.v1.importer import JSONStringImporter
from authentik.core.tests.utils import create_test_flow
from authentik.lib.tests.utils import load_fixture
class TestBlueprintsV1JSON(TransactionTestCase):
"""Test Blueprints"""
def test_import(self):
"""Test JSON Import"""
test_flow = create_test_flow()
importer = JSONStringImporter(
load_fixture("fixtures/test.json"),
{
"flow": str(test_flow.pk),
},
)
self.assertTrue(importer.validate()[0])
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 YAMLStringImporter
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 = YAMLStringImporter(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 = YAMLStringImporter(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 = YAMLStringImporter(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 = YAMLStringImporter(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 = YAMLStringImporter(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 = YAMLStringImporter(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
@ -554,21 +555,30 @@ class BlueprintDumper(SafeDumper):
return super().represent(data) return super().represent(data)
def yaml_key_map() -> dict[str, type[YAMLTag]]:
"""get a dict of all yaml tags, key being the actual tag
and the value is the class"""
return {
"!KeyOf": KeyOf,
"!Find": Find,
"!Context": Context,
"!Format": Format,
"!Condition": Condition,
"!If": If,
"!Env": Env,
"!Enumerate": Enumerate,
"!Value": Value,
"!Index": Index,
}
class BlueprintLoader(SafeLoader): class BlueprintLoader(SafeLoader):
"""Loader for blueprints with custom tag support""" """Loader for blueprints with custom tag support"""
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.add_constructor("!KeyOf", KeyOf) for tag, cls in yaml_key_map().items():
self.add_constructor("!Find", Find) self.add_constructor(tag, cls)
self.add_constructor("!Context", Context)
self.add_constructor("!Format", Format)
self.add_constructor("!Condition", Condition)
self.add_constructor("!If", If)
self.add_constructor("!Env", Env)
self.add_constructor("!Enumerate", Enumerate)
self.add_constructor("!Value", Value)
self.add_constructor("!Index", Index)
class EntryInvalidError(SentryIgnoredException): class EntryInvalidError(SentryIgnoredException):

View File

@ -27,6 +27,7 @@ from authentik.blueprints.v1.common import (
BlueprintLoader, BlueprintLoader,
EntryInvalidError, EntryInvalidError,
) )
from authentik.blueprints.v1.json_parser import BlueprintJSONDecoder
from authentik.blueprints.v1.meta.registry import BaseMetaModel, registry from authentik.blueprints.v1.meta.registry import BaseMetaModel, registry
from authentik.core.models import ( from authentik.core.models import (
AuthenticatedSession, AuthenticatedSession,
@ -85,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"""
@ -151,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):
@ -163,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,
}, },
@ -181,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]
@ -207,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", model=model, **updated_identifiers "initialised new serializer instance", model=model, **updated_identifiers
@ -217,7 +220,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)
@ -239,6 +242,7 @@ class Importer:
def apply(self) -> bool: def apply(self) -> bool:
"""Apply (create/update) models yaml, in database transaction""" """Apply (create/update) models yaml, in database transaction"""
self.logger.debug("Starting blueprint import")
try: try:
with transaction.atomic(): with transaction.atomic():
if not self._apply_models(): if not self._apply_models():
@ -252,8 +256,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:
@ -266,15 +270,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
@ -306,8 +314,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 (
@ -320,5 +328,33 @@ 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 YAMLStringImporter(Importer):
"""Importer that also parses from YAML 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)
class JSONStringImporter(Importer):
"""Importer that also parses from JSON string"""
def __init__(self, json_import: str, context: dict | None = None):
import_dict = load(json_import, BlueprintJSONDecoder)
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

@ -0,0 +1,77 @@
"""Blueprint JSON decoder"""
import codecs
from collections.abc import Hashable
from typing import Any
from django.conf import settings
from rest_framework.exceptions import ParseError
from rest_framework.parsers import JSONParser
from yaml import load
from yaml.nodes import MappingNode
from authentik.blueprints.v1.common import BlueprintLoader, YAMLTag, yaml_key_map
TAG_KEY = "goauthentik.io/yaml-key"
ARGS_KEY = "args"
class BlueprintJSONDecoder(BlueprintLoader):
"""Blueprint JSON decoder, allows using tag logic when using JSON data (e.g. through the API,
when YAML tags are not available).
This is still based on a YAML Loader, since all the YAML Tag constructors expect *Node objects
from YAML, this makes things a lot easier."""
tag_map: dict[str, type[YAMLTag]]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.tag_map = yaml_key_map()
self.add_constructor("tag:yaml.org,2002:map", BlueprintJSONDecoder.construct_yaml_map)
def construct_yaml_map(self, node):
"""The original construct_yaml_map creates a dict, yields it, then updates it,
which is probably some sort of performance optimisation, however it breaks here
when we don't return a dict from the `construct_mapping` function"""
value = self.construct_mapping(node)
yield value
def construct_mapping(self, node: MappingNode, deep: bool = False) -> dict[Hashable, Any]:
"""Check if the mapping has a special key and create an in-place YAML tag for it,
and return that instead of the actual dict"""
parsed = super().construct_mapping(node, deep=deep)
if TAG_KEY not in parsed:
return parsed
tag_cls = self.parse_yaml_tag(parsed)
if not tag_cls:
return parsed
# MappingNode's value is a list of tuples where the tuples
# consist of (KeyNode, ValueNode)
# so this filters out the value node for `args`
raw_args_pair = [x for x in node.value if x[0].value == ARGS_KEY]
if len(raw_args_pair) < 1:
return parsed
# Get the value of the first Node in the pair we get from above
# where the value isn't `args`, i.e. the actual argument data
raw_args_data = [x for x in raw_args_pair[0] if x.value != ARGS_KEY][0]
return tag_cls(self, raw_args_data)
def parse_yaml_tag(self, data: dict) -> YAMLTag | None:
"""parse the tag"""
yaml_tag = data.get(TAG_KEY)
tag_cls = self.tag_map.get(yaml_tag)
if not tag_cls:
return None
return tag_cls
class BlueprintJSONParser(JSONParser):
"""Wrapper around the rest_framework JSON parser that uses the `BlueprintJSONDecoder`"""
def parse(self, stream, media_type=None, parser_context=None):
encoding = parser_context.get("encoding", settings.DEFAULT_CHARSET)
try:
decoded_stream = codecs.getreader(encoding)(stream)
return load(decoded_stream, BlueprintJSONDecoder)
except ValueError as exc:
raise ParseError("JSON parse error") from exc

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 YAMLStringImporter
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 = YAMLStringImporter(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

@ -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, YAMLStringImporter
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 = YAMLStringImporter(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."""
mapping = {}
class Inner(dict): classes = all_subclasses(Challenge)
"""dummy class with custom callback on .items()""" classes.remove(WithUserInfoChallenge)
for cls in classes:
def items(self): mapping[cls().fields["component"].default] = cls
mapping = {} return mapping
classes = all_subclasses(Challenge)
classes.remove(WithUserInfoChallenge)
for cls in classes:
mapping[cls().fields["component"].default] = cls
return mapping.items()
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."""
mapping = {}
class Inner(dict): classes = all_subclasses(ChallengeResponse)
"""dummy class with custom callback on .items()""" for cls in classes:
mapping[cls(stage=None).fields["component"].default] = cls
def items(self): return mapping
mapping = {}
classes = all_subclasses(ChallengeResponse)
for cls in classes:
mapping[cls(stage=None).fields["component"].default] = cls
return mapping.items()
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

@ -125,7 +125,7 @@ SPECTACULAR_SETTINGS = {
"SCHEMA_PATH_PREFIX_TRIM": True, "SCHEMA_PATH_PREFIX_TRIM": True,
"SERVERS": [ "SERVERS": [
{ {
"url": "/api/v3/", "url": "/api/v3",
}, },
], ],
"CONTACT": { "CONTACT": {

View File

@ -73,9 +73,9 @@ class Invitation(SerializerModel, ExpiringModel):
@property @property
def serializer(self) -> Serializer: def serializer(self) -> Serializer:
from authentik.stages.consent.api import UserConsentSerializer from authentik.stages.invitation.api import InvitationSerializer
return UserConsentSerializer return InvitationSerializer
def __str__(self): def __str__(self):
return f"Invitation {self.invite_uuid.hex} created by {self.created_by}" return f"Invitation {self.invite_uuid.hex} created by {self.created_by}"

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"
}, },
@ -7242,146 +7313,32 @@
"model_authentik_stages_invitation.invitation": { "model_authentik_stages_invitation.invitation": {
"type": "object", "type": "object",
"properties": { "properties": {
"name": {
"type": "string",
"maxLength": 50,
"minLength": 1,
"pattern": "^[-a-zA-Z0-9_]+$",
"title": "Name"
},
"expires": { "expires": {
"type": "string", "type": "string",
"format": "date-time", "format": "date-time",
"title": "Expires" "title": "Expires"
}, },
"user": { "fixed_data": {
"type": "object", "type": "object",
"properties": { "additionalProperties": true,
"username": { "title": "Fixed data"
"type": "string",
"maxLength": 150,
"minLength": 1,
"title": "Username"
},
"name": {
"type": "string",
"title": "Name",
"description": "User's display name."
},
"is_active": {
"type": "boolean",
"title": "Active",
"description": "Designates whether this user should be treated as active. Unselect this instead of deleting accounts."
},
"last_login": {
"type": [
"string",
"null"
],
"format": "date-time",
"title": "Last login"
},
"groups": {
"type": "array",
"items": {
"type": "integer"
},
"title": "Groups"
},
"email": {
"type": "string",
"format": "email",
"maxLength": 254,
"title": "Email address"
},
"attributes": {
"type": "object",
"additionalProperties": true,
"title": "Attributes"
},
"path": {
"type": "string",
"minLength": 1,
"title": "Path"
},
"type": {
"type": "string",
"enum": [
"internal",
"external",
"service_account",
"internal_service_account"
],
"title": "Type"
}
},
"required": [
"username",
"name"
],
"title": "User"
}, },
"application": { "single_use": {
"type": "object", "type": "boolean",
"properties": { "title": "Single use",
"name": { "description": "When enabled, the invitation will be deleted after usage."
"type": "string",
"minLength": 1,
"title": "Name",
"description": "Application's display Name."
},
"slug": {
"type": "string",
"maxLength": 50,
"minLength": 1,
"pattern": "^[-a-zA-Z0-9_]+$",
"title": "Slug",
"description": "Internal application name, used in URLs."
},
"provider": {
"type": "integer",
"title": "Provider"
},
"backchannel_providers": {
"type": "array",
"items": {
"type": "integer"
},
"title": "Backchannel providers"
},
"open_in_new_tab": {
"type": "boolean",
"title": "Open in new tab",
"description": "Open launch URL in a new browser tab or window."
},
"meta_launch_url": {
"type": "string",
"title": "Meta launch url"
},
"meta_description": {
"type": "string",
"title": "Meta description"
},
"meta_publisher": {
"type": "string",
"title": "Meta publisher"
},
"policy_engine_mode": {
"type": "string",
"enum": [
"all",
"any"
],
"title": "Policy engine mode"
},
"group": {
"type": "string",
"title": "Group"
}
},
"required": [
"name",
"slug"
],
"title": "Application"
}, },
"permissions": { "flow": {
"type": "string", "type": "integer",
"minLength": 1, "title": "Flow",
"title": "Permissions" "description": "When set, only the configured flow can use this invitation."
} }
}, },
"required": [] "required": []

View File

@ -8480,6 +8480,46 @@ paths:
schema: schema:
$ref: '#/components/schemas/GenericError' $ref: '#/components/schemas/GenericError'
description: '' description: ''
/managed/blueprints/procedural/:
put:
operationId: managed_blueprints_procedural_update
description: |-
Run a client-provided blueprint once, as-is. Blueprint is not kept in memory/database
and will not be continuously applied
parameters:
- in: query
name: validate_only
schema:
type: boolean
tags:
- managed
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/BlueprintRequest'
required: true
security:
- authentik: []
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/BlueprintProceduralResult'
description: ''
'400':
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
description: ''
'403':
content:
application/json:
schema:
$ref: '#/components/schemas/GenericError'
description: ''
/oauth2/access_tokens/: /oauth2/access_tokens/:
get: get:
operationId: oauth2_access_tokens_list operationId: oauth2_access_tokens_list
@ -27967,6 +28007,26 @@ components:
* `REDIRECT` - Redirect Binding * `REDIRECT` - Redirect Binding
* `POST` - POST Binding * `POST` - POST Binding
* `POST_AUTO` - POST Binding with auto-confirmation * `POST_AUTO` - POST Binding with auto-confirmation
BlueprintEntryRequest:
type: object
description: Validate a single blueprint entry, similar to a subset of regular
blueprints
properties:
model:
type: string
minLength: 1
id:
type: string
identifiers:
type: object
additionalProperties: {}
attrs:
type: object
additionalProperties: {}
required:
- attrs
- identifiers
- model
BlueprintFile: BlueprintFile:
type: object type: object
properties: properties:
@ -28068,6 +28128,36 @@ components:
* `error` - Error * `error` - Error
* `orphaned` - Orphaned * `orphaned` - Orphaned
* `unknown` - Unknown * `unknown` - Unknown
BlueprintProceduralResult:
type: object
description: Result of applying a procedural blueprint
properties:
valid:
type: boolean
applied:
type: boolean
logs:
type: array
items:
type: string
required:
- applied
- logs
- valid
BlueprintRequest:
type: object
description: Validate a procedural blueprint, which is a subset of a regular
blueprint
properties:
entries:
type: array
items:
$ref: '#/components/schemas/BlueprintEntryRequest'
context:
type: object
additionalProperties: {}
required:
- entries
Cache: Cache:
type: object type: object
description: Generic cache stats for an object description: Generic cache stats for an object
@ -40845,6 +40935,6 @@ components:
type: apiKey type: apiKey
in: header in: header
name: Authorization name: Authorization
scheme: bearer scheme: Bearer
servers: servers:
- url: /api/v3/ - url: /api/v3