Compare commits
12 commits
trustchain
...
blueprints
Author | SHA1 | Date | |
---|---|---|---|
a331affd42 | |||
9d3bd8418d | |||
9ee77993a9 | |||
42e2eb1529 | |||
e2d18f6011 | |||
d811aabd38 | |||
c592599633 | |||
8b13da354f | |||
79175266cc | |||
629af26742 | |||
edc7f2fdb0 | |||
a95c33f1ca |
|
@ -31,5 +31,5 @@ class AuthentikAPIConfig(AppConfig):
|
||||||
"type": "apiKey",
|
"type": "apiKey",
|
||||||
"in": "header",
|
"in": "header",
|
||||||
"name": "Authorization",
|
"name": "Authorization",
|
||||||
"scheme": "bearer",
|
"scheme": "Bearer",
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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"}},
|
||||||
|
|
|
@ -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
|
||||||
|
|
44
authentik/blueprints/tests/fixtures/test.json
vendored
Normal file
44
authentik/blueprints/tests/fixtures/test.json
vendored
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -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())
|
||||||
|
|
||||||
|
|
|
@ -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())
|
||||||
|
|
|
@ -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())
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
22
authentik/blueprints/tests/test_v1_json.py
Normal file
22
authentik/blueprints/tests/test_v1_json.py
Normal 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())
|
|
@ -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()
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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)
|
||||||
|
|
77
authentik/blueprints/v1/json_parser.py
Normal file
77
authentik/blueprints/v1/json_parser.py
Normal 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
|
|
@ -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()
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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=[
|
||||||
|
|
|
@ -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": {
|
||||||
|
|
|
@ -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}"
|
||||||
|
|
|
@ -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": []
|
||||||
|
|
94
schema.yml
94
schema.yml
|
@ -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
|
||||||
|
|
Reference in a new issue