From 10b0c84d9700587c7e684f0d051a0446cbc5548f Mon Sep 17 00:00:00 2001 From: Jens L Date: Mon, 31 Jul 2023 19:34:46 +0200 Subject: [PATCH] root: migrate bootstrap to blueprints (#6433) * remove old bootstrap Signed-off-by: Jens Langhammer * add meta model to set user password Signed-off-by: Jens Langhammer * ensure KeyOf works with objects in the state of created that already exist Signed-off-by: Jens Langhammer * migrate Signed-off-by: Jens Langhammer Signed-off-by: Jens Langhammer * add support for shorter form !If tag Signed-off-by: Jens Langhammer * allow !Context to resolve other yaml tags Signed-off-by: Jens Langhammer * don't require serializer to be valid for deleting an object Signed-off-by: Jens Langhammer * fix check if a model is being created Signed-off-by: Jens Langhammer * remove duplicate way to set password Signed-off-by: Jens Langhammer * migrate token Signed-off-by: Jens Langhammer * only change what is required with migrations Signed-off-by: Jens Langhammer * add description Signed-off-by: Jens Langhammer * fix admin status Signed-off-by: Jens Langhammer * expand tests Signed-off-by: Jens Langhammer * don't require bootstrap in events to fix ci? Signed-off-by: Jens Langhammer --------- Signed-off-by: Jens Langhammer Signed-off-by: Jens Langhammer --- .vscode/settings.json | 3 +- .../tests/fixtures/state_absent.yaml | 2 - authentik/blueprints/tests/fixtures/tags.yaml | 4 ++ authentik/blueprints/tests/test_v1.py | 2 + authentik/blueprints/v1/common.py | 10 +++- authentik/blueprints/v1/importer.py | 33 ++++++++---- ...3_1133_squashed_0011_provider_name_temp.py | 50 ------------------- ...0_1345_squashed_0028_alter_token_intent.py | 27 ---------- authentik/root/celery.py | 10 +++- blueprints/default/events-default.yaml | 7 +++ .../migrations/migrate-prompt-name.yaml | 2 +- blueprints/system/bootstrap.yaml | 49 ++++++++++++++++++ website/developer-docs/blueprints/v1/tags.md | 12 ++++- 13 files changed, 114 insertions(+), 97 deletions(-) create mode 100644 blueprints/system/bootstrap.yaml diff --git a/.vscode/settings.json b/.vscode/settings.json index 410a3bd5f..e674c02b5 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -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", diff --git a/authentik/blueprints/tests/fixtures/state_absent.yaml b/authentik/blueprints/tests/fixtures/state_absent.yaml index c75d9e3df..6e7141daa 100644 --- a/authentik/blueprints/tests/fixtures/state_absent.yaml +++ b/authentik/blueprints/tests/fixtures/state_absent.yaml @@ -7,7 +7,5 @@ entries: state: absent - identifiers: name: "%(id)s" - expression: | - return True model: authentik_policies_expression.expressionpolicy state: absent diff --git a/authentik/blueprints/tests/fixtures/tags.yaml b/authentik/blueprints/tests/fixtures/tags.yaml index a57e207dd..16785ee78 100644 --- a/authentik/blueprints/tests/fixtures/tags.yaml +++ b/authentik/blueprints/tests/fixtures/tags.yaml @@ -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: diff --git a/authentik/blueprints/tests/test_v1.py b/authentik/blueprints/tests/test_v1.py index 4679de909..c5136a1ba 100644 --- a/authentik/blueprints/tests/test_v1.py +++ b/authentik/blueprints/tests/test_v1.py @@ -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", } ) ) diff --git a/authentik/blueprints/v1/common.py b/authentik/blueprints/v1/common.py index 3a75129fe..9eda93300 100644 --- a/authentik/blueprints/v1/common.py +++ b/authentik/blueprints/v1/common.py @@ -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): diff --git a/authentik/blueprints/v1/importer.py b/authentik/blueprints/v1/importer.py index 7dcfbe517..e9b29938e 100644 --- a/authentik/blueprints/v1/importer.py +++ b/authentik/blueprints/v1/importer.py @@ -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 diff --git a/authentik/core/migrations/0002_auto_20200523_1133_squashed_0011_provider_name_temp.py b/authentik/core/migrations/0002_auto_20200523_1133_squashed_0011_provider_name_temp.py index c65edfa7c..6b629122c 100644 --- a/authentik/core/migrations/0002_auto_20200523_1133_squashed_0011_provider_name_temp.py +++ b/authentik/core/migrations/0002_auto_20200523_1133_squashed_0011_provider_name_temp.py @@ -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=[ diff --git a/authentik/core/migrations/0018_auto_20210330_1345_squashed_0028_alter_token_intent.py b/authentik/core/migrations/0018_auto_20210330_1345_squashed_0028_alter_token_intent.py index 0b276a79c..353aefa56 100644 --- a/authentik/core/migrations/0018_auto_20210330_1345_squashed_0028_alter_token_intent.py +++ b/authentik/core/migrations/0018_auto_20210330_1345_squashed_0028_alter_token_intent.py @@ -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", diff --git a/authentik/root/celery.py b/authentik/root/celery.py index e9293b58d..2747bae45 100644 --- a/authentik/root/celery.py +++ b/authentik/root/celery.py @@ -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 diff --git a/blueprints/default/events-default.yaml b/blueprints/default/events-default.yaml index a5bd390f7..205c2b990 100644 --- a/blueprints/default/events-default.yaml +++ b/blueprints/default/events-default.yaml @@ -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 diff --git a/blueprints/migrations/migrate-prompt-name.yaml b/blueprints/migrations/migrate-prompt-name.yaml index 65724df11..05955e1d0 100644 --- a/blueprints/migrations/migrate-prompt-name.yaml +++ b/blueprints/migrations/migrate-prompt-name.yaml @@ -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: diff --git a/blueprints/system/bootstrap.yaml b/blueprints/system/bootstrap.yaml new file mode 100644 index 000000000..049caea45 --- /dev/null +++ b/blueprints/system/bootstrap.yaml @@ -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 diff --git a/website/developer-docs/blueprints/v1/tags.md b/website/developer-docs/blueprints/v1/tags.md index 91e2c47c7..79edbdca3 100644 --- a/website/developer-docs/blueprints/v1/tags.md +++ b/website/developer-docs/blueprints/v1/tags.md @@ -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 [, , ` +```yaml +# Short form +# !If [] +required: !If [true] +``` + +```yaml +# Longer form +# !If [, , ] +required: !If [true, true, false] +``` Full example: