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
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 a new issue