From 30cb38ac6df72584f60849eeedb70d8915741347 Mon Sep 17 00:00:00 2001 From: Jens L Date: Mon, 28 Aug 2023 18:27:44 +0200 Subject: [PATCH] blueprints: fix tag values not resolved correctly (#6653) * blueprints: fix tag values not resolved correctly this lead to `null` in an `!Env` tag being returned as `"null"` Signed-off-by: Jens Langhammer * make blueprint user password optional Signed-off-by: Jens Langhammer * ensure user doesn't have a usable password set when its an empty string Signed-off-by: Jens Langhammer --------- Signed-off-by: Jens Langhammer --- .dockerignore | 1 + .../tests/fixtures/conditional_fields.yaml | 5 +++++ authentik/blueprints/tests/fixtures/tags.yaml | 1 + authentik/blueprints/tests/test_v1.py | 3 ++- .../tests/test_v1_conditional_fields.py | 6 +++++ authentik/blueprints/v1/common.py | 16 +++++++------- authentik/core/api/users.py | 22 +++++++++++++------ blueprints/schema.json | 5 ++++- 8 files changed, 42 insertions(+), 17 deletions(-) diff --git a/.dockerignore b/.dockerignore index 1e6a89ac0..72cdb7ebe 100644 --- a/.dockerignore +++ b/.dockerignore @@ -7,3 +7,4 @@ build/** build_docs/** Dockerfile authentik/enterprise +blueprints/local diff --git a/authentik/blueprints/tests/fixtures/conditional_fields.yaml b/authentik/blueprints/tests/fixtures/conditional_fields.yaml index 0380ecd00..5ce07719c 100644 --- a/authentik/blueprints/tests/fixtures/conditional_fields.yaml +++ b/authentik/blueprints/tests/fixtures/conditional_fields.yaml @@ -45,3 +45,8 @@ entries: attrs: name: "%(uid)s" password: "%(uid)s" + - model: authentik_core.user + identifiers: + username: "%(uid)s-no-password" + attrs: + name: "%(uid)s" diff --git a/authentik/blueprints/tests/fixtures/tags.yaml b/authentik/blueprints/tests/fixtures/tags.yaml index 16785ee78..832443117 100644 --- a/authentik/blueprints/tests/fixtures/tags.yaml +++ b/authentik/blueprints/tests/fixtures/tags.yaml @@ -36,6 +36,7 @@ entries: model: authentik_policies_expression.expressionpolicy - attrs: attributes: + env_null: !Env [bar-baz, null] policy_pk1: !Format [ "%s-%s", diff --git a/authentik/blueprints/tests/test_v1.py b/authentik/blueprints/tests/test_v1.py index c5136a1ba..762c76550 100644 --- a/authentik/blueprints/tests/test_v1.py +++ b/authentik/blueprints/tests/test_v1.py @@ -213,8 +213,9 @@ class TestBlueprintsV1(TransactionTestCase): }, }, "nested_context": "context-nested-value", + "env_null": None, } - ) + ).exists() ) self.assertTrue( OAuthSource.objects.filter( diff --git a/authentik/blueprints/tests/test_v1_conditional_fields.py b/authentik/blueprints/tests/test_v1_conditional_fields.py index a28083651..9e0a956a9 100644 --- a/authentik/blueprints/tests/test_v1_conditional_fields.py +++ b/authentik/blueprints/tests/test_v1_conditional_fields.py @@ -51,3 +51,9 @@ class TestBlueprintsV1ConditionalFields(TransactionTestCase): user: User = User.objects.filter(username=self.uid).first() self.assertIsNotNone(user) self.assertTrue(user.check_password(self.uid)) + + def test_user_null(self): + """Test user""" + user: User = User.objects.filter(username=f"{self.uid}-no-password").first() + self.assertIsNotNone(user) + self.assertFalse(user.has_usable_password()) diff --git a/authentik/blueprints/v1/common.py b/authentik/blueprints/v1/common.py index 9eda93300..248737bf8 100644 --- a/authentik/blueprints/v1/common.py +++ b/authentik/blueprints/v1/common.py @@ -223,11 +223,11 @@ class Env(YAMLTag): if isinstance(node, ScalarNode): self.key = node.value if isinstance(node, SequenceNode): - self.key = node.value[0].value - self.default = node.value[1].value + self.key = loader.construct_object(node.value[0]) + self.default = loader.construct_object(node.value[1]) def resolve(self, entry: BlueprintEntry, blueprint: Blueprint) -> Any: - return getenv(self.key, self.default) + return getenv(self.key) or self.default class Context(YAMLTag): @@ -242,8 +242,8 @@ class Context(YAMLTag): if isinstance(node, ScalarNode): self.key = node.value if isinstance(node, SequenceNode): - self.key = node.value[0].value - self.default = node.value[1].value + self.key = loader.construct_object(node.value[0]) + self.default = loader.construct_object(node.value[1]) def resolve(self, entry: BlueprintEntry, blueprint: Blueprint) -> Any: value = self.default @@ -262,7 +262,7 @@ class Format(YAMLTag): def __init__(self, loader: "BlueprintLoader", node: SequenceNode) -> None: super().__init__() - self.format_string = node.value[0].value + self.format_string = loader.construct_object(node.value[0]) self.args = [] for raw_node in node.value[1:]: self.args.append(loader.construct_object(raw_node)) @@ -341,7 +341,7 @@ class Condition(YAMLTag): def __init__(self, loader: "BlueprintLoader", node: SequenceNode) -> None: super().__init__() - self.mode = node.value[0].value + self.mode = loader.construct_object(node.value[0]) self.args = [] for raw_node in node.value[1:]: self.args.append(loader.construct_object(raw_node)) @@ -416,7 +416,7 @@ class Enumerate(YAMLTag, YAMLTagContext): def __init__(self, loader: "BlueprintLoader", node: SequenceNode) -> None: super().__init__() self.iterable = loader.construct_object(node.value[0]) - self.output_body = node.value[1].value + self.output_body = loader.construct_object(node.value[1]) self.item_body = loader.construct_object(node.value[2]) self.__current_context: tuple[Any, Any] = tuple() diff --git a/authentik/core/api/users.py b/authentik/core/api/users.py index 5489b3021..2029ff7a6 100644 --- a/authentik/core/api/users.py +++ b/authentik/core/api/users.py @@ -123,27 +123,35 @@ class UserSerializer(ModelSerializer): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) if SERIALIZER_CONTEXT_BLUEPRINT in self.context: - self.fields["password"] = CharField(required=False) + self.fields["password"] = CharField(required=False, allow_null=True) def create(self, validated_data: dict) -> User: """If this serializer is used in the blueprint context, we allow for directly setting a password. However should be done via the `set_password` method instead of directly setting it like rest_framework.""" + password = validated_data.pop("password", None) instance: User = super().create(validated_data) - if SERIALIZER_CONTEXT_BLUEPRINT in self.context and "password" in validated_data: - instance.set_password(validated_data["password"]) - instance.save() + self._set_password(instance, password) return instance def update(self, instance: User, validated_data: dict) -> User: """Same as `create` above, set the password directly if we're in a blueprint context""" + password = validated_data.pop("password", None) instance = super().update(instance, validated_data) - if SERIALIZER_CONTEXT_BLUEPRINT in self.context and "password" in validated_data: - instance.set_password(validated_data["password"]) - instance.save() + self._set_password(instance, password) return instance + def _set_password(self, instance: User, password: Optional[str]): + """Set password of user if we're in a blueprint context, and if it's an empty + string then use an unusable password""" + if SERIALIZER_CONTEXT_BLUEPRINT in self.context and password: + instance.set_password(password) + instance.save() + if len(instance.password) == 0: + instance.set_unusable_password() + instance.save() + def validate_path(self, path: str) -> str: """Validate path""" if path[:1] == "/" or path[-1] == "/": diff --git a/blueprints/schema.json b/blueprints/schema.json index 064b0117c..5b43dfdaf 100644 --- a/blueprints/schema.json +++ b/blueprints/schema.json @@ -8400,7 +8400,10 @@ "title": "Type" }, "password": { - "type": "string", + "type": [ + "string", + "null" + ], "minLength": 1, "title": "Password" }