diff --git a/authentik/blueprints/api.py b/authentik/blueprints/api.py index 09f8ee6dd..c38ccbe40 100644 --- a/authentik/blueprints/api.py +++ b/authentik/blueprints/api.py @@ -14,7 +14,7 @@ from rest_framework.viewsets import ModelViewSet from authentik.api.decorators import permission_required from authentik.blueprints.models import BlueprintInstance from authentik.blueprints.v1.common import Blueprint, BlueprintEntry, BlueprintEntryDesiredState -from authentik.blueprints.v1.importer import StringImporter, is_model_allowed +from authentik.blueprints.v1.importer import YAMLStringImporter, is_model_allowed from authentik.blueprints.v1.json_parser import BlueprintJSONParser from authentik.blueprints.v1.oci import OCI_PREFIX from authentik.blueprints.v1.tasks import apply_blueprint, blueprints_find_dict @@ -52,7 +52,7 @@ class BlueprintInstanceSerializer(ModelSerializer): if content == "": return content context = self.instance.context if self.instance else {} - valid, logs = StringImporter(content, context).validate() + valid, logs = YAMLStringImporter(content, context).validate() if not valid: text_logs = "\n".join([x["event"] for x in logs]) raise ValidationError(_("Failed to validate blueprint: %(logs)s" % {"logs": text_logs})) diff --git a/authentik/blueprints/management/commands/apply_blueprint.py b/authentik/blueprints/management/commands/apply_blueprint.py index 427c13bb6..daa5e1415 100644 --- a/authentik/blueprints/management/commands/apply_blueprint.py +++ b/authentik/blueprints/management/commands/apply_blueprint.py @@ -5,7 +5,7 @@ from django.core.management.base import BaseCommand, no_translations from structlog.stdlib import get_logger from authentik.blueprints.models import BlueprintInstance -from authentik.blueprints.v1.importer import StringImporter +from authentik.blueprints.v1.importer import YAMLStringImporter LOGGER = get_logger() @@ -18,7 +18,7 @@ class Command(BaseCommand): """Apply all blueprints in order, abort when one fails to import""" for blueprint_path in options.get("blueprints", []): content = BlueprintInstance(path=blueprint_path).retrieve() - importer = StringImporter(content) + importer = YAMLStringImporter(content) valid, _ = importer.validate() if not valid: self.stderr.write("blueprint invalid") diff --git a/authentik/blueprints/tests/__init__.py b/authentik/blueprints/tests/__init__.py index 06a3d04bf..de139d903 100644 --- a/authentik/blueprints/tests/__init__.py +++ b/authentik/blueprints/tests/__init__.py @@ -11,7 +11,7 @@ from authentik.blueprints.models import BlueprintInstance def apply_blueprint(*files: str): """Apply blueprint before test""" - from authentik.blueprints.v1.importer import StringImporter + from authentik.blueprints.v1.importer import YAMLStringImporter def wrapper_outer(func: Callable): """Apply blueprint before test""" @@ -20,7 +20,7 @@ def apply_blueprint(*files: str): def wrapper(*args, **kwargs): for file in files: content = BlueprintInstance(path=file).retrieve() - StringImporter(content).apply() + YAMLStringImporter(content).apply() return func(*args, **kwargs) return wrapper diff --git a/authentik/blueprints/tests/test_packaged.py b/authentik/blueprints/tests/test_packaged.py index 5edf7afc3..41dc432f3 100644 --- a/authentik/blueprints/tests/test_packaged.py +++ b/authentik/blueprints/tests/test_packaged.py @@ -6,7 +6,7 @@ from django.test import TransactionTestCase from authentik.blueprints.models import BlueprintInstance from authentik.blueprints.tests import apply_blueprint -from authentik.blueprints.v1.importer import StringImporter +from authentik.blueprints.v1.importer import YAMLStringImporter from authentik.tenants.models import Tenant @@ -25,7 +25,7 @@ def blueprint_tester(file_name: Path) -> Callable: def tester(self: TestPackaged): base = Path("blueprints/") rel_path = Path(file_name).relative_to(base) - importer = StringImporter(BlueprintInstance(path=str(rel_path)).retrieve()) + importer = YAMLStringImporter(BlueprintInstance(path=str(rel_path)).retrieve()) self.assertTrue(importer.validate()[0]) self.assertTrue(importer.apply()) diff --git a/authentik/blueprints/tests/test_v1.py b/authentik/blueprints/tests/test_v1.py index ae10bb66a..5bd3651a7 100644 --- a/authentik/blueprints/tests/test_v1.py +++ b/authentik/blueprints/tests/test_v1.py @@ -4,7 +4,7 @@ from os import environ from django.test import TransactionTestCase from authentik.blueprints.v1.exporter import FlowExporter -from authentik.blueprints.v1.importer import StringImporter, transaction_rollback +from authentik.blueprints.v1.importer import YAMLStringImporter, transaction_rollback from authentik.core.models import Group from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding from authentik.lib.generators import generate_id @@ -21,14 +21,14 @@ class TestBlueprintsV1(TransactionTestCase): def test_blueprint_invalid_format(self): """Test blueprint with invalid format""" - importer = StringImporter('{"version": 3}') + importer = YAMLStringImporter('{"version": 3}') self.assertFalse(importer.validate()[0]) - importer = StringImporter( + importer = YAMLStringImporter( '{"version": 1,"entries":[{"identifiers":{},"attrs":{},' '"model": "authentik_core.User"}]}' ) self.assertFalse(importer.validate()[0]) - importer = StringImporter( + importer = YAMLStringImporter( '{"version": 1, "entries": [{"attrs": {"name": "test"}, ' '"identifiers": {}, ' '"model": "authentik_core.Group"}]}' @@ -54,7 +54,7 @@ class TestBlueprintsV1(TransactionTestCase): }, ) - importer = StringImporter( + importer = YAMLStringImporter( '{"version": 1, "entries": [{"attrs": {"name": "test999", "attributes": ' '{"key": ["updated_value"]}}, "identifiers": {"attributes": {"other_key": ' '["other_value"]}}, "model": "authentik_core.Group"}]}' @@ -103,7 +103,7 @@ class TestBlueprintsV1(TransactionTestCase): self.assertEqual(len(export.entries), 3) export_yaml = exporter.export_to_string() - importer = StringImporter(export_yaml) + importer = YAMLStringImporter(export_yaml) self.assertTrue(importer.validate()[0]) self.assertTrue(importer.apply()) @@ -113,14 +113,14 @@ class TestBlueprintsV1(TransactionTestCase): """Test export and import it twice""" count_initial = Prompt.objects.filter(field_key="username").count() - importer = StringImporter(load_fixture("fixtures/static_prompt_export.yaml")) + importer = YAMLStringImporter(load_fixture("fixtures/static_prompt_export.yaml")) self.assertTrue(importer.validate()[0]) self.assertTrue(importer.apply()) count_before = Prompt.objects.filter(field_key="username").count() self.assertEqual(count_initial + 1, count_before) - importer = StringImporter(load_fixture("fixtures/static_prompt_export.yaml")) + importer = YAMLStringImporter(load_fixture("fixtures/static_prompt_export.yaml")) self.assertTrue(importer.apply()) self.assertEqual(Prompt.objects.filter(field_key="username").count(), count_before) @@ -130,7 +130,7 @@ class TestBlueprintsV1(TransactionTestCase): ExpressionPolicy.objects.filter(name="foo-bar-baz-qux").delete() Group.objects.filter(name="test").delete() environ["foo"] = generate_id() - importer = StringImporter(load_fixture("fixtures/tags.yaml"), {"bar": "baz"}) + importer = YAMLStringImporter(load_fixture("fixtures/tags.yaml"), {"bar": "baz"}) self.assertTrue(importer.validate()[0]) self.assertTrue(importer.apply()) policy = ExpressionPolicy.objects.filter(name="foo-bar-baz-qux").first() @@ -247,7 +247,7 @@ class TestBlueprintsV1(TransactionTestCase): exporter = FlowExporter(flow) export_yaml = exporter.export_to_string() - importer = StringImporter(export_yaml) + importer = YAMLStringImporter(export_yaml) self.assertTrue(importer.validate()[0]) self.assertTrue(importer.apply()) self.assertTrue(UserLoginStage.objects.filter(name=stage_name).exists()) @@ -296,7 +296,7 @@ class TestBlueprintsV1(TransactionTestCase): exporter = FlowExporter(flow) export_yaml = exporter.export_to_string() - importer = StringImporter(export_yaml) + importer = YAMLStringImporter(export_yaml) self.assertTrue(importer.validate()[0]) self.assertTrue(importer.apply()) diff --git a/authentik/blueprints/tests/test_v1_conditional_fields.py b/authentik/blueprints/tests/test_v1_conditional_fields.py index a8b880157..5f53db453 100644 --- a/authentik/blueprints/tests/test_v1_conditional_fields.py +++ b/authentik/blueprints/tests/test_v1_conditional_fields.py @@ -1,7 +1,7 @@ """Test blueprints v1""" from django.test import TransactionTestCase -from authentik.blueprints.v1.importer import StringImporter +from authentik.blueprints.v1.importer import YAMLStringImporter from authentik.core.models import Application, Token, User from authentik.core.tests.utils import create_test_admin_user from authentik.flows.models import Flow @@ -18,7 +18,7 @@ class TestBlueprintsV1ConditionalFields(TransactionTestCase): self.uid = generate_id() import_yaml = load_fixture("fixtures/conditional_fields.yaml", uid=self.uid, user=user.pk) - importer = StringImporter(import_yaml) + importer = YAMLStringImporter(import_yaml) self.assertTrue(importer.validate()[0]) self.assertTrue(importer.apply()) diff --git a/authentik/blueprints/tests/test_v1_conditions.py b/authentik/blueprints/tests/test_v1_conditions.py index 57217c49e..6a248d3d7 100644 --- a/authentik/blueprints/tests/test_v1_conditions.py +++ b/authentik/blueprints/tests/test_v1_conditions.py @@ -1,7 +1,7 @@ """Test blueprints v1""" from django.test import TransactionTestCase -from authentik.blueprints.v1.importer import StringImporter +from authentik.blueprints.v1.importer import YAMLStringImporter from authentik.flows.models import Flow from authentik.lib.generators import generate_id from authentik.lib.tests.utils import load_fixture @@ -18,7 +18,7 @@ class TestBlueprintsV1Conditions(TransactionTestCase): "fixtures/conditions_fulfilled.yaml", id1=flow_slug1, id2=flow_slug2 ) - importer = StringImporter(import_yaml) + importer = YAMLStringImporter(import_yaml) self.assertTrue(importer.validate()[0]) self.assertTrue(importer.apply()) # Ensure objects exist @@ -35,7 +35,7 @@ class TestBlueprintsV1Conditions(TransactionTestCase): "fixtures/conditions_not_fulfilled.yaml", id1=flow_slug1, id2=flow_slug2 ) - importer = StringImporter(import_yaml) + importer = YAMLStringImporter(import_yaml) self.assertTrue(importer.validate()[0]) self.assertTrue(importer.apply()) # Ensure objects do not exist diff --git a/authentik/blueprints/tests/test_v1_state.py b/authentik/blueprints/tests/test_v1_state.py index a14b7f5e8..29b211482 100644 --- a/authentik/blueprints/tests/test_v1_state.py +++ b/authentik/blueprints/tests/test_v1_state.py @@ -1,7 +1,7 @@ """Test blueprints v1""" from django.test import TransactionTestCase -from authentik.blueprints.v1.importer import StringImporter +from authentik.blueprints.v1.importer import YAMLStringImporter from authentik.flows.models import Flow from authentik.lib.generators import generate_id from authentik.lib.tests.utils import load_fixture @@ -15,7 +15,7 @@ class TestBlueprintsV1State(TransactionTestCase): flow_slug = generate_id() import_yaml = load_fixture("fixtures/state_present.yaml", id=flow_slug) - importer = StringImporter(import_yaml) + importer = YAMLStringImporter(import_yaml) self.assertTrue(importer.validate()[0]) self.assertTrue(importer.apply()) # Ensure object exists @@ -30,7 +30,7 @@ class TestBlueprintsV1State(TransactionTestCase): self.assertEqual(flow.title, "bar") # Ensure importer updates it - importer = StringImporter(import_yaml) + importer = YAMLStringImporter(import_yaml) self.assertTrue(importer.validate()[0]) self.assertTrue(importer.apply()) flow: Flow = Flow.objects.filter(slug=flow_slug).first() @@ -41,7 +41,7 @@ class TestBlueprintsV1State(TransactionTestCase): flow_slug = generate_id() import_yaml = load_fixture("fixtures/state_created.yaml", id=flow_slug) - importer = StringImporter(import_yaml) + importer = YAMLStringImporter(import_yaml) self.assertTrue(importer.validate()[0]) self.assertTrue(importer.apply()) # Ensure object exists @@ -56,7 +56,7 @@ class TestBlueprintsV1State(TransactionTestCase): self.assertEqual(flow.title, "bar") # Ensure importer doesn't update it - importer = StringImporter(import_yaml) + importer = YAMLStringImporter(import_yaml) self.assertTrue(importer.validate()[0]) self.assertTrue(importer.apply()) flow: Flow = Flow.objects.filter(slug=flow_slug).first() @@ -67,7 +67,7 @@ class TestBlueprintsV1State(TransactionTestCase): flow_slug = generate_id() import_yaml = load_fixture("fixtures/state_created.yaml", id=flow_slug) - importer = StringImporter(import_yaml) + importer = YAMLStringImporter(import_yaml) self.assertTrue(importer.validate()[0]) self.assertTrue(importer.apply()) # Ensure object exists @@ -75,7 +75,7 @@ class TestBlueprintsV1State(TransactionTestCase): self.assertEqual(flow.slug, flow_slug) import_yaml = load_fixture("fixtures/state_absent.yaml", id=flow_slug) - importer = StringImporter(import_yaml) + importer = YAMLStringImporter(import_yaml) self.assertTrue(importer.validate()[0]) self.assertTrue(importer.apply()) flow: Flow = Flow.objects.filter(slug=flow_slug).first() diff --git a/authentik/blueprints/v1/importer.py b/authentik/blueprints/v1/importer.py index fd47e06ae..628eb6fc1 100644 --- a/authentik/blueprints/v1/importer.py +++ b/authentik/blueprints/v1/importer.py @@ -1,6 +1,7 @@ """Blueprint importer""" from contextlib import contextmanager from copy import deepcopy +from json import loads from typing import Any, Optional from dacite.config import Config @@ -27,6 +28,7 @@ from authentik.blueprints.v1.common import ( BlueprintLoader, EntryInvalidError, ) +from authentik.blueprints.v1.json_parser import BlueprintJSONDecoder from authentik.blueprints.v1.meta.registry import BaseMetaModel, registry from authentik.core.models import ( AuthenticatedSession, @@ -330,8 +332,8 @@ class Importer: return successful, logs -class StringImporter(Importer): - """Importer that also parses from string""" +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) @@ -342,3 +344,17 @@ class StringImporter(Importer): 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 = loads(json_import, cls=BlueprintJSONDecoder) + try: + _import = from_dict( + Blueprint, import_dict, config=Config(cast=[BlueprintEntryDesiredState]) + ) + except DaciteError as exc: + raise EntryInvalidError from exc + super().__init__(_import, context) diff --git a/authentik/blueprints/v1/tasks.py b/authentik/blueprints/v1/tasks.py index 40351dfc1..0d0905c03 100644 --- a/authentik/blueprints/v1/tasks.py +++ b/authentik/blueprints/v1/tasks.py @@ -26,7 +26,7 @@ from authentik.blueprints.models import ( BlueprintRetrievalFailed, ) from authentik.blueprints.v1.common import BlueprintLoader, BlueprintMetadata, EntryInvalidError -from authentik.blueprints.v1.importer import StringImporter +from authentik.blueprints.v1.importer import YAMLStringImporter from authentik.blueprints.v1.labels import LABEL_AUTHENTIK_INSTANTIATE from authentik.blueprints.v1.oci import OCI_PREFIX from authentik.events.monitored_tasks import ( @@ -190,7 +190,7 @@ def apply_blueprint(self: MonitoredTask, instance_pk: str): self.set_uid(slugify(instance.name)) blueprint_content = instance.retrieve() file_hash = sha512(blueprint_content.encode()).hexdigest() - importer = StringImporter(blueprint_content, instance.context) + importer = YAMLStringImporter(blueprint_content, instance.context) if importer.blueprint.metadata: instance.metadata = asdict(importer.blueprint.metadata) valid, logs = importer.validate() diff --git a/authentik/flows/api/flows.py b/authentik/flows/api/flows.py index a12e28929..a67d6f9ff 100644 --- a/authentik/flows/api/flows.py +++ b/authentik/flows/api/flows.py @@ -16,7 +16,7 @@ from structlog.stdlib import get_logger from authentik.api.decorators import permission_required from authentik.blueprints.v1.exporter import FlowExporter -from authentik.blueprints.v1.importer import SERIALIZER_CONTEXT_BLUEPRINT, StringImporter +from authentik.blueprints.v1.importer import SERIALIZER_CONTEXT_BLUEPRINT, YAMLStringImporter from authentik.core.api.used_by import UsedByMixin from authentik.core.api.utils import CacheSerializer, LinkSerializer, PassiveSerializer from authentik.events.utils import sanitize_dict @@ -181,7 +181,7 @@ class FlowViewSet(UsedByMixin, ModelViewSet): if not file: return Response(data=import_response.initial_data, status=400) - importer = StringImporter(file.read().decode()) + importer = YAMLStringImporter(file.read().decode()) valid, logs = importer.validate() import_response.initial_data["logs"] = [sanitize_dict(log) for log in logs] import_response.initial_data["success"] = valid