---
# n8n owner setup: probe whether the owner account exists by attempting a
# single login with the alphanumeric break-glass owner_password. This
# replaces the deprecated /api/v1/owner/pre-setup probe (removed in n8n 1.x,
# always returns 404) and avoids two n8n class-validator false-positives:
# 1. @NoXss() on password: escapeHtml turns special chars (e.g. single quotes)
# to HTML entities, so the system admin password fails the check. Fix: use a
# dedicated alphanumeric owner_password (meta/secrets.yml).
# 2. @NoUrl() on firstName/lastName: regex /(\.[\p{L}\d-]+)/iu flags ANY dot
# followed by letters β including org names like "Infinito.Nexus". Fix:
# replace dots with spaces before sending (see firstName/lastName below).
# Normal users authenticate via SSO/Keycloak; this password is break-glass only.
#
# A single probe avoids triggering n8n's rate limiter. If a retry is still
# needed (e.g. n8n not fully up yet), keep well under n8n's default login
# rate limit of 5 attempts per 5-minute window: delay=30s/retries=4 caps this
# task at 5 attempts total, spanning 120s, so it never crosses the window.
- name: "π Probe n8n owner account via single login attempt"
uri_retry:
url: "{{ N8N_INTERNAL_API_URL }}/rest/login"
method: POST
body_format: json
body:
emailOrLdapLoginId: "{{ lookup('users', 'platform_administrator').email }}"
password: "{{ N8N_OWNER_PASSWORD }}"
status_code: [200, 401, 403]
return_content: false
register: n8n_owner_probe
until: "'json' in (n8n_owner_probe.content_type | default(''))"
retries: 4
delay: 30
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
- name: "π§ Initialise n8n owner account (first-time only)"
uri_retry:
url: "{{ N8N_INTERNAL_API_URL }}/rest/owner/setup"
method: POST
body_format: json
body:
email: "{{ lookup('users', 'platform_administrator').email }}"
firstName: "{{ lookup('users', 'platform_administrator').firstname | replace('.', ' ') | trim }}"
lastName: "{{ lookup('users', 'platform_administrator').lastname | replace('.', ' ') | trim }}"
password: "{{ N8N_OWNER_PASSWORD }}"
status_code: [200, 201, 400]
return_content: true
register: n8n_owner_setup
when: n8n_owner_probe.status != 200
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
- name: "β
Assert n8n owner setup succeeded or was already done"
ansible.builtin.assert:
that:
- >-
(n8n_owner_setup is skipped)
or (n8n_owner_setup.status in [200, 201])
or ('already setup' in (n8n_owner_setup.content | default('')))
fail_msg: "n8n owner setup returned {{ n8n_owner_setup.status | default('n/a') }}: {{ n8n_owner_setup.content | default('') }}"
- name: "π Resolve {{ application_id }} hosting node + container id"
ansible.builtin.include_tasks: "{{ lookup('path_absolute', 'roles/sys-svc-compose/tasks/utils/swarm/resolve_host_cid.yml') }}"
vars:
resolve_service_key: "{{ entity_name }}"
resolve_output_prefix: N8N
resolve_compose_cid: "{{ N8N_CONTAINER }}"
- name: "π Converge a rotated owner password n8n still rejects"
when:
- n8n_owner_probe.status in [401, 403]
- "'already setup' in (n8n_owner_setup.content | default(''))"
block:
- name: "π Write the deployed owner password into n8n's user row"
ansible.builtin.command:
argv:
- container
- exec
- -i
- -e
- "NODE_PATH={{ N8N_NODE_MODULES }}"
- -e
- "N8N_OWNER_EMAIL={{ N8N_OWNER_EMAIL }}"
- -e
- N8N_OWNER_PASSWORD
- "{{ N8N_LOCAL_CID }}" # nocheck: container-exec-resolver
- node
- "-"
stdin: "{{ lookup('file', 'javascript/reconcile/owner_password.js') }}"
environment:
N8N_OWNER_PASSWORD: "{{ N8N_OWNER_PASSWORD }}"
register: n8n_owner_password
retries: 30
delay: 10
until: n8n_owner_password.rc == 0
changed_when: n8n_owner_password.stdout.startswith('CHANGED')
delegate_to: "{{ N8N_HOST_NODE }}"
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
- name: "π Confirm the owner signs in with the deployed password"
uri_retry:
url: "{{ N8N_INTERNAL_API_URL }}/rest/login"
method: POST
body_format: json
body:
emailOrLdapLoginId: "{{ lookup('users', 'platform_administrator').email }}"
password: "{{ N8N_OWNER_PASSWORD }}"
status_code: [200]
return_content: false
retries: 0
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"