blueprints: allow setting user's passwords from blueprints (#5797)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
This commit is contained in:
Jens L 2023-05-29 21:28:44 +02:00 committed by GitHub
parent d09bee7bf9
commit f0619814f9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 80 additions and 15 deletions

View file

@ -11,31 +11,37 @@ metadata:
entries: entries:
- model: authentik_core.token - model: authentik_core.token
identifiers: identifiers:
identifier: %(uid)s-token identifier: "%(uid)s-token"
attrs: attrs:
key: %(uid)s key: "%(uid)s"
user: %(user)s user: "%(user)s"
intent: api intent: api
- model: authentik_core.application - model: authentik_core.application
identifiers: identifiers:
slug: %(uid)s-app slug: "%(uid)s-app"
attrs: attrs:
name: %(uid)s-app name: "%(uid)s-app"
icon: https://goauthentik.io/img/icon.png icon: https://goauthentik.io/img/icon.png
- model: authentik_sources_oauth.oauthsource - model: authentik_sources_oauth.oauthsource
identifiers: identifiers:
slug: %(uid)s-source slug: "%(uid)s-source"
attrs: attrs:
name: %(uid)s-source name: "%(uid)s-source"
provider_type: azuread provider_type: azuread
consumer_key: %(uid)s consumer_key: "%(uid)s"
consumer_secret: %(uid)s consumer_secret: "%(uid)s"
icon: https://goauthentik.io/img/icon.png icon: https://goauthentik.io/img/icon.png
- model: authentik_flows.flow - model: authentik_flows.flow
identifiers: identifiers:
slug: %(uid)s-flow slug: "%(uid)s-flow"
attrs: attrs:
name: %(uid)s-flow name: "%(uid)s-flow"
title: %(uid)s-flow title: "%(uid)s-flow"
designation: authentication designation: authentication
background: https://goauthentik.io/img/icon.png background: https://goauthentik.io/img/icon.png
- model: authentik_core.user
identifiers:
username: "%(uid)s"
attrs:
name: "%(uid)s"
password: "%(uid)s"

View file

@ -2,7 +2,7 @@
from django.test import TransactionTestCase from django.test import TransactionTestCase
from authentik.blueprints.v1.importer import Importer 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.core.tests.utils import create_test_admin_user
from authentik.flows.models import Flow from authentik.flows.models import Flow
from authentik.lib.generators import generate_id from authentik.lib.generators import generate_id
@ -45,3 +45,9 @@ class TestBlueprintsV1ConditionalFields(TransactionTestCase):
flow = Flow.objects.filter(slug=f"{self.uid}-flow").first() flow = Flow.objects.filter(slug=f"{self.uid}-flow").first()
self.assertIsNotNone(flow) self.assertIsNotNone(flow)
self.assertEqual(flow.background, "https://goauthentik.io/img/icon.png") 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))

View file

@ -184,9 +184,9 @@ def apply_blueprint(self: MonitoredTask, instance_pk: str):
instance: Optional[BlueprintInstance] = None instance: Optional[BlueprintInstance] = None
try: try:
instance: BlueprintInstance = BlueprintInstance.objects.filter(pk=instance_pk).first() instance: BlueprintInstance = BlueprintInstance.objects.filter(pk=instance_pk).first()
self.set_uid(slugify(instance.name))
if not instance or not instance.enabled: if not instance or not instance.enabled:
return return
self.set_uid(slugify(instance.name))
blueprint_content = instance.retrieve() blueprint_content = instance.retrieve()
file_hash = sha512(blueprint_content.encode()).hexdigest() file_hash = sha512(blueprint_content.encode()).hexdigest()
importer = Importer(blueprint_content, instance.context) importer = Importer(blueprint_content, instance.context)

View file

@ -33,7 +33,7 @@ class TokenSerializer(ManagedSerializer, ModelSerializer):
def __init__(self, *args, **kwargs) -> None: def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
if SERIALIZER_CONTEXT_BLUEPRINT in self.context: 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]: def validate(self, attrs: dict[Any, str]) -> dict[Any, str]:
"""Ensure only API or App password tokens are created.""" """Ensure only API or App password tokens are created."""

View file

@ -51,6 +51,7 @@ from structlog.stdlib import get_logger
from authentik.admin.api.metrics import CoordinateSerializer from authentik.admin.api.metrics import CoordinateSerializer
from authentik.api.decorators import permission_required 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.used_by import UsedByMixin
from authentik.core.api.utils import LinkSerializer, PassiveSerializer, is_dict from authentik.core.api.utils import LinkSerializer, PassiveSerializer, is_dict
from authentik.core.middleware import ( from authentik.core.middleware import (
@ -112,6 +113,30 @@ class UserSerializer(ModelSerializer):
uid = CharField(read_only=True) uid = CharField(read_only=True)
username = CharField(max_length=150, validators=[UniqueValidator(queryset=User.objects.all())]) 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: 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

@ -8228,6 +8228,11 @@
"type": "string", "type": "string",
"minLength": 1, "minLength": 1,
"title": "Path" "title": "Path"
},
"password": {
"type": "string",
"minLength": 1,
"title": "Password"
} }
}, },
"required": [] "required": []

View file

@ -26,6 +26,29 @@ For example:
intent: api 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/<id>/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` ### `authentik_core.application`
:::info :::info