root: migrate bootstrap to blueprints (#6433)
* remove old bootstrap Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * add meta model to set user password Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * ensure KeyOf works with objects in the state of created that already exist Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * migrate Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add support for shorter form !If tag Signed-off-by: Jens Langhammer <jens@goauthentik.io> * allow !Context to resolve other yaml tags Signed-off-by: Jens Langhammer <jens@goauthentik.io> * don't require serializer to be valid for deleting an object Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix check if a model is being created Signed-off-by: Jens Langhammer <jens@goauthentik.io> * remove duplicate way to set password Signed-off-by: Jens Langhammer <jens@goauthentik.io> * migrate token Signed-off-by: Jens Langhammer <jens@goauthentik.io> * only change what is required with migrations Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add description Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix admin status Signed-off-by: Jens Langhammer <jens@goauthentik.io> * expand tests Signed-off-by: Jens Langhammer <jens@goauthentik.io> * don't require bootstrap in events to fix ci? Signed-off-by: Jens Langhammer <jens@goauthentik.io> --------- Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> Signed-off-by: Jens Langhammer <jens@goauthentik.io>
This commit is contained in:
		
							
								
								
									
										3
									
								
								.vscode/settings.json
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.vscode/settings.json
									
									
									
									
										vendored
									
									
								
							| @ -31,7 +31,8 @@ | ||||
|         "!Format sequence", | ||||
|         "!Condition sequence", | ||||
|         "!Env sequence", | ||||
|         "!Env scalar" | ||||
|         "!Env scalar", | ||||
|         "!If sequence" | ||||
|     ], | ||||
|     "typescript.preferences.importModuleSpecifier": "non-relative", | ||||
|     "typescript.preferences.importModuleSpecifierEnding": "index", | ||||
|  | ||||
| @ -7,7 +7,5 @@ entries: | ||||
|       state: absent | ||||
|     - identifiers: | ||||
|           name: "%(id)s" | ||||
|           expression: | | ||||
|             return True | ||||
|       model: authentik_policies_expression.expressionpolicy | ||||
|       state: absent | ||||
|  | ||||
| @ -9,6 +9,8 @@ context: | ||||
|     mapping: | ||||
|       key1: value | ||||
|       key2: 2 | ||||
|     context1: context-nested-value | ||||
|     context2: !Context context1 | ||||
| entries: | ||||
|     - model: !Format ["%s", authentik_sources_oauth.oauthsource] | ||||
|       state: !Format ["%s", present] | ||||
| @ -97,6 +99,7 @@ entries: | ||||
|                       [list, with, items, !Format ["foo-%s", !Context foo]], | ||||
|                   ] | ||||
|               if_true_simple: !If [!Context foo, true, text] | ||||
|               if_short: !If [!Context foo] | ||||
|               if_false_simple: !If [null, false, 2] | ||||
|               enumerate_mapping_to_mapping: !Enumerate [ | ||||
|                   !Context mapping, | ||||
| @ -141,6 +144,7 @@ entries: | ||||
|                       ] | ||||
|                   ] | ||||
|               ] | ||||
|               nested_context: !Context context2 | ||||
|       identifiers: | ||||
|           name: test | ||||
|       conditions: | ||||
|  | ||||
| @ -155,6 +155,7 @@ class TestBlueprintsV1(TransactionTestCase): | ||||
|                     }, | ||||
|                     "if_false_complex": ["list", "with", "items", "foo-bar"], | ||||
|                     "if_true_simple": True, | ||||
|                     "if_short": True, | ||||
|                     "if_false_simple": 2, | ||||
|                     "enumerate_mapping_to_mapping": { | ||||
|                         "prefix-key1": "other-prefix-value", | ||||
| @ -211,6 +212,7 @@ class TestBlueprintsV1(TransactionTestCase): | ||||
|                             ], | ||||
|                         }, | ||||
|                     }, | ||||
|                     "nested_context": "context-nested-value", | ||||
|                 } | ||||
|             ) | ||||
|         ) | ||||
|  | ||||
| @ -249,6 +249,8 @@ class Context(YAMLTag): | ||||
|         value = self.default | ||||
|         if self.key in blueprint.context: | ||||
|             value = blueprint.context[self.key] | ||||
|         if isinstance(value, YAMLTag): | ||||
|             return value.resolve(entry, blueprint) | ||||
|         return value | ||||
|  | ||||
|  | ||||
| @ -372,8 +374,12 @@ class If(YAMLTag): | ||||
|     def __init__(self, loader: "BlueprintLoader", node: SequenceNode) -> None: | ||||
|         super().__init__() | ||||
|         self.condition = loader.construct_object(node.value[0]) | ||||
|         self.when_true = loader.construct_object(node.value[1]) | ||||
|         self.when_false = loader.construct_object(node.value[2]) | ||||
|         if len(node.value) == 1: | ||||
|             self.when_true = True | ||||
|             self.when_false = False | ||||
|         else: | ||||
|             self.when_true = loader.construct_object(node.value[1]) | ||||
|             self.when_false = loader.construct_object(node.value[2]) | ||||
|  | ||||
|     def resolve(self, entry: BlueprintEntry, blueprint: Blueprint) -> Any: | ||||
|         if isinstance(self.condition, YAMLTag): | ||||
|  | ||||
| @ -199,9 +199,6 @@ class Importer: | ||||
|         serializer_kwargs = {} | ||||
|         model_instance = existing_models.first() | ||||
|         if not isinstance(model(), BaseMetaModel) and model_instance: | ||||
|             if entry.get_state(self.__import) == BlueprintEntryDesiredState.CREATED: | ||||
|                 self.logger.debug("instance exists, skipping") | ||||
|                 return None | ||||
|             self.logger.debug( | ||||
|                 "initialise serializer with instance", | ||||
|                 model=model, | ||||
| @ -268,21 +265,34 @@ class Importer: | ||||
|             try: | ||||
|                 serializer = self._validate_single(entry) | ||||
|             except EntryInvalidError as exc: | ||||
|                 # For deleting objects we don't need the serializer to be valid | ||||
|                 if entry.get_state(self.__import) == BlueprintEntryDesiredState.ABSENT: | ||||
|                     continue | ||||
|                 self.logger.warning(f"entry invalid: {exc}", entry=entry, error=exc) | ||||
|                 return False | ||||
|             if not serializer: | ||||
|                 continue | ||||
|  | ||||
|             state = entry.get_state(self.__import) | ||||
|             if state in [ | ||||
|                 BlueprintEntryDesiredState.PRESENT, | ||||
|                 BlueprintEntryDesiredState.CREATED, | ||||
|             ]: | ||||
|                 model = serializer.save() | ||||
|             if state in [BlueprintEntryDesiredState.PRESENT, BlueprintEntryDesiredState.CREATED]: | ||||
|                 instance = serializer.instance | ||||
|                 if ( | ||||
|                     instance | ||||
|                     and not instance._state.adding | ||||
|                     and state == BlueprintEntryDesiredState.CREATED | ||||
|                 ): | ||||
|                     self.logger.debug( | ||||
|                         "instance exists, skipping", | ||||
|                         model=model, | ||||
|                         instance=instance, | ||||
|                         pk=instance.pk, | ||||
|                     ) | ||||
|                 else: | ||||
|                     instance = serializer.save() | ||||
|                     self.logger.debug("updated model", model=instance) | ||||
|                 if "pk" in entry.identifiers: | ||||
|                     self.__pk_map[entry.identifiers["pk"]] = model.pk | ||||
|                 entry._state = BlueprintEntryState(model) | ||||
|                 self.logger.debug("updated model", model=model) | ||||
|                     self.__pk_map[entry.identifiers["pk"]] = instance.pk | ||||
|                 entry._state = BlueprintEntryState(instance) | ||||
|             elif state == BlueprintEntryDesiredState.ABSENT: | ||||
|                 instance: Optional[Model] = serializer.instance | ||||
|                 if instance.pk: | ||||
| @ -309,5 +319,6 @@ class Importer: | ||||
|                 self.logger.debug("Blueprint validation failed") | ||||
|         for log in logs: | ||||
|             getattr(self.logger, log.get("log_level"))(**log) | ||||
|         self.logger.debug("Finished blueprint import validation") | ||||
|         self.__import = orig_import | ||||
|         return successful, logs | ||||
|  | ||||
| @ -1,55 +1,11 @@ | ||||
| # Generated by Django 3.2.8 on 2021-10-10 16:16 | ||||
|  | ||||
| from os import environ | ||||
|  | ||||
| import django.db.models.deletion | ||||
| from django.apps.registry import Apps | ||||
| from django.conf import settings | ||||
| from django.db import migrations, models | ||||
| from django.db.backends.base.schema import BaseDatabaseSchemaEditor | ||||
|  | ||||
| import authentik.core.models | ||||
|  | ||||
|  | ||||
| def create_default_user(apps: Apps, schema_editor: BaseDatabaseSchemaEditor): | ||||
|     from django.contrib.auth.hashers import make_password | ||||
|  | ||||
|     User = apps.get_model("authentik_core", "User") | ||||
|     db_alias = schema_editor.connection.alias | ||||
|  | ||||
|     akadmin, _ = User.objects.using(db_alias).get_or_create( | ||||
|         username="akadmin", | ||||
|         email=environ.get("AUTHENTIK_BOOTSTRAP_EMAIL", "root@localhost"), | ||||
|         name="authentik Default Admin", | ||||
|     ) | ||||
|     password = None | ||||
|     if "TF_BUILD" in environ or settings.TEST: | ||||
|         password = "akadmin"  # noqa # nosec | ||||
|     if "AUTHENTIK_BOOTSTRAP_PASSWORD" in environ: | ||||
|         password = environ["AUTHENTIK_BOOTSTRAP_PASSWORD"] | ||||
|     if password: | ||||
|         akadmin.password = make_password(password) | ||||
|     else: | ||||
|         akadmin.password = make_password(None) | ||||
|     akadmin.save() | ||||
|  | ||||
|  | ||||
| def create_default_admin_group(apps: Apps, schema_editor: BaseDatabaseSchemaEditor): | ||||
|     db_alias = schema_editor.connection.alias | ||||
|     Group = apps.get_model("authentik_core", "Group") | ||||
|     User = apps.get_model("authentik_core", "User") | ||||
|  | ||||
|     # Creates a default admin group | ||||
|     group, _ = Group.objects.using(db_alias).get_or_create( | ||||
|         is_superuser=True, | ||||
|         defaults={ | ||||
|             "name": "authentik Admins", | ||||
|         }, | ||||
|     ) | ||||
|     group.users.set(User.objects.filter(username="akadmin")) | ||||
|     group.save() | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|     replaces = [ | ||||
|         ("authentik_core", "0002_auto_20200523_1133"), | ||||
| @ -119,9 +75,6 @@ class Migration(migrations.Migration): | ||||
|             model_name="user", | ||||
|             name="is_staff", | ||||
|         ), | ||||
|         migrations.RunPython( | ||||
|             code=create_default_user, | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name="user", | ||||
|             name="is_superuser", | ||||
| @ -201,9 +154,6 @@ class Migration(migrations.Migration): | ||||
|                 default=False, help_text="Users added to this group will be superusers." | ||||
|             ), | ||||
|         ), | ||||
|         migrations.RunPython( | ||||
|             code=create_default_admin_group, | ||||
|         ), | ||||
|         migrations.AlterModelManagers( | ||||
|             name="user", | ||||
|             managers=[ | ||||
|  | ||||
| @ -1,7 +1,6 @@ | ||||
| # Generated by Django 3.2.8 on 2021-10-10 16:12 | ||||
|  | ||||
| import uuid | ||||
| from os import environ | ||||
|  | ||||
| import django.db.models.deletion | ||||
| from django.apps.registry import Apps | ||||
| @ -35,29 +34,6 @@ def fix_duplicates(apps: Apps, schema_editor: BaseDatabaseSchemaEditor): | ||||
|         Token.objects.using(db_alias).filter(identifier=ident["identifier"]).delete() | ||||
|  | ||||
|  | ||||
| def create_default_user_token(apps: Apps, schema_editor: BaseDatabaseSchemaEditor): | ||||
|     from authentik.core.models import TokenIntents | ||||
|  | ||||
|     User = apps.get_model("authentik_core", "User") | ||||
|     Token = apps.get_model("authentik_core", "Token") | ||||
|  | ||||
|     db_alias = schema_editor.connection.alias | ||||
|  | ||||
|     akadmin = User.objects.using(db_alias).filter(username="akadmin") | ||||
|     if not akadmin.exists(): | ||||
|         return | ||||
|     if "AUTHENTIK_BOOTSTRAP_TOKEN" not in environ: | ||||
|         return | ||||
|     key = environ["AUTHENTIK_BOOTSTRAP_TOKEN"] | ||||
|     Token.objects.using(db_alias).create( | ||||
|         identifier="authentik-bootstrap-token", | ||||
|         user=akadmin.first(), | ||||
|         intent=TokenIntents.INTENT_API, | ||||
|         expiring=False, | ||||
|         key=key, | ||||
|     ) | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|     replaces = [ | ||||
|         ("authentik_core", "0018_auto_20210330_1345"), | ||||
| @ -214,9 +190,6 @@ class Migration(migrations.Migration): | ||||
|                 "verbose_name_plural": "Authenticated Sessions", | ||||
|             }, | ||||
|         ), | ||||
|         migrations.RunPython( | ||||
|             code=create_default_user_token, | ||||
|         ), | ||||
|         migrations.AlterField( | ||||
|             model_name="token", | ||||
|             name="intent", | ||||
|  | ||||
| @ -44,7 +44,11 @@ def config_loggers(*args, **kwargs): | ||||
| def after_task_publish_hook(sender=None, headers=None, body=None, **kwargs): | ||||
|     """Log task_id after it was published""" | ||||
|     info = headers if "task" in headers else body | ||||
|     LOGGER.info("Task published", task_id=info.get("id", ""), task_name=info.get("task", "")) | ||||
|     LOGGER.info( | ||||
|         "Task published", | ||||
|         task_id=info.get("id", "").replace("-", ""), | ||||
|         task_name=info.get("task", ""), | ||||
|     ) | ||||
|  | ||||
|  | ||||
| @task_prerun.connect | ||||
| @ -59,7 +63,9 @@ def task_prerun_hook(task_id: str, task, *args, **kwargs): | ||||
| def task_postrun_hook(task_id, task, *args, retval=None, state=None, **kwargs): | ||||
|     """Log task_id on worker""" | ||||
|     CTX_TASK_ID.set(...) | ||||
|     LOGGER.info("Task finished", task_id=task_id, task_name=task.__name__, state=state) | ||||
|     LOGGER.info( | ||||
|         "Task finished", task_id=task_id.replace("-", ""), task_name=task.__name__, state=state | ||||
|     ) | ||||
|  | ||||
|  | ||||
| @task_failure.connect | ||||
|  | ||||
| @ -2,6 +2,12 @@ version: 1 | ||||
| metadata: | ||||
|   name: Default - Events Transport & Rules | ||||
| entries: | ||||
| # Run bootstrap blueprint first to ensure we have the group created | ||||
| - model: authentik_blueprints.metaapplyblueprint | ||||
|   attrs: | ||||
|     identifiers: | ||||
|       path: system/bootstrap.yaml | ||||
|     required: false | ||||
| - model: authentik_events.notificationtransport | ||||
|   id: default-email-transport | ||||
|   attrs: | ||||
| @ -16,6 +22,7 @@ entries: | ||||
|     name: default-local-transport | ||||
| - model: authentik_core.group | ||||
|   id: group | ||||
|   state: created | ||||
|   identifiers: | ||||
|     name: authentik Admins | ||||
|  | ||||
|  | ||||
| @ -1,8 +1,8 @@ | ||||
| version: 1 | ||||
| metadata: | ||||
|   name: Migration - Remove old prompt fields | ||||
|   labels: | ||||
|     blueprints.goauthentik.io/description: Migrate to 2023.2, remove unused prompt fields | ||||
|   name: Migration - Remove old prompt fields | ||||
| entries: | ||||
|   - model: authentik_stages_prompt.prompt | ||||
|     identifiers: | ||||
|  | ||||
							
								
								
									
										49
									
								
								blueprints/system/bootstrap.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								blueprints/system/bootstrap.yaml
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,49 @@ | ||||
| version: 1 | ||||
| metadata: | ||||
|   name: authentik Bootstrap | ||||
|   labels: | ||||
|     blueprints.goauthentik.io/system-bootstrap: "true" | ||||
|     blueprints.goauthentik.io/system: "true" | ||||
|     blueprints.goauthentik.io/description: | | ||||
|       This blueprint configures the default admin user and group, and configures them for the [Automated install](https://goauthentik.io/docs/installation/automated-install). | ||||
| context: | ||||
|   username: akadmin | ||||
|   group_name: authentik Admins | ||||
|   email: !Env [AUTHENTIK_BOOTSTRAP_EMAIL, "root@example.com"] | ||||
|   password: !Env [AUTHENTIK_BOOTSTRAP_PASSWORD, null] | ||||
|   token: !Env [AUTHENTIK_BOOTSTRAP_TOKEN, null] | ||||
| entries: | ||||
|   - model: authentik_core.group | ||||
|     state: created | ||||
|     identifiers: | ||||
|       name: !Context group_name | ||||
|     attrs: | ||||
|       is_superuser: true | ||||
|     id: admin-group | ||||
|   - model: authentik_core.user | ||||
|     state: created | ||||
|     id: admin-user | ||||
|     identifiers: | ||||
|       username: !Context username | ||||
|     attrs: | ||||
|       name: authentik Default Admin | ||||
|       email: !Context email | ||||
|       groups: | ||||
|         - !KeyOf admin-group | ||||
|       password: !Context password | ||||
|   - model: authentik_core.token | ||||
|     state: created | ||||
|     conditions: | ||||
|       - !If [!Context token] | ||||
|     identifiers: | ||||
|       identifier: authentik-bootstrap-token | ||||
|       intent: api | ||||
|       expiring: false | ||||
|       key: !Context token | ||||
|       user: !KeyOf admin-user | ||||
|   - model: authentik_blueprints.blueprintinstance | ||||
|     identifiers: | ||||
|       metadata: | ||||
|         labels: | ||||
|           blueprints.goauthentik.io/system-bootstrap: "true" | ||||
|     state: absent | ||||
| @ -49,7 +49,17 @@ Format a string using python's % formatting. First argument is the format string | ||||
|  | ||||
| Minimal example: | ||||
|  | ||||
| `required: !If [true, true, false] # !If [<condition>, <when true>, <when false>` | ||||
| ```yaml | ||||
| # Short form | ||||
| # !If [<condition>] | ||||
| required: !If [true] | ||||
| ``` | ||||
|  | ||||
| ```yaml | ||||
| # Longer form | ||||
| # !If [<condition>, <when true>, <when false>] | ||||
| required: !If [true, true, false] | ||||
| ``` | ||||
|  | ||||
| Full example: | ||||
|  | ||||
|  | ||||
		Reference in New Issue
	
	Block a user
	 Jens L
					Jens L