blueprints: allow setting user's passwords from blueprints (#5797)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
This commit is contained in:
parent
d09bee7bf9
commit
f0619814f9
|
@ -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 a new issue