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 <jens@goauthentik.io>

* make blueprint user password optional

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* ensure user doesn't have a usable password set when its an empty string

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
This commit is contained in:
Jens L 2023-08-28 18:27:44 +02:00 committed by GitHub
parent 799b9c09de
commit 30cb38ac6d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 42 additions and 17 deletions

View File

@ -7,3 +7,4 @@ build/**
build_docs/** build_docs/**
Dockerfile Dockerfile
authentik/enterprise authentik/enterprise
blueprints/local

View File

@ -45,3 +45,8 @@ entries:
attrs: attrs:
name: "%(uid)s" name: "%(uid)s"
password: "%(uid)s" password: "%(uid)s"
- model: authentik_core.user
identifiers:
username: "%(uid)s-no-password"
attrs:
name: "%(uid)s"

View File

@ -36,6 +36,7 @@ entries:
model: authentik_policies_expression.expressionpolicy model: authentik_policies_expression.expressionpolicy
- attrs: - attrs:
attributes: attributes:
env_null: !Env [bar-baz, null]
policy_pk1: policy_pk1:
!Format [ !Format [
"%s-%s", "%s-%s",

View File

@ -213,8 +213,9 @@ class TestBlueprintsV1(TransactionTestCase):
}, },
}, },
"nested_context": "context-nested-value", "nested_context": "context-nested-value",
"env_null": None,
} }
) ).exists()
) )
self.assertTrue( self.assertTrue(
OAuthSource.objects.filter( OAuthSource.objects.filter(

View File

@ -51,3 +51,9 @@ class TestBlueprintsV1ConditionalFields(TransactionTestCase):
user: User = User.objects.filter(username=self.uid).first() user: User = User.objects.filter(username=self.uid).first()
self.assertIsNotNone(user) self.assertIsNotNone(user)
self.assertTrue(user.check_password(self.uid)) 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())

View File

@ -223,11 +223,11 @@ class Env(YAMLTag):
if isinstance(node, ScalarNode): if isinstance(node, ScalarNode):
self.key = node.value self.key = node.value
if isinstance(node, SequenceNode): if isinstance(node, SequenceNode):
self.key = node.value[0].value self.key = loader.construct_object(node.value[0])
self.default = node.value[1].value self.default = loader.construct_object(node.value[1])
def resolve(self, entry: BlueprintEntry, blueprint: Blueprint) -> Any: def resolve(self, entry: BlueprintEntry, blueprint: Blueprint) -> Any:
return getenv(self.key, self.default) return getenv(self.key) or self.default
class Context(YAMLTag): class Context(YAMLTag):
@ -242,8 +242,8 @@ class Context(YAMLTag):
if isinstance(node, ScalarNode): if isinstance(node, ScalarNode):
self.key = node.value self.key = node.value
if isinstance(node, SequenceNode): if isinstance(node, SequenceNode):
self.key = node.value[0].value self.key = loader.construct_object(node.value[0])
self.default = node.value[1].value self.default = loader.construct_object(node.value[1])
def resolve(self, entry: BlueprintEntry, blueprint: Blueprint) -> Any: def resolve(self, entry: BlueprintEntry, blueprint: Blueprint) -> Any:
value = self.default value = self.default
@ -262,7 +262,7 @@ class Format(YAMLTag):
def __init__(self, loader: "BlueprintLoader", node: SequenceNode) -> None: def __init__(self, loader: "BlueprintLoader", node: SequenceNode) -> None:
super().__init__() super().__init__()
self.format_string = node.value[0].value self.format_string = loader.construct_object(node.value[0])
self.args = [] self.args = []
for raw_node in node.value[1:]: for raw_node in node.value[1:]:
self.args.append(loader.construct_object(raw_node)) self.args.append(loader.construct_object(raw_node))
@ -341,7 +341,7 @@ class Condition(YAMLTag):
def __init__(self, loader: "BlueprintLoader", node: SequenceNode) -> None: def __init__(self, loader: "BlueprintLoader", node: SequenceNode) -> None:
super().__init__() super().__init__()
self.mode = node.value[0].value self.mode = loader.construct_object(node.value[0])
self.args = [] self.args = []
for raw_node in node.value[1:]: for raw_node in node.value[1:]:
self.args.append(loader.construct_object(raw_node)) self.args.append(loader.construct_object(raw_node))
@ -416,7 +416,7 @@ class Enumerate(YAMLTag, YAMLTagContext):
def __init__(self, loader: "BlueprintLoader", node: SequenceNode) -> None: def __init__(self, loader: "BlueprintLoader", node: SequenceNode) -> None:
super().__init__() super().__init__()
self.iterable = loader.construct_object(node.value[0]) 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.item_body = loader.construct_object(node.value[2])
self.__current_context: tuple[Any, Any] = tuple() self.__current_context: tuple[Any, Any] = tuple()

View File

@ -123,27 +123,35 @@ class UserSerializer(ModelSerializer):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
if SERIALIZER_CONTEXT_BLUEPRINT in self.context: 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: def create(self, validated_data: dict) -> User:
"""If this serializer is used in the blueprint context, we allow for """If this serializer is used in the blueprint context, we allow for
directly setting a password. However should be done via the `set_password` directly setting a password. However should be done via the `set_password`
method instead of directly setting it like rest_framework.""" method instead of directly setting it like rest_framework."""
password = validated_data.pop("password", None)
instance: User = super().create(validated_data) instance: User = super().create(validated_data)
if SERIALIZER_CONTEXT_BLUEPRINT in self.context and "password" in validated_data: self._set_password(instance, password)
instance.set_password(validated_data["password"])
instance.save()
return instance return instance
def update(self, instance: User, validated_data: dict) -> User: def update(self, instance: User, validated_data: dict) -> User:
"""Same as `create` above, set the password directly if we're in a blueprint """Same as `create` above, set the password directly if we're in a blueprint
context""" context"""
password = validated_data.pop("password", None)
instance = super().update(instance, validated_data) instance = super().update(instance, validated_data)
if SERIALIZER_CONTEXT_BLUEPRINT in self.context and "password" in validated_data: self._set_password(instance, password)
instance.set_password(validated_data["password"])
instance.save()
return instance 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: def validate_path(self, path: str) -> str:
"""Validate path""" """Validate path"""
if path[:1] == "/" or path[-1] == "/": if path[:1] == "/" or path[-1] == "/":

View File

@ -8400,7 +8400,10 @@
"title": "Type" "title": "Type"
}, },
"password": { "password": {
"type": "string", "type": [
"string",
"null"
],
"minLength": 1, "minLength": 1,
"title": "Password" "title": "Password"
} }