diff --git a/README.md b/README.md index e654ff6..4e26fba 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,7 @@ * `abra app secret generate -a ` * `abra app deploy ` * `abra app cmd app set_admin_pass` +* `abra app cmd worker apply_blueprints` ## Rotate Secrets @@ -60,4 +61,46 @@ Run this command after every deploy/upgrade: `abra app command --local customize ` +## Blueprints + +Blueprint Dependency Requirements: + +- Recovery with email verification + - Default - Password change flow + - Default - Authentication flow +- Custom Authentication Flow + - Default - Authentication flow + - Recovery with email verification +- Invitation Enrollment Flow + - Default - User settings flow + - Default - Authentication flow + - Default - Source enrollment flow +- Custom Invalidation Flow + - Default - Invalidation flow +- Flow Translations + - Recovery with email verification + - Default - Password change flow + - Default - User settings flow + - Default - Source enrollment flow +- Custom System Tenant + - Default - Tenant + - Recovery with email verification + + +Blueprint Dependency Graph: + +5. Custom System Tenant + - Default - Tenant + 4. Invitation Enrollment Flow + 3. Flow Translations + - Default - User settings flow + - Default - Source enrollment flow + 2. Custom Authentication Flow + 1. Recovery with email verification + - Default - Authentication flow + - Default - Password change flow +6. Custom Invalidation Flow + - Default - Invalidation flow + + For more, see [`docs.coopcloud.tech`](https://docs.coopcloud.tech). diff --git a/abra.sh b/abra.sh index ce810d9..8ff4ebd 100644 --- a/abra.sh +++ b/abra.sh @@ -1,5 +1,10 @@ export CUSTOM_CSS_VERSION=v2 -export CUSTOM_FLOWS_VERSION=v5 +export FLOW_AUTHENTICATION_VERSION=v1 +export FLOW_INVITATION_VERSION=v1 +export FLOW_INVALIDATION_VERSION=v1 +export FLOW_RECOVERY_VERSION=v1 +export FLOW_TRANSLATION_VERSION=v1 +export SYSTEM_TENANT_VERSION=v1 customize() { if [ -z "$1" ] @@ -47,3 +52,56 @@ rotate_db_pass() { db_password=$(cat /run/secrets/db_password) psql -U authentik -c """ALTER USER authentik WITH PASSWORD '$db_password';""" } + +apply_blueprints() { + enable_blueprint default/flow-default-authentication-flow.yaml + enable_blueprint default/flow-default-user-settings-flow.yaml + enable_blueprint default/flow-password-change.yaml + ak apply_blueprint 6_flow_invalidation.yaml + ak apply_blueprint 5_system_tenant.yaml + disable_blueprint default/flow-default-authentication-flow.yaml + disable_blueprint default/flow-default-user-settings-flow.yaml + disable_blueprint default/flow-password-change.yaml +} + +disable_blueprint() { + blueprint_state False $@ +} + +enable_blueprint() { + blueprint_state True $@ +} + +blueprint_state() { +TOKEN=$(cat /run/secrets/admin_token) +python -c """ +import requests +session = requests.Session() +my_token='$TOKEN' +blueprint_state=$1 +blueprint_path='$2' +resp = session.get(f'https://$DOMAIN/api/v3/managed/blueprints/?path={blueprint_path}', headers={'Authorization':f'Bearer {my_token}'}) +if not resp.ok: + print(f'Error fetching blueprint: {resp.content}') + exit() +auth_flow_uuid = resp.json()['results'][0]['pk'] +blueprint_name = resp.json()['results'][0]['name'] +params = {'name': blueprint_name,'path': blueprint_path,'context':{},'enabled': blueprint_state} +resp = session.put(f'https://$DOMAIN/api/v3/managed/blueprints/{auth_flow_uuid}/', json=params, headers={'Authorization':f'Bearer {my_token}'}) +if resp.ok: + print(f'{blueprint_name} enabled: {blueprint_state}') +else: + print(f'Error changing blueprint state: {resp.content}') +""" + +} + +blueprint_cleanup() { +/manage.py shell -c """ +delete_flows = ['default-recovery-flow' , 'custom-authentication-flow' , 'invitation-enrollment-flow' , 'initial-setup'] +Flow.objects.filter(slug__in=delete_flows).delete() +Stage.objects.filter(flow=None).delete() +Prompt.objects.filter(promptstage=None).delete() +""" +apply_blueprints +} diff --git a/compose.yml b/compose.yml index 4c478f7..9d190a2 100644 --- a/compose.yml +++ b/compose.yml @@ -91,11 +91,20 @@ services: - media:/media - /var/run/docker.sock:/var/run/docker.sock - custom-templates:/templates - - /dev/null:/blueprints/default/flow-default-authentication-flow.yaml - - /dev/null:/blueprints/default/flow-default-invalidation-flow.yaml + - /dev/null:/blueprints/default/flow-oobe.yaml configs: - - source: custom_flows - target: /blueprints/custom_flows.yaml + - source: flow_recovery + target: /blueprints/1_flow_recovery.yaml + - source: flow_authentication + target: /blueprints/2_flow_authentication.yaml + - source: flow_translation + target: /blueprints/3_flow_translation.yaml + - source: flow_invitation + target: /blueprints/4_flow_invitation.yaml + - source: system_tenant + target: /blueprints/5_system_tenant.yaml + - source: flow_invalidation + target: /blueprints/6_flow_invalidation.yaml environment: *env db: @@ -167,7 +176,27 @@ configs: name: ${STACK_NAME}_custom_css_${CUSTOM_CSS_VERSION} file: custom.css.tmpl template_driver: golang - custom_flows: - name: ${STACK_NAME}_custom_flows_${CUSTOM_FLOWS_VERSION} - file: custom_flows.yaml.tmpl + flow_authentication: + name: ${STACK_NAME}_flow_authentication_${FLOW_AUTHENTICATION_VERSION} + file: flow_authentication.yaml.tmpl + template_driver: golang + flow_invitation: + name: ${STACK_NAME}_flow_invitation_${FLOW_INVITATION_VERSION} + file: flow_invitation.yaml.tmpl + template_driver: golang + flow_invalidation: + name: ${STACK_NAME}_flow_invalidation_${FLOW_INVALIDATION_VERSION} + file: flow_invalidation.yaml.tmpl + template_driver: golang + flow_recovery: + name: ${STACK_NAME}_flow_recovery_${FLOW_RECOVERY_VERSION} + file: flow_recovery.yaml.tmpl + template_driver: golang + flow_translation: + name: ${STACK_NAME}_flow_translation_${FLOW_TRANSLATION_VERSION} + file: flow_translation.yaml.tmpl + template_driver: golang + system_tenant: + name: ${STACK_NAME}_system_tenant_${SYSTEM_TENANT_VERSION} + file: system_tenant.yaml.tmpl template_driver: golang diff --git a/flow_authentication.yaml.tmpl b/flow_authentication.yaml.tmpl new file mode 100644 index 0000000..979a25c --- /dev/null +++ b/flow_authentication.yaml.tmpl @@ -0,0 +1,45 @@ +version: 1 +metadata: + labels: + blueprints.goauthentik.io/instantiate: "true" + name: Custom Authentication Flow +context: + welcome_message: {{ if eq (env "WELCOME_MESSAGE") "" }} "Welcome to authentik!" {{ else }} {{ env "WELCOME_MESSAGE" }} {{ end }} + +entries: +### DEPENDENCIES +- model: authentik_blueprints.metaapplyblueprint + attrs: + identifiers: + name: Recovery with email verification + required: true + +### FLOW +- model: authentik_flows.flow + identifiers: + slug: default-authentication-flow + id: flow + attrs: + name: !Context welcome_message + title: !Context welcome_message + +### STAGES +- identifiers: + name: default-authentication-identification + model: authentik_stages_identification.identificationstage + attrs: + password_stage: !Find [authentik_stages_password.passwordstage, [name, default-authentication-password]] + recovery_flow: !Find [authentik_flows.flow, [slug, default-recovery-flow]] + +- identifiers: + name: default-authentication-login + model: authentik_stages_user_login.userloginstage + attrs: + session_duration: seconds=0 + +- identifiers: + order: 20 + stage: !Find [authentik_stages_password.passwordstage, [name, default-authentication-password]] + target: !KeyOf flow + model: authentik_flows.flowstagebinding + state: absent diff --git a/flow_invalidation.yaml.tmpl b/flow_invalidation.yaml.tmpl new file mode 100644 index 0000000..8a13332 --- /dev/null +++ b/flow_invalidation.yaml.tmpl @@ -0,0 +1,44 @@ +version: 1 +metadata: + labels: + blueprints.goauthentik.io/instantiate: "true" + name: Custom Invalidation Flow +entries: +### DEPENDENCIES +- model: authentik_blueprints.metaapplyblueprint + attrs: + identifiers: + name: Default - Invalidation flow + required: true + +### STAGE BINDINGS + +- identifiers: + order: 0 + stage: !Find [authentik_stages_user_logout.userlogoutstage, [name, default-invalidation-logout]] + target: !Find [authentik_flows.flow, [slug, default-invalidation-flow]] + model: authentik_flows.flowstagebinding + attrs: + re_evaluate_policies: true + id: logout-stage-binding + +### POLICIES +- attrs: + execution_logging: true + expression: 'context[''flow_plan''].context[''redirect''] = ''{{ env "LOGOUT_REDIRECT" }}'' + + return True' + identifiers: + name: redirect-policy + id: redirect-policy + model: authentik_policies_expression.expressionpolicy + +### POLICY BINDINGS +- identifiers: + policy: !KeyOf redirect-policy + target: !KeyOf logout-stage-binding + order: 0 + model: authentik_policies.policybinding + attrs: + enabled: {{ if eq (env "LOGOUT_REDIRECT") "" }} false {{ else }} true {{ end }} + timeout: 30 diff --git a/flow_invitation.yaml.tmpl b/flow_invitation.yaml.tmpl new file mode 100644 index 0000000..d3f6240 --- /dev/null +++ b/flow_invitation.yaml.tmpl @@ -0,0 +1,65 @@ +version: 1 +metadata: + labels: + blueprints.goauthentik.io/instantiate: "true" + name: Invitation Enrollment Flow +context: + welcome_message: {{ if eq (env "WELCOME_MESSAGE") "" }} "Welcome to authentik!" {{ else }} {{ env "WELCOME_MESSAGE" }} {{ end }} + +entries: +### DEPENDENCIES +- model: authentik_blueprints.metaapplyblueprint + attrs: + identifiers: + name: Flow Translations + required: true + +### FLOW +- attrs: + designation: enrollment + name: invitation-enrollment-flow + title: !Context welcome_message + identifiers: + slug: invitation-enrollment-flow + id: invitation-enrollment-flow + model: authentik_flows.flow + +### STAGES +- identifiers: + name: invitation-stage + id: invitation-stage + model: authentik_stages_invitation.invitationstage + +- identifiers: + name: enrollment-prompt-userdata + id: enrollment-prompt-userdata + model: authentik_stages_prompt.promptstage + attrs: + fields: + - !Find [authentik_stages_prompt.prompt, [name, default-source-enrollment-field-username]] + - !Find [authentik_stages_prompt.prompt, [name, default-user-settings-field-name]] + - !Find [authentik_stages_prompt.prompt, [name, default-user-settings-field-email]] + - !Find [authentik_stages_prompt.prompt, [name, default-password-change-field-password]] + - !Find [authentik_stages_prompt.prompt, [name, default-password-change-field-password-repeat]] + +### STAGE BINDINGS +- identifiers: + order: 1 + stage: !KeyOf invitation-stage + target: !KeyOf invitation-enrollment-flow + model: authentik_flows.flowstagebinding +- identifiers: + order: 10 + stage: !KeyOf enrollment-prompt-userdata + target: !KeyOf invitation-enrollment-flow + model: authentik_flows.flowstagebinding +- identifiers: + order: 20 + stage: !Find [authentik_stages_user_write.userwritestage, [name, default-source-enrollment-write]] + target: !KeyOf invitation-enrollment-flow + model: authentik_flows.flowstagebinding +- identifiers: + order: 100 + stage: !Find [authentik_stages_user_login.userloginstage, [name, default-authentication-login]] + target: !KeyOf invitation-enrollment-flow + model: authentik_flows.flowstagebinding diff --git a/flow_recovery.yaml.tmpl b/flow_recovery.yaml.tmpl new file mode 100644 index 0000000..2a62ef9 --- /dev/null +++ b/flow_recovery.yaml.tmpl @@ -0,0 +1,128 @@ +version: 1 +metadata: + labels: + blueprints.goauthentik.io/instantiate: "true" + name: Recovery with email verification +context: + token_expiry: {{ if eq (env "EMAIL_TOKEN_EXPIRY_MINUTES") "" }} 30 {{ else }} {{ env "EMAIL_TOKEN_EXPIRY_MINUTES" }} {{ end }} + subject: {{ if eq (env "EMAIL_SUBJECT") "" }} Account Recovery {{ else }} {{ env "EMAIL_SUBJECT" }} {{ end }} +entries: +### DEPENDENCIES +- model: authentik_blueprints.metaapplyblueprint + attrs: + identifiers: + name: Default - Authentication flow + required: true + +### FLOW +- identifiers: + slug: default-recovery-flow + model: authentik_flows.flow + state: created + attrs: + name: Default recovery flow + title: Reset your password + designation: recovery + authentication: require_unauthenticated + +### STAGES +- identifiers: + name: default-recovery-email + id: default-recovery-email + model: authentik_stages_email.emailstage + attrs: + use_global_settings: true + token_expiry: !Context token_expiry + subject: !Context subject + template: email/password_reset.html + activate_user_on_success: true +- identifiers: + name: default-recovery-identification + id: default-recovery-identification + model: authentik_stages_identification.identificationstage + attrs: + user_fields: + - email + - username + +### STAGE BINDINGS +- identifiers: + target: !Find [authentik_flows.flow, [slug, default-recovery-flow]] + stage: !KeyOf default-recovery-identification + order: 10 + model: authentik_flows.flowstagebinding + id: flow-binding-identification + attrs: + evaluate_on_plan: true + re_evaluate_policies: true + policy_engine_mode: any + invalid_response_action: retry +- identifiers: + target: !Find [authentik_flows.flow, [slug, default-recovery-flow]] + stage: !KeyOf default-recovery-email + order: 20 + model: authentik_flows.flowstagebinding + id: flow-binding-email + attrs: + evaluate_on_plan: true + re_evaluate_policies: true + policy_engine_mode: any + invalid_response_action: retry +- identifiers: + target: !Find [authentik_flows.flow, [slug, default-recovery-flow]] + stage: !Find [authentik_stages_prompt.promptstage, [name, default-password-change-prompt]] + order: 30 + model: authentik_flows.flowstagebinding + attrs: + evaluate_on_plan: true + re_evaluate_policies: false + policy_engine_mode: any + invalid_response_action: retry +- identifiers: + target: !Find [authentik_flows.flow, [slug, default-recovery-flow]] + stage: !Find [authentik_stages_user_write.userwritestage, [name, default-password-change-write]] + order: 40 + model: authentik_flows.flowstagebinding + attrs: + evaluate_on_plan: true + re_evaluate_policies: false + policy_engine_mode: any + invalid_response_action: retry +- identifiers: + target: !Find [authentik_flows.flow, [slug, default-recovery-flow]] + stage: !Find [authentik_stages_user_login.userloginstage, [name, default-authentication-login]] + order: 100 + model: authentik_flows.flowstagebinding + attrs: + evaluate_on_plan: true + re_evaluate_policies: false + policy_engine_mode: any + invalid_response_action: retry + +### POLICIES +- identifiers: + name: default-recovery-skip-if-restored + id: default-recovery-skip-if-restored + model: authentik_policies_expression.expressionpolicy + attrs: + expression: | + return request.context.get('is_restored', False) +- identifiers: + policy: !KeyOf default-recovery-skip-if-restored + target: !KeyOf flow-binding-identification + order: 0 + model: authentik_policies.policybinding + attrs: + negate: false + enabled: false # TODO: why does this doesn't work? + timeout: 30 +- identifiers: + policy: !KeyOf default-recovery-skip-if-restored + target: !KeyOf flow-binding-email + order: 0 + state: absent + model: authentik_policies.policybinding + attrs: + negate: false + enabled: true + timeout: 30 diff --git a/flow_translation.yaml.tmpl b/flow_translation.yaml.tmpl new file mode 100644 index 0000000..1627263 --- /dev/null +++ b/flow_translation.yaml.tmpl @@ -0,0 +1,71 @@ +version: 1 +metadata: + labels: + blueprints.goauthentik.io/instantiate: "true" + name: Flow Translations +context: + transl_recovery: {{ if eq (env "DEFAULT_LANGUAGE") "de" }} "Passwort Zurücksetzen" {{ else }} "Reset your password" {{ end }} + transl_password: {{ if eq (env "DEFAULT_LANGUAGE") "de" }} "Passwort" {{ else }} "Password" {{ end }} + transl_password_repeat: {{ if eq (env "DEFAULT_LANGUAGE") "de" }} "Passwort (wiederholen)" {{ else }} "Password (repeat)" {{ end }} + transl_username: {{ if eq (env "DEFAULT_LANGUAGE") "de" }} "Benutzername" {{ else }} "Username" {{ end }} + transl_name: {{ if eq (env "DEFAULT_LANGUAGE") "de" }} "Vor- und Nachname" {{ else }} "Full name" {{ end }} + +entries: +### DEPENDENCIES +- model: authentik_blueprints.metaapplyblueprint + attrs: + identifiers: + name: Custom Authentication Flow + required: true +- model: authentik_blueprints.metaapplyblueprint + attrs: + identifiers: + name: Default - User settings flow + required: true +- model: authentik_blueprints.metaapplyblueprint + attrs: + identifiers: + name: Default - Source enrollment flow + required: true + +### FLOWS +- model: authentik_flows.flow + identifiers: + slug: default-recovery-flow + id: recovery_flow + model: authentik_flows.flow + attrs: + name: Default recovery flow + title: !Context transl_recovery + designation: recovery + + +### PROMPTS +- model: authentik_stages_prompt.prompt + identifiers: + name: default-password-change-field-password + attrs: + label: !Context transl_password + placeholder: !Context transl_password +- model: authentik_stages_prompt.prompt + identifiers: + name: default-password-change-field-password-repeat + attrs: + label: !Context transl_password_repeat + placeholder: !Context transl_password_repeat +- model: authentik_stages_prompt.prompt + identifiers: + name: default-user-settings-field-username + attrs: + label: !Context transl_username +- model: authentik_stages_prompt.prompt + identifiers: + name: default-user-settings-field-name + attrs: + label: !Context transl_name +- model: authentik_stages_prompt.prompt + identifiers: + name: default-source-enrollment-field-username + attrs: + label: !Context transl_username + placeholder: !Context transl_username diff --git a/release/next b/release/next new file mode 100644 index 0000000..d48cd21 --- /dev/null +++ b/release/next @@ -0,0 +1 @@ +Run `abra app cmd worker blueprint_cleanup` to apply the new blueprint configuration and delete the old configuration. diff --git a/system_tenant.yaml.tmpl b/system_tenant.yaml.tmpl new file mode 100644 index 0000000..f941788 --- /dev/null +++ b/system_tenant.yaml.tmpl @@ -0,0 +1,35 @@ +version: 1 +metadata: + labels: + blueprints.goauthentik.io/instantiate: "true" + name: Custom System Tenant +entries: +### DEPENDENCIES +- model: authentik_blueprints.metaapplyblueprint + attrs: + identifiers: + name: Default - Tenant + required: true +- model: authentik_blueprints.metaapplyblueprint + attrs: + identifiers: + name: Invitation Enrollment Flow + required: true + + +### SYSTEM TENANT +# remove custom tenant from old recipe +- identifiers: + domain: {{ env "DOMAIN" }} + model: authentik_tenants.tenant + state: absent + +- attrs: + attributes: + settings: + locale: {{ if eq (env "DEFAULT_LANGUAGE") "" }} en {{ else }} {{ env "DEFAULT_LANGUAGE" }} {{ end }} + flow_recovery: !Find [authentik_flows.flow, [slug, default-recovery-flow]] + identifiers: + default: true + domain: authentik-default + model: authentik_tenants.tenant