From 98b5b75f29d4467b389cefffe7601eeeaaf6278e Mon Sep 17 00:00:00 2001 From: Marc 'risson' Schmitt Date: Wed, 18 Dec 2024 14:10:37 +0100 Subject: [PATCH] blueprints: add AtIndex tag (#12386) --- .vscode/settings.json | 3 +- authentik/blueprints/tests/fixtures/tags.yaml | 4 ++ authentik/blueprints/tests/test_v1.py | 4 ++ authentik/blueprints/v1/common.py | 52 +++++++++++++++++++ website/docs/customize/blueprints/v1/tags.md | 23 +++++++- 5 files changed, 84 insertions(+), 2 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 152d5d5c5e..699d566efb 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -33,7 +33,8 @@ "!If sequence", "!Index scalar", "!KeyOf scalar", - "!Value scalar" + "!Value scalar", + "!AtIndex scalar" ], "typescript.preferences.importModuleSpecifier": "non-relative", "typescript.preferences.importModuleSpecifierEnding": "index", diff --git a/authentik/blueprints/tests/fixtures/tags.yaml b/authentik/blueprints/tests/fixtures/tags.yaml index 8324431178..7cc660ad5c 100644 --- a/authentik/blueprints/tests/fixtures/tags.yaml +++ b/authentik/blueprints/tests/fixtures/tags.yaml @@ -146,6 +146,10 @@ entries: ] ] nested_context: !Context context2 + at_index_sequence: !AtIndex [!Context sequence, 0] + 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"] identifiers: name: test conditions: diff --git a/authentik/blueprints/tests/test_v1.py b/authentik/blueprints/tests/test_v1.py index 5f85e69477..a001ba2100 100644 --- a/authentik/blueprints/tests/test_v1.py +++ b/authentik/blueprints/tests/test_v1.py @@ -215,6 +215,10 @@ class TestBlueprintsV1(TransactionTestCase): }, "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() ) diff --git a/authentik/blueprints/v1/common.py b/authentik/blueprints/v1/common.py index 424b085da2..538e7316b5 100644 --- a/authentik/blueprints/v1/common.py +++ b/authentik/blueprints/v1/common.py @@ -24,6 +24,10 @@ from authentik.lib.sentry import SentryIgnoredException from authentik.policies.models import PolicyBindingModel +class UNSET: + """Used to test whether a key has not been set.""" + + def get_attrs(obj: SerializerModel) -> dict[str, Any]: """Get object's attributes via their serializer, and convert it to a normal dict""" serializer: Serializer = obj.serializer(obj) @@ -556,6 +560,53 @@ class Value(EnumeratedItem): raise EntryInvalidError.from_entry(f"Empty/invalid context: {context}", entry) from exc +class AtIndex(YAMLTag): + """Get value at index of a sequence or mapping""" + + obj: YAMLTag | dict | list | tuple + attribute: int | str | YAMLTag + default: Any | UNSET + + def __init__(self, loader: "BlueprintLoader", node: SequenceNode) -> None: + super().__init__() + self.obj = loader.construct_object(node.value[0]) + self.attribute = loader.construct_object(node.value[1]) + if len(node.value) == 2: # noqa: PLR2004 + self.default = UNSET + else: + self.default = loader.construct_object(node.value[2]) + + def resolve(self, entry: BlueprintEntry, blueprint: Blueprint) -> Any: + if isinstance(self.obj, YAMLTag): + obj = self.obj.resolve(entry, blueprint) + else: + obj = self.obj + if isinstance(self.attribute, YAMLTag): + attribute = self.attribute.resolve(entry, blueprint) + else: + attribute = self.attribute + + if isinstance(obj, list | tuple): + try: + return obj[attribute] + except TypeError as exc: + raise EntryInvalidError.from_entry( + f"Invalid index for list: {attribute}", entry + ) from exc + except IndexError as exc: + if self.default is UNSET: + raise EntryInvalidError.from_entry( + f"Index out of range: {attribute}", entry + ) from exc + return self.default + if attribute in obj: + return obj[attribute] + else: + if self.default is UNSET: + raise EntryInvalidError.from_entry(f"Key does not exist: {attribute}", entry) + return self.default + + class BlueprintDumper(SafeDumper): """Dump dataclasses to yaml""" @@ -606,6 +657,7 @@ class BlueprintLoader(SafeLoader): self.add_constructor("!Enumerate", Enumerate) self.add_constructor("!Value", Value) self.add_constructor("!Index", Index) + self.add_constructor("!AtIndex", AtIndex) class EntryInvalidError(SentryIgnoredException): diff --git a/website/docs/customize/blueprints/v1/tags.md b/website/docs/customize/blueprints/v1/tags.md index a60309a2c0..e0c325fc4d 100644 --- a/website/docs/customize/blueprints/v1/tags.md +++ b/website/docs/customize/blueprints/v1/tags.md @@ -16,7 +16,8 @@ For VS Code, for example, add these entries to your `settings.json`: "!If sequence", "!Index scalar", "!KeyOf scalar", - "!Value scalar" + "!Value scalar", + "!AtIndex scalar" ] } ``` @@ -299,3 +300,23 @@ The above example will resolve to something like this: - "bar: (index: 1, letter: a)" - "bar: (index: 2, letter: r)" ``` + +#### `!AtIndex` authentik 2024.12+ + +Minimal example: + +```yaml +value_in_sequence: !AtIndex [["first", "second"], 0] # Resolves to "first" +value_in_mapping: !AtIndex [{ "foo": "bar", "other": "value" }, "foo"] # Resolves to "bar" +``` + +Example with default value: + +```yaml +default_value: !AtIndex [["first"], 100, "default"] # Resolves to "default" +default_value: !AtIndex [["first"], 100] # Throws an error +``` + +Resolves to the value at a specific index in a sequence or a mapping. + +If no value can be found at the specified index and no default is provided, an error is raised and the blueprint is invalid.