From f0619814f99e630300937f5c45fa98e72066bab4 Mon Sep 17 00:00:00 2001 From: Jens L Date: Mon, 29 May 2023 21:28:44 +0200 Subject: [PATCH] blueprints: allow setting user's passwords from blueprints (#5797) Signed-off-by: Jens Langhammer --- .../tests/fixtures/conditional_fields.yaml | 30 +++++++++++-------- .../tests/test_v1_conditional_fields.py | 8 ++++- authentik/blueprints/v1/tasks.py | 2 +- authentik/core/api/tokens.py | 2 +- authentik/core/api/users.py | 25 ++++++++++++++++ blueprints/schema.json | 5 ++++ .../developer-docs/blueprints/v1/models.md | 23 ++++++++++++++ 7 files changed, 80 insertions(+), 15 deletions(-) diff --git a/authentik/blueprints/tests/fixtures/conditional_fields.yaml b/authentik/blueprints/tests/fixtures/conditional_fields.yaml index 6720f2c28..0380ecd00 100644 --- a/authentik/blueprints/tests/fixtures/conditional_fields.yaml +++ b/authentik/blueprints/tests/fixtures/conditional_fields.yaml @@ -11,31 +11,37 @@ metadata: entries: - model: authentik_core.token identifiers: - identifier: %(uid)s-token + identifier: "%(uid)s-token" attrs: - key: %(uid)s - user: %(user)s + key: "%(uid)s" + user: "%(user)s" intent: api - model: authentik_core.application identifiers: - slug: %(uid)s-app + slug: "%(uid)s-app" attrs: - name: %(uid)s-app + name: "%(uid)s-app" icon: https://goauthentik.io/img/icon.png - model: authentik_sources_oauth.oauthsource identifiers: - slug: %(uid)s-source + slug: "%(uid)s-source" attrs: - name: %(uid)s-source + name: "%(uid)s-source" provider_type: azuread - consumer_key: %(uid)s - consumer_secret: %(uid)s + consumer_key: "%(uid)s" + consumer_secret: "%(uid)s" icon: https://goauthentik.io/img/icon.png - model: authentik_flows.flow identifiers: - slug: %(uid)s-flow + slug: "%(uid)s-flow" attrs: - name: %(uid)s-flow - title: %(uid)s-flow + name: "%(uid)s-flow" + title: "%(uid)s-flow" designation: authentication background: https://goauthentik.io/img/icon.png + - model: authentik_core.user + identifiers: + username: "%(uid)s" + attrs: + name: "%(uid)s" + password: "%(uid)s" diff --git a/authentik/blueprints/tests/test_v1_conditional_fields.py b/authentik/blueprints/tests/test_v1_conditional_fields.py index 1372030e5..a28083651 100644 --- a/authentik/blueprints/tests/test_v1_conditional_fields.py +++ b/authentik/blueprints/tests/test_v1_conditional_fields.py @@ -2,7 +2,7 @@ from django.test import TransactionTestCase from authentik.blueprints.v1.importer import Importer -from authentik.core.models import Application, Token +from authentik.core.models import Application, Token, User from authentik.core.tests.utils import create_test_admin_user from authentik.flows.models import Flow from authentik.lib.generators import generate_id @@ -45,3 +45,9 @@ class TestBlueprintsV1ConditionalFields(TransactionTestCase): flow = Flow.objects.filter(slug=f"{self.uid}-flow").first() self.assertIsNotNone(flow) self.assertEqual(flow.background, "https://goauthentik.io/img/icon.png") + + def test_user(self): + """Test user""" + user: User = User.objects.filter(username=self.uid).first() + self.assertIsNotNone(user) + self.assertTrue(user.check_password(self.uid)) diff --git a/authentik/blueprints/v1/tasks.py b/authentik/blueprints/v1/tasks.py index 3a278ef3c..21c9c094c 100644 --- a/authentik/blueprints/v1/tasks.py +++ b/authentik/blueprints/v1/tasks.py @@ -184,9 +184,9 @@ def apply_blueprint(self: MonitoredTask, instance_pk: str): instance: Optional[BlueprintInstance] = None try: instance: BlueprintInstance = BlueprintInstance.objects.filter(pk=instance_pk).first() - self.set_uid(slugify(instance.name)) if not instance or not instance.enabled: return + self.set_uid(slugify(instance.name)) blueprint_content = instance.retrieve() file_hash = sha512(blueprint_content.encode()).hexdigest() importer = Importer(blueprint_content, instance.context) diff --git a/authentik/core/api/tokens.py b/authentik/core/api/tokens.py index b00011baf..7caabb9dd 100644 --- a/authentik/core/api/tokens.py +++ b/authentik/core/api/tokens.py @@ -33,7 +33,7 @@ class TokenSerializer(ManagedSerializer, ModelSerializer): def __init__(self, *args, **kwargs) -> None: super().__init__(*args, **kwargs) if SERIALIZER_CONTEXT_BLUEPRINT in self.context: - self.fields["key"] = CharField() + self.fields["key"] = CharField(required=False) def validate(self, attrs: dict[Any, str]) -> dict[Any, str]: """Ensure only API or App password tokens are created.""" diff --git a/authentik/core/api/users.py b/authentik/core/api/users.py index 9c2f9ed2c..911948fd0 100644 --- a/authentik/core/api/users.py +++ b/authentik/core/api/users.py @@ -51,6 +51,7 @@ from structlog.stdlib import get_logger from authentik.admin.api.metrics import CoordinateSerializer from authentik.api.decorators import permission_required +from authentik.blueprints.v1.importer import SERIALIZER_CONTEXT_BLUEPRINT from authentik.core.api.used_by import UsedByMixin from authentik.core.api.utils import LinkSerializer, PassiveSerializer, is_dict from authentik.core.middleware import ( @@ -112,6 +113,30 @@ class UserSerializer(ModelSerializer): uid = CharField(read_only=True) username = CharField(max_length=150, validators=[UniqueValidator(queryset=User.objects.all())]) + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + if SERIALIZER_CONTEXT_BLUEPRINT in self.context: + self.fields["password"] = CharField(required=False) + + 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.""" + 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() + 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""" + 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() + return instance + 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 ef4088f95..9422fbc79 100644 --- a/blueprints/schema.json +++ b/blueprints/schema.json @@ -8228,6 +8228,11 @@ "type": "string", "minLength": 1, "title": "Path" + }, + "password": { + "type": "string", + "minLength": 1, + "title": "Password" } }, "required": [] diff --git a/website/developer-docs/blueprints/v1/models.md b/website/developer-docs/blueprints/v1/models.md index b6d07cf5d..26ecd03ce 100644 --- a/website/developer-docs/blueprints/v1/models.md +++ b/website/developer-docs/blueprints/v1/models.md @@ -26,6 +26,29 @@ For example: intent: api ``` +### `authentik_core.user` + +:::info +Requires authentik 2023.6 +::: + +Via the standard API, a user's password can only be set via the separate `/api/v3/core/users//set_password/` endpoint. In blueprints, the password of a user can be set using the `password` field. + +Keep in mind that if an LDAPĀ Source is configured and the user maps to an LDAP user, this password change will be propagated to the LDAP server. + +For example: + +```yaml +# [...] +- model: authentik_core.user + state: present + identifiers: + username: test-user + attrs: + name: test user + password: this-should-be-a-long-value +``` + ### `authentik_core.application` :::info