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:
parent
5139656e95
commit
10b0c84d97
|
@ -31,7 +31,8 @@
|
||||||
"!Format sequence",
|
"!Format sequence",
|
||||||
"!Condition sequence",
|
"!Condition sequence",
|
||||||
"!Env sequence",
|
"!Env sequence",
|
||||||
"!Env scalar"
|
"!Env scalar",
|
||||||
|
"!If sequence"
|
||||||
],
|
],
|
||||||
"typescript.preferences.importModuleSpecifier": "non-relative",
|
"typescript.preferences.importModuleSpecifier": "non-relative",
|
||||||
"typescript.preferences.importModuleSpecifierEnding": "index",
|
"typescript.preferences.importModuleSpecifierEnding": "index",
|
||||||
|
|
|
@ -7,7 +7,5 @@ entries:
|
||||||
state: absent
|
state: absent
|
||||||
- identifiers:
|
- identifiers:
|
||||||
name: "%(id)s"
|
name: "%(id)s"
|
||||||
expression: |
|
|
||||||
return True
|
|
||||||
model: authentik_policies_expression.expressionpolicy
|
model: authentik_policies_expression.expressionpolicy
|
||||||
state: absent
|
state: absent
|
||||||
|
|
|
@ -9,6 +9,8 @@ context:
|
||||||
mapping:
|
mapping:
|
||||||
key1: value
|
key1: value
|
||||||
key2: 2
|
key2: 2
|
||||||
|
context1: context-nested-value
|
||||||
|
context2: !Context context1
|
||||||
entries:
|
entries:
|
||||||
- model: !Format ["%s", authentik_sources_oauth.oauthsource]
|
- model: !Format ["%s", authentik_sources_oauth.oauthsource]
|
||||||
state: !Format ["%s", present]
|
state: !Format ["%s", present]
|
||||||
|
@ -97,6 +99,7 @@ entries:
|
||||||
[list, with, items, !Format ["foo-%s", !Context foo]],
|
[list, with, items, !Format ["foo-%s", !Context foo]],
|
||||||
]
|
]
|
||||||
if_true_simple: !If [!Context foo, true, text]
|
if_true_simple: !If [!Context foo, true, text]
|
||||||
|
if_short: !If [!Context foo]
|
||||||
if_false_simple: !If [null, false, 2]
|
if_false_simple: !If [null, false, 2]
|
||||||
enumerate_mapping_to_mapping: !Enumerate [
|
enumerate_mapping_to_mapping: !Enumerate [
|
||||||
!Context mapping,
|
!Context mapping,
|
||||||
|
@ -141,6 +144,7 @@ entries:
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
nested_context: !Context context2
|
||||||
identifiers:
|
identifiers:
|
||||||
name: test
|
name: test
|
||||||
conditions:
|
conditions:
|
||||||
|
|
|
@ -155,6 +155,7 @@ class TestBlueprintsV1(TransactionTestCase):
|
||||||
},
|
},
|
||||||
"if_false_complex": ["list", "with", "items", "foo-bar"],
|
"if_false_complex": ["list", "with", "items", "foo-bar"],
|
||||||
"if_true_simple": True,
|
"if_true_simple": True,
|
||||||
|
"if_short": True,
|
||||||
"if_false_simple": 2,
|
"if_false_simple": 2,
|
||||||
"enumerate_mapping_to_mapping": {
|
"enumerate_mapping_to_mapping": {
|
||||||
"prefix-key1": "other-prefix-value",
|
"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
|
value = self.default
|
||||||
if self.key in blueprint.context:
|
if self.key in blueprint.context:
|
||||||
value = blueprint.context[self.key]
|
value = blueprint.context[self.key]
|
||||||
|
if isinstance(value, YAMLTag):
|
||||||
|
return value.resolve(entry, blueprint)
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
@ -372,8 +374,12 @@ class If(YAMLTag):
|
||||||
def __init__(self, loader: "BlueprintLoader", node: SequenceNode) -> None:
|
def __init__(self, loader: "BlueprintLoader", node: SequenceNode) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.condition = loader.construct_object(node.value[0])
|
self.condition = loader.construct_object(node.value[0])
|
||||||
self.when_true = loader.construct_object(node.value[1])
|
if len(node.value) == 1:
|
||||||
self.when_false = loader.construct_object(node.value[2])
|
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:
|
def resolve(self, entry: BlueprintEntry, blueprint: Blueprint) -> Any:
|
||||||
if isinstance(self.condition, YAMLTag):
|
if isinstance(self.condition, YAMLTag):
|
||||||
|
|
|
@ -199,9 +199,6 @@ class Importer:
|
||||||
serializer_kwargs = {}
|
serializer_kwargs = {}
|
||||||
model_instance = existing_models.first()
|
model_instance = existing_models.first()
|
||||||
if not isinstance(model(), BaseMetaModel) and model_instance:
|
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(
|
self.logger.debug(
|
||||||
"initialise serializer with instance",
|
"initialise serializer with instance",
|
||||||
model=model,
|
model=model,
|
||||||
|
@ -268,21 +265,34 @@ class Importer:
|
||||||
try:
|
try:
|
||||||
serializer = self._validate_single(entry)
|
serializer = self._validate_single(entry)
|
||||||
except EntryInvalidError as exc:
|
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)
|
self.logger.warning(f"entry invalid: {exc}", entry=entry, error=exc)
|
||||||
return False
|
return False
|
||||||
if not serializer:
|
if not serializer:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
state = entry.get_state(self.__import)
|
state = entry.get_state(self.__import)
|
||||||
if state in [
|
if state in [BlueprintEntryDesiredState.PRESENT, BlueprintEntryDesiredState.CREATED]:
|
||||||
BlueprintEntryDesiredState.PRESENT,
|
instance = serializer.instance
|
||||||
BlueprintEntryDesiredState.CREATED,
|
if (
|
||||||
]:
|
instance
|
||||||
model = serializer.save()
|
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:
|
if "pk" in entry.identifiers:
|
||||||
self.__pk_map[entry.identifiers["pk"]] = model.pk
|
self.__pk_map[entry.identifiers["pk"]] = instance.pk
|
||||||
entry._state = BlueprintEntryState(model)
|
entry._state = BlueprintEntryState(instance)
|
||||||
self.logger.debug("updated model", model=model)
|
|
||||||
elif state == BlueprintEntryDesiredState.ABSENT:
|
elif state == BlueprintEntryDesiredState.ABSENT:
|
||||||
instance: Optional[Model] = serializer.instance
|
instance: Optional[Model] = serializer.instance
|
||||||
if instance.pk:
|
if instance.pk:
|
||||||
|
@ -309,5 +319,6 @@ class Importer:
|
||||||
self.logger.debug("Blueprint validation failed")
|
self.logger.debug("Blueprint validation failed")
|
||||||
for log in logs:
|
for log in logs:
|
||||||
getattr(self.logger, log.get("log_level"))(**log)
|
getattr(self.logger, log.get("log_level"))(**log)
|
||||||
|
self.logger.debug("Finished blueprint import validation")
|
||||||
self.__import = orig_import
|
self.__import = orig_import
|
||||||
return successful, logs
|
return successful, logs
|
||||||
|
|
|
@ -1,55 +1,11 @@
|
||||||
# Generated by Django 3.2.8 on 2021-10-10 16:16
|
# Generated by Django 3.2.8 on 2021-10-10 16:16
|
||||||
|
|
||||||
from os import environ
|
|
||||||
|
|
||||||
import django.db.models.deletion
|
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 import migrations, models
|
||||||
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
|
|
||||||
|
|
||||||
import authentik.core.models
|
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):
|
class Migration(migrations.Migration):
|
||||||
replaces = [
|
replaces = [
|
||||||
("authentik_core", "0002_auto_20200523_1133"),
|
("authentik_core", "0002_auto_20200523_1133"),
|
||||||
|
@ -119,9 +75,6 @@ class Migration(migrations.Migration):
|
||||||
model_name="user",
|
model_name="user",
|
||||||
name="is_staff",
|
name="is_staff",
|
||||||
),
|
),
|
||||||
migrations.RunPython(
|
|
||||||
code=create_default_user,
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name="user",
|
model_name="user",
|
||||||
name="is_superuser",
|
name="is_superuser",
|
||||||
|
@ -201,9 +154,6 @@ class Migration(migrations.Migration):
|
||||||
default=False, help_text="Users added to this group will be superusers."
|
default=False, help_text="Users added to this group will be superusers."
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
migrations.RunPython(
|
|
||||||
code=create_default_admin_group,
|
|
||||||
),
|
|
||||||
migrations.AlterModelManagers(
|
migrations.AlterModelManagers(
|
||||||
name="user",
|
name="user",
|
||||||
managers=[
|
managers=[
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
# Generated by Django 3.2.8 on 2021-10-10 16:12
|
# Generated by Django 3.2.8 on 2021-10-10 16:12
|
||||||
|
|
||||||
import uuid
|
import uuid
|
||||||
from os import environ
|
|
||||||
|
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
from django.apps.registry import Apps
|
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()
|
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):
|
class Migration(migrations.Migration):
|
||||||
replaces = [
|
replaces = [
|
||||||
("authentik_core", "0018_auto_20210330_1345"),
|
("authentik_core", "0018_auto_20210330_1345"),
|
||||||
|
@ -214,9 +190,6 @@ class Migration(migrations.Migration):
|
||||||
"verbose_name_plural": "Authenticated Sessions",
|
"verbose_name_plural": "Authenticated Sessions",
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
migrations.RunPython(
|
|
||||||
code=create_default_user_token,
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name="token",
|
model_name="token",
|
||||||
name="intent",
|
name="intent",
|
||||||
|
|
|
@ -44,7 +44,11 @@ def config_loggers(*args, **kwargs):
|
||||||
def after_task_publish_hook(sender=None, headers=None, body=None, **kwargs):
|
def after_task_publish_hook(sender=None, headers=None, body=None, **kwargs):
|
||||||
"""Log task_id after it was published"""
|
"""Log task_id after it was published"""
|
||||||
info = headers if "task" in headers else body
|
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
|
@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):
|
def task_postrun_hook(task_id, task, *args, retval=None, state=None, **kwargs):
|
||||||
"""Log task_id on worker"""
|
"""Log task_id on worker"""
|
||||||
CTX_TASK_ID.set(...)
|
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
|
@task_failure.connect
|
||||||
|
|
|
@ -2,6 +2,12 @@ version: 1
|
||||||
metadata:
|
metadata:
|
||||||
name: Default - Events Transport & Rules
|
name: Default - Events Transport & Rules
|
||||||
entries:
|
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
|
- model: authentik_events.notificationtransport
|
||||||
id: default-email-transport
|
id: default-email-transport
|
||||||
attrs:
|
attrs:
|
||||||
|
@ -16,6 +22,7 @@ entries:
|
||||||
name: default-local-transport
|
name: default-local-transport
|
||||||
- model: authentik_core.group
|
- model: authentik_core.group
|
||||||
id: group
|
id: group
|
||||||
|
state: created
|
||||||
identifiers:
|
identifiers:
|
||||||
name: authentik Admins
|
name: authentik Admins
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
version: 1
|
version: 1
|
||||||
metadata:
|
metadata:
|
||||||
|
name: Migration - Remove old prompt fields
|
||||||
labels:
|
labels:
|
||||||
blueprints.goauthentik.io/description: Migrate to 2023.2, remove unused prompt fields
|
blueprints.goauthentik.io/description: Migrate to 2023.2, remove unused prompt fields
|
||||||
name: Migration - Remove old prompt fields
|
|
||||||
entries:
|
entries:
|
||||||
- model: authentik_stages_prompt.prompt
|
- model: authentik_stages_prompt.prompt
|
||||||
identifiers:
|
identifiers:
|
||||||
|
|
|
@ -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:
|
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:
|
Full example:
|
||||||
|
|
||||||
|
|
Reference in New Issue