diff --git a/authentik/blueprints/tests/fixtures/conditions_fulfilled.yaml b/authentik/blueprints/tests/fixtures/conditions_fulfilled.yaml index 659bd84b8..841333241 100644 --- a/authentik/blueprints/tests/fixtures/conditions_fulfilled.yaml +++ b/authentik/blueprints/tests/fixtures/conditions_fulfilled.yaml @@ -1,21 +1,21 @@ version: 1 entries: -- identifiers: - name: "%(id1)s" - slug: "%(id1)s" - model: authentik_flows.flow - conditions: - - true - attrs: - designation: stage_configuration - title: foo -- identifiers: - name: "%(id2)s" - slug: "%(id2)s" - model: authentik_flows.flow - conditions: - - true - - true - attrs: - designation: stage_configuration - title: foo + - identifiers: + name: "%(id1)s" + slug: "%(id1)s" + model: authentik_flows.flow + conditions: + - true + attrs: + designation: stage_configuration + title: foo + - identifiers: + name: "%(id2)s" + slug: "%(id2)s" + model: authentik_flows.flow + conditions: + - true + - true + attrs: + designation: stage_configuration + title: foo diff --git a/authentik/blueprints/tests/fixtures/conditions_not_fulfilled.yaml b/authentik/blueprints/tests/fixtures/conditions_not_fulfilled.yaml index 0e4b9abdb..96e62e058 100644 --- a/authentik/blueprints/tests/fixtures/conditions_not_fulfilled.yaml +++ b/authentik/blueprints/tests/fixtures/conditions_not_fulfilled.yaml @@ -1,21 +1,21 @@ version: 1 entries: -- identifiers: - name: "%(id1)s" - slug: "%(id1)s" - model: authentik_flows.flow - conditions: - - false - attrs: - designation: stage_configuration - title: foo -- identifiers: - name: "%(id2)s" - slug: "%(id2)s" - model: authentik_flows.flow - conditions: - - true - - false - attrs: - designation: stage_configuration - title: foo + - identifiers: + name: "%(id1)s" + slug: "%(id1)s" + model: authentik_flows.flow + conditions: + - false + attrs: + designation: stage_configuration + title: foo + - identifiers: + name: "%(id2)s" + slug: "%(id2)s" + model: authentik_flows.flow + conditions: + - true + - false + attrs: + designation: stage_configuration + title: foo diff --git a/authentik/blueprints/tests/fixtures/state_absent.yaml b/authentik/blueprints/tests/fixtures/state_absent.yaml index e1588c940..d73d929e2 100644 --- a/authentik/blueprints/tests/fixtures/state_absent.yaml +++ b/authentik/blueprints/tests/fixtures/state_absent.yaml @@ -1,7 +1,7 @@ version: 1 entries: -- identifiers: - name: "%(id)s" - slug: "%(id)s" - model: authentik_flows.flow - state: absent + - identifiers: + name: "%(id)s" + slug: "%(id)s" + model: authentik_flows.flow + state: absent diff --git a/authentik/blueprints/tests/fixtures/state_created.yaml b/authentik/blueprints/tests/fixtures/state_created.yaml index 8091fd600..8806bf085 100644 --- a/authentik/blueprints/tests/fixtures/state_created.yaml +++ b/authentik/blueprints/tests/fixtures/state_created.yaml @@ -1,10 +1,10 @@ version: 1 entries: -- identifiers: - name: "%(id)s" - slug: "%(id)s" - model: authentik_flows.flow - state: created - attrs: - designation: stage_configuration - title: foo + - identifiers: + name: "%(id)s" + slug: "%(id)s" + model: authentik_flows.flow + state: created + attrs: + designation: stage_configuration + title: foo diff --git a/authentik/blueprints/tests/fixtures/state_present.yaml b/authentik/blueprints/tests/fixtures/state_present.yaml index 86286112b..1ecff0ec6 100644 --- a/authentik/blueprints/tests/fixtures/state_present.yaml +++ b/authentik/blueprints/tests/fixtures/state_present.yaml @@ -1,10 +1,10 @@ version: 1 entries: -- identifiers: - name: "%(id)s" - slug: "%(id)s" - model: authentik_flows.flow - state: present - attrs: - designation: stage_configuration - title: foo + - identifiers: + name: "%(id)s" + slug: "%(id)s" + model: authentik_flows.flow + state: present + attrs: + designation: stage_configuration + title: foo diff --git a/authentik/blueprints/tests/fixtures/static_prompt_export.yaml b/authentik/blueprints/tests/fixtures/static_prompt_export.yaml index 8d7dd7f58..28d3588cd 100644 --- a/authentik/blueprints/tests/fixtures/static_prompt_export.yaml +++ b/authentik/blueprints/tests/fixtures/static_prompt_export.yaml @@ -1,12 +1,12 @@ version: 1 entries: -- identifiers: - pk: cb954fd4-65a5-4ad9-b1ee-180ee9559cf4 - model: authentik_stages_prompt.prompt - attrs: - field_key: username - label: Username - type: username - required: true - placeholder: Username - order: 0 + - identifiers: + pk: cb954fd4-65a5-4ad9-b1ee-180ee9559cf4 + model: authentik_stages_prompt.prompt + attrs: + field_key: username + label: Username + type: username + required: true + placeholder: Username + order: 0 diff --git a/authentik/blueprints/tests/fixtures/tags.yaml b/authentik/blueprints/tests/fixtures/tags.yaml index a05482bd2..e7c6d4071 100644 --- a/authentik/blueprints/tests/fixtures/tags.yaml +++ b/authentik/blueprints/tests/fixtures/tags.yaml @@ -4,37 +4,97 @@ context: policy_property: name policy_property_value: foo-bar-baz-qux entries: -- model: authentik_sources_oauth.oauthsource - identifiers: - slug: test - attrs: - name: test - provider_type: github - consumer_key: !Env foo - consumer_secret: !Env [bar, baz] - authentication_flow: !Find [authentik_flows.Flow, [slug, default-source-authentication]] - enrollment_flow: !Find [authentik_flows.Flow, [slug, default-source-enrollment]] -- attrs: - expression: return True - identifiers: - name: !Format [foo-%s-%s-%s, !Context foo, !Context bar, qux] - id: policy - model: authentik_policies_expression.expressionpolicy -- attrs: - attributes: - policy_pk1: !Format ["%s-%s", !Find [authentik_policies_expression.expressionpolicy, [!Context policy_property, !Context policy_property_value], [expression, return True]], suffix] - policy_pk2: !Format ["%s-%s", !KeyOf policy, suffix] - boolAnd: !Condition [AND, !Context foo, !Format ["%s", "a_string"], 1] - boolNand: !Condition [NAND, !Context foo, !Format ["%s", "a_string"], 1] - boolOr: !Condition [OR, !Context foo, !Format ["%s", "a_string"], null] - boolNor: !Condition [NOR, !Context foo, !Format ["%s", "a_string"], null] - boolXor: !Condition [XOR, !Context foo, !Format ["%s", "a_string"], 1] - boolXnor: !Condition [XNOR, !Context foo, !Format ["%s", "a_string"], 1] - boolComplex: !Condition [XNOR, !Condition [AND, !Context non_existing], !Condition [NOR, a string], !Condition [XOR, null]] - identifiers: - name: test - conditions: - - !Condition [AND, true, true, text] - - true - - text - model: authentik_core.group + - model: authentik_sources_oauth.oauthsource + identifiers: + slug: test + attrs: + name: test + provider_type: github + consumer_key: !Env foo + consumer_secret: !Env [bar, baz] + authentication_flow: + !Find [ + authentik_flows.Flow, + [slug, default-source-authentication], + ] + enrollment_flow: + !Find [authentik_flows.Flow, [slug, default-source-enrollment]] + - attrs: + expression: return True + identifiers: + name: !Format [foo-%s-%s-%s, !Context foo, !Context bar, qux] + id: policy + model: authentik_policies_expression.expressionpolicy + - attrs: + attributes: + policy_pk1: + !Format [ + "%s-%s", + !Find [ + authentik_policies_expression.expressionpolicy, + [ + !Context policy_property, + !Context policy_property_value, + ], + [expression, return True], + ], + suffix, + ] + policy_pk2: !Format ["%s-%s", !KeyOf policy, suffix] + boolAnd: + !Condition [AND, !Context foo, !Format ["%s", "a_string"], 1] + boolNand: + !Condition [NAND, !Context foo, !Format ["%s", "a_string"], 1] + boolOr: + !Condition [ + OR, + !Context foo, + !Format ["%s", "a_string"], + null, + ] + boolNor: + !Condition [ + NOR, + !Context foo, + !Format ["%s", "a_string"], + null, + ] + boolXor: + !Condition [XOR, !Context foo, !Format ["%s", "a_string"], 1] + boolXnor: + !Condition [XNOR, !Context foo, !Format ["%s", "a_string"], 1] + boolComplex: + !Condition [ + XNOR, + !Condition [AND, !Context non_existing], + !Condition [NOR, a string], + !Condition [XOR, null], + ] + if_true_complex: + !If [ + true, + { + dictionary: + { + with: { keys: "and_values" }, + and_nested_custom_tags: + !Format ["foo-%s", !Context foo], + }, + }, + null, + ] + if_false_complex: + !If [ + !Condition [AND, false], + null, + [list, with, items, !Format ["foo-%s", !Context foo]], + ] + if_true_simple: !If [!Context foo, true, text] + if_false_simple: !If [null, false, 2] + identifiers: + name: test + conditions: + - !Condition [AND, true, true, text] + - true + - text + model: authentik_core.group diff --git a/authentik/blueprints/tests/test_v1.py b/authentik/blueprints/tests/test_v1.py index 5cb5966b2..6b384ac4a 100644 --- a/authentik/blueprints/tests/test_v1.py +++ b/authentik/blueprints/tests/test_v1.py @@ -153,6 +153,15 @@ class TestBlueprintsV1(TransactionTestCase): "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_false_simple": 2, } ) ) diff --git a/authentik/blueprints/v1/common.py b/authentik/blueprints/v1/common.py index ad9c98bf3..1a585b2c8 100644 --- a/authentik/blueprints/v1/common.py +++ b/authentik/blueprints/v1/common.py @@ -312,6 +312,35 @@ class Condition(YAMLTag): raise EntryInvalidError(exc) +class If(YAMLTag): + """Select YAML to use based on condition""" + + condition: Any + when_true: Any + when_false: Any + + # pylint: disable=unused-argument + def __init__(self, loader: "BlueprintLoader", node: SequenceNode) -> None: + super().__init__() + self.condition = loader.construct_object(node.value[0]) + self.when_true = loader.construct_object(node.value[1]) + self.when_false = loader.construct_object(node.value[2]) + + def resolve(self, entry: BlueprintEntry, blueprint: Blueprint) -> Any: + if isinstance(self.condition, YAMLTag): + condition = self.condition.resolve(entry, blueprint) + else: + condition = self.condition + + try: + return entry.tag_resolver( + self.when_true if condition else self.when_false, + blueprint, + ) + except TypeError as exc: + raise EntryInvalidError(exc) + + class BlueprintDumper(SafeDumper): """Dump dataclasses to yaml""" @@ -353,6 +382,7 @@ class BlueprintLoader(SafeLoader): 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) diff --git a/website/developer-docs/blueprints/v1/tags.md b/website/developer-docs/blueprints/v1/tags.md index 562f34ed2..da9ec3386 100644 --- a/website/developer-docs/blueprints/v1/tags.md +++ b/website/developer-docs/blueprints/v1/tags.md @@ -45,6 +45,42 @@ Example: `name: !Format [my-policy-%s, !Context instance_name]` Format a string using python's % formatting. First argument is the format string, any remaining arguments are used for formatting. +#### `!If` + +Minimal example: + +`required: !If [true, true, false] # !If [, , ` + +Full example: + +``` +attributes: !If [ + !Condition [...], # Or any valid YAML or custom tag. Evaluated as boolean in Python + { # When condition evaluates to true + dictionary: + { + with: + { + keys: "and_values" + }, + and_nested_custom_tags: !Format ["foo-%s", !Context foo] + } + }, + [ # When condition evaluates to false + list, + with, + items, + !Format ["foo-%s", !Context foo] + ] +] +``` + +Conditionally add YAML to a blueprint. + +Similar to a one-line if, the first argument is the condition, which can be any valid yaml or custom tag. It will be evaluted as boolean in python. However, keep in mind that dictionaries and lists will always evaluate to `true`, unless they are empty. + +The second argument is used when the condition is `true`, and the third - when `false`. The YAML inside both arguments will be fully resolved, thus it is possible to use custom YAML tags and even nest them inside dictionaries and lists. + #### `!Condition` Minimal example: @@ -68,4 +104,4 @@ Requires at least one argument after the mode selection. If only a single argument is provided, its boolean representation will be returned for all normal modes and its negated boolean representation will be returned for all negated modes. -Normally, it should be used to define complex conditions for the `conditions` attribute of a blueprint entry (see [the blueprint file structure](./structure.md)). However, this is essentially just a boolean evaluator so it can be used everywhere a boolean representation is required. +Normally, it should be used to define complex conditions for use with an `!If` tag or for the `conditions` attribute of a blueprint entry (see [the blueprint file structure](./structure.md)). However, this is essentially just a boolean evaluator so it can be used everywhere a boolean representation is required.