diff --git a/.vscode/settings.json b/.vscode/settings.json index 699d566efb..a503990045 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -29,6 +29,7 @@ "!Enumerate sequence", "!Env scalar", "!Find sequence", + "!FindObject sequence", "!Format sequence", "!If sequence", "!Index scalar", @@ -51,7 +52,9 @@ "ignoreCase": false } ], - "go.testFlags": ["-count=1"], + "go.testFlags": [ + "-count=1" + ], "github-actions.workflows.pinned.workflows": [ ".github/workflows/ci-main.yml" ] diff --git a/authentik/blueprints/tests/fixtures/tags.yaml b/authentik/blueprints/tests/fixtures/tags.yaml index 7cc660ad5c..ef3f9563d5 100644 --- a/authentik/blueprints/tests/fixtures/tags.yaml +++ b/authentik/blueprints/tests/fixtures/tags.yaml @@ -150,6 +150,7 @@ entries: at_index_sequence_default: !AtIndex [!Context sequence, 100, "non existent"] at_index_mapping: !AtIndex [!Context mapping, "key2"] at_index_mapping_default: !AtIndex [!Context mapping, "invalid", "non existent"] + find_object: !AtIndex [!FindObject [authentik_providers_oauth2.scopemapping, [scope_name, openid]], managed] identifiers: name: test conditions: diff --git a/authentik/blueprints/tests/test_v1.py b/authentik/blueprints/tests/test_v1.py index a001ba2100..21c95d19a6 100644 --- a/authentik/blueprints/tests/test_v1.py +++ b/authentik/blueprints/tests/test_v1.py @@ -4,6 +4,7 @@ from os import environ from django.test import TransactionTestCase +from authentik.blueprints.tests import apply_blueprint from authentik.blueprints.v1.exporter import FlowExporter from authentik.blueprints.v1.importer import Importer, transaction_rollback from authentik.core.models import Group @@ -126,6 +127,7 @@ class TestBlueprintsV1(TransactionTestCase): self.assertEqual(Prompt.objects.filter(field_key="username").count(), count_before) + @apply_blueprint("system/providers-oauth2.yaml") def test_import_yaml_tags(self): """Test some yaml tags""" ExpressionPolicy.objects.filter(name="foo-bar-baz-qux").delete() @@ -136,91 +138,93 @@ class TestBlueprintsV1(TransactionTestCase): self.assertTrue(importer.apply()) policy = ExpressionPolicy.objects.filter(name="foo-bar-baz-qux").first() self.assertTrue(policy) - self.assertTrue( - Group.objects.filter( - attributes={ - "policy_pk1": str(policy.pk) + "-suffix", - "policy_pk2": str(policy.pk) + "-suffix", - "boolAnd": True, - "boolNand": False, - "boolOr": True, - "boolNor": False, - "boolXor": True, - "boolXnor": False, - "boolComplex": True, - "if_true_complex": { - "dictionary": { - "with": {"keys": "and_values"}, - "and_nested_custom_tags": "foo-bar", - } + group = Group.objects.filter(name="test").first() + self.assertIsNotNone(group) + self.assertEqual( + group.attributes, + { + "policy_pk1": str(policy.pk) + "-suffix", + "policy_pk2": str(policy.pk) + "-suffix", + "boolAnd": True, + "boolNand": False, + "boolOr": True, + "boolNor": False, + "boolXor": True, + "boolXnor": False, + "boolComplex": True, + "if_true_complex": { + "dictionary": { + "with": {"keys": "and_values"}, + "and_nested_custom_tags": "foo-bar", + } + }, + "if_false_complex": ["list", "with", "items", "foo-bar"], + "if_true_simple": True, + "if_short": True, + "if_false_simple": 2, + "enumerate_mapping_to_mapping": { + "prefix-key1": "other-prefix-value", + "prefix-key2": "other-prefix-2", + }, + "enumerate_mapping_to_sequence": [ + "prefixed-pair-key1-value", + "prefixed-pair-key2-2", + ], + "enumerate_sequence_to_sequence": [ + "prefixed-items-0-foo", + "prefixed-items-1-bar", + ], + "enumerate_sequence_to_mapping": {"index: 0": "foo", "index: 1": "bar"}, + "nested_complex_enumeration": { + "0": { + "key1": [ + ["prefixed-f", "prefixed-o", "prefixed-o"], + { + "outer_value": "foo", + "outer_index": 0, + "middle_value": "value", + "middle_index": "key1", + }, + ], + "key2": [ + ["prefixed-f", "prefixed-o", "prefixed-o"], + { + "outer_value": "foo", + "outer_index": 0, + "middle_value": 2, + "middle_index": "key2", + }, + ], }, - "if_false_complex": ["list", "with", "items", "foo-bar"], - "if_true_simple": True, - "if_short": True, - "if_false_simple": 2, - "enumerate_mapping_to_mapping": { - "prefix-key1": "other-prefix-value", - "prefix-key2": "other-prefix-2", + "1": { + "key1": [ + ["prefixed-b", "prefixed-a", "prefixed-r"], + { + "outer_value": "bar", + "outer_index": 1, + "middle_value": "value", + "middle_index": "key1", + }, + ], + "key2": [ + ["prefixed-b", "prefixed-a", "prefixed-r"], + { + "outer_value": "bar", + "outer_index": 1, + "middle_value": 2, + "middle_index": "key2", + }, + ], }, - "enumerate_mapping_to_sequence": [ - "prefixed-pair-key1-value", - "prefixed-pair-key2-2", - ], - "enumerate_sequence_to_sequence": [ - "prefixed-items-0-foo", - "prefixed-items-1-bar", - ], - "enumerate_sequence_to_mapping": {"index: 0": "foo", "index: 1": "bar"}, - "nested_complex_enumeration": { - "0": { - "key1": [ - ["prefixed-f", "prefixed-o", "prefixed-o"], - { - "outer_value": "foo", - "outer_index": 0, - "middle_value": "value", - "middle_index": "key1", - }, - ], - "key2": [ - ["prefixed-f", "prefixed-o", "prefixed-o"], - { - "outer_value": "foo", - "outer_index": 0, - "middle_value": 2, - "middle_index": "key2", - }, - ], - }, - "1": { - "key1": [ - ["prefixed-b", "prefixed-a", "prefixed-r"], - { - "outer_value": "bar", - "outer_index": 1, - "middle_value": "value", - "middle_index": "key1", - }, - ], - "key2": [ - ["prefixed-b", "prefixed-a", "prefixed-r"], - { - "outer_value": "bar", - "outer_index": 1, - "middle_value": 2, - "middle_index": "key2", - }, - ], - }, - }, - "nested_context": "context-nested-value", - "env_null": None, - "at_index_sequence": "foo", - "at_index_sequence_default": "non existent", - "at_index_mapping": 2, - "at_index_mapping_default": "non existent", - } - ).exists() + }, + "nested_context": "context-nested-value", + "env_null": None, + "at_index_sequence": "foo", + "at_index_sequence_default": "non existent", + "at_index_mapping": 2, + "at_index_mapping_default": "non existent", + "find_object": "goauthentik.io/providers/oauth2/scope-openid", + }, ) self.assertTrue( OAuthSource.objects.filter( diff --git a/authentik/blueprints/v1/common.py b/authentik/blueprints/v1/common.py index 538e7316b5..f2912821c3 100644 --- a/authentik/blueprints/v1/common.py +++ b/authentik/blueprints/v1/common.py @@ -311,7 +311,7 @@ class Format(YAMLTag): class Find(YAMLTag): - """Find any object""" + """Find any object primary key""" model_name: str | YAMLTag conditions: list[list] @@ -326,7 +326,7 @@ class Find(YAMLTag): values.append(loader.construct_object(node_values)) self.conditions.append(values) - def resolve(self, entry: BlueprintEntry, blueprint: Blueprint) -> Any: + def _get_instance(self, entry: BlueprintEntry, blueprint: Blueprint) -> Any: if isinstance(self.model_name, YAMLTag): model_name = self.model_name.resolve(entry, blueprint) else: @@ -348,12 +348,29 @@ class Find(YAMLTag): else: query_value = cond[1] query &= Q(**{query_key: query_value}) - instance = model_class.objects.filter(query).first() + return model_class.objects.filter(query).first() + + def resolve(self, entry: BlueprintEntry, blueprint: Blueprint) -> Any: + instance = self._get_instance(entry, blueprint) if instance: return instance.pk return None +class FindObject(Find): + """Find any object""" + + def resolve(self, entry: BlueprintEntry, blueprint: Blueprint) -> Any: + instance = self._get_instance(entry, blueprint) + if not instance: + return None + if not isinstance(instance, SerializerModel): + raise EntryInvalidError.from_entry( + f"Model {self.model_name} is not resolvable through FindObject", entry + ) + return instance.serializer(instance=instance).data + + class Condition(YAMLTag): """Convert all values to a single boolean""" @@ -649,6 +666,7 @@ class BlueprintLoader(SafeLoader): super().__init__(*args, **kwargs) self.add_constructor("!KeyOf", KeyOf) self.add_constructor("!Find", Find) + self.add_constructor("!FindObject", FindObject) self.add_constructor("!Context", Context) self.add_constructor("!Format", Format) self.add_constructor("!Condition", Condition) diff --git a/website/docs/customize/blueprints/v1/tags.md b/website/docs/customize/blueprints/v1/tags.md index e0c325fc4d..004b5d6cab 100644 --- a/website/docs/customize/blueprints/v1/tags.md +++ b/website/docs/customize/blueprints/v1/tags.md @@ -12,6 +12,7 @@ For VS Code, for example, add these entries to your `settings.json`: "!Enumerate sequence", "!Env scalar", "!Find sequence", + "!FindObject sequence", "!Format sequence", "!If sequence", "!Index scalar", @@ -60,7 +61,22 @@ configure_flow: ] ``` -Looks up any model and resolves to the the matches' primary key. +Looks up any model and resolves to the matches' primary key. +First argument is the model to be queried, remaining arguments are expected to be pairs of key=value pairs to query for. + +#### `!FindObject` authentik 2025.2+ + +Examples: + +```yaml +flow_designation: + !AtIndex [ + !FindObject [authentik_flows.flow, [slug, default-password-change]], + designation, + ] +``` + +Looks up any model and resolves to the matches' serialized data. First argument is the model to be queried, remaining arguments are expected to be pairs of key=value pairs to query for. #### `!Context`