blueprints: allow setting user's passwords from blueprints (#5797)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
This commit is contained in:
		| @ -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" | ||||||
|  | |||||||
| @ -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)) | ||||||
|  | |||||||
| @ -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) | ||||||
|  | |||||||
| @ -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.""" | ||||||
|  | |||||||
| @ -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] == "/": | ||||||
|  | |||||||
| @ -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": [] | ||||||
|  | |||||||
| @ -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 | ||||||
|  | |||||||
		Reference in New Issue
	
	Block a user
	 Jens L
					Jens L