Repository: goauthentik/authentik

Issue:
Is there a way to load test data in docker?
Context:
I write an app (experiment with Spring boot service). For security I need an auth service. Ideally is to reuse an existing solution like Authentik. During the development phase I do write integration and system tests. Which means I need to run the auth service during those tests. For that a docker-compose was configured which looks to be fine for now.

Question:
Can I configure (or automate) authentik, so during docker start it would load few test users and an app credentials that I can use in tests?

Thanks


Discussion:
You can do that with blueprints, see https://goauthentik.io/developer-docs/blueprints/

for example the blueprint I use for some testing users:

```yaml
# yaml-language-server: $schema=https://goauthentik.io/blueprints/schema.json
version: 1
metadata:
  name: local - dev users & groups
  labels:
    blueprints.goauthentik.io/instantiate: "false"
entries:
  - model: authentik_core.group
    identifiers:
      name: Department foo
    id: group_foo
  - model: authentik_core.group
    identifiers:
      name: Department bar
    id: group_bar
  - model: authentik_core.group
    identifiers:
      name: Department baz
    id: group_baz
  - model: authentik_core.user
    identifiers:
      attributes:
        managed: "user1"
    attrs:
      username: user1
      name: user1
      groups:
        - !KeyOf group_foo
  - model: authentik_core.user
    identifiers:
      attributes:
        managed: "user2"
    attrs:
      username: user2
      name: user2
      groups:
        - !KeyOf group_bar
        - !KeyOf group_foo
  - model: authentik_core.user
    identifiers:
      attributes:
        managed: "user3"
    attrs:
      username: user3
      name: user3
      groups:
        - !KeyOf group_baz
        - !KeyOf group_bar

```
Thank you @BeryJu , Looks good, however is not clear to set the passwords - which seems to be a blocker

Resolving patch:
diff --git a/authentik/blueprints/v1/tasks.py b/authentik/blueprints/v1/tasks.py
--- 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
--- 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
--- a/authentik/core/api/users.py
+++ b/authentik/core/api/users.py
@@ -51,6 +51,7 @@
 
 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] == "/":


Test patch:
diff --git a/authentik/blueprints/tests/fixtures/conditional_fields.yaml b/authentik/blueprints/tests/fixtures/conditional_fields.yaml
--- 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
--- 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 @@ def test_flow(self):
         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))