--- # 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', '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', 'administrator').email }}" firstName: "{{ lookup('users', 'administrator').firstname | replace('.', ' ') | trim }}" lastName: "{{ lookup('users', '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_OWNER_PASSWORD }}" - "{{ N8N_LOCAL_CID }}" # nocheck: container-exec-resolver - node - "-" stdin: "{{ lookup('file', 'javascript/reconcile/owner_password.js') }}" 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', 'administrator').email }}" password: "{{ N8N_OWNER_PASSWORD }}" status_code: [200] return_content: false retries: 0 no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"