Files
computer-playbook/roles/web-app-fider/tasks/01_tenant.yml
kevinveenbirkenbachandClaude Opus 5.5 73b21eef98 fix(web-app-fider): bootstrap the tenant without SSO
00_core.yml included 01_setup_oidc.yml only when services.sso.enabled
was true, but that file carried the whole tenant bootstrap: wait for
HTTP, count tenants, POST /_api/tenants, create the admin user and
activate the tenant. Only its last task configured the Keycloak
provider. With SSO off no tenant was ever created.

Fider's RequireTenant middleware then answers every tenant route,
/api/v1/tags included, with 307 to /signup, and /signup is open to any
visitor, who can claim the fresh installation and become its
administrator. The MCP read fider_list_tags got that 307 in the swarm
SSO-off matrix row.

The tenant bootstrap moves unchanged into tasks/01_tenant.yml, which
00_core.yml includes on the stack host regardless of SSO. The OIDC file
becomes tasks/02_setup_oidc.yml and keeps only the provider upsert,
still gated on services.sso.enabled. The README states which bootstrap
steps run without SSO.

A unit test evaluates the include and task guards for every SSO,
stack-host and deploy-mode state: a fresh stack host posts exactly one
tenant with and without SSO, an existing tenant is never posted again,
the Keycloak provider is written only with SSO, and a non-stack host
runs none of it. The compose matrix row with SSO off stayed green
because nothing there read a tenant route.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 23:55:01 +01:00

88 lines
3.7 KiB
YAML

---
- 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: fider
resolve_output_prefix: FIDER
resolve_compose_cid: "{{ FIDER_CONTAINER }}"
- name: "⏳ Wait for {{ application_id }} HTTP endpoint (compose host-port)"
wait_for:
host: "{{ DOCKER_REACH_HOST }}"
port: "{{ lookup('config', application_id, 'services.fider.ports.local.http') }}"
delay: 5
timeout: "{{ lookup('timeout', 300) | int }}"
when: DEPLOYMENT_MODE == 'compose'
- name: "⏳ Wait for {{ application_id }} HTTP endpoint (swarm in-container)"
ansible.builtin.shell: > # nocheck: container-exec-resolver runtime-resolved CID, delegated to the hosting node
container exec {{ FIDER_LOCAL_CID | quote }} ./fider ping
register: fider_http_ready
changed_when: false
retries: 60
delay: 5
until: fider_http_ready.rc == 0
delegate_to: "{{ FIDER_HOST_NODE }}"
when: DEPLOYMENT_MODE == 'swarm'
- name: "🔎 Fider | Check if tenant is already initialized"
database_query:
config: "{{ lookup('database', application_id) }}"
query_file: "{{ role_path }}/files/sql/count-tenants.sql"
expect_rows: true
mask_values: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
register: fider_tenant_count
changed_when: false
- name: "🥾 Fider | Bootstrap initial tenant via API (compose host-port)"
uri_retry:
url: "http://{{ DOCKER_REACH_HOST }}:{{ lookup('config', application_id, 'services.fider.ports.local.http') }}/_api/tenants"
method: POST
body_format: json
body: "{{ FIDER_TENANT_BOOTSTRAP_BODY }}"
status_code: [200, 201]
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
when:
- DEPLOYMENT_MODE == 'compose'
- (fider_tenant_count.rows[0][0] | int) == 0
- name: "🥾 Fider | Bootstrap initial tenant via API (swarm in-container)"
ansible.builtin.shell:
cmd: >- # nocheck: container-exec-resolver runtime-resolved CID, delegated to the hosting node
container exec -i {{ FIDER_LOCAL_CID | quote }}
timeout {{ lookup('timeout', 30) | int }} bash -c
'set -e; body=$(cat); length=$(printf %s "$body" | wc -c);
exec 3<>"/dev/tcp/127.0.0.1/$1" || exit 75;
printf "POST /_api/tenants HTTP/1.0\r\nContent-Type: application/json\r\nContent-Length: %d\r\n\r\n%s" "$length" "$body" >&3;
read -r _ status _ <&3; printf %s "$status";
case $status in 5*) exit 75;; esac' _ {{ FIDER_INTERNAL_PORT | quote }}
stdin: "{{ FIDER_TENANT_BOOTSTRAP_BODY | to_json }}"
register: fider_tenant_bootstrap
changed_when: true
failed_when: fider_tenant_bootstrap.rc != 0 or (fider_tenant_bootstrap.stdout | trim) not in ['200', '201']
retries: 30
delay: 2
until: fider_tenant_bootstrap.rc != 75
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
delegate_to: "{{ FIDER_HOST_NODE }}"
when:
- DEPLOYMENT_MODE == 'swarm'
- (fider_tenant_count.rows[0][0] | int) == 0
- name: "📁 Fider | Create admin user (bypass email verification)"
database_query:
config: "{{ lookup('database', application_id) }}"
query_file: "{{ role_path }}/files/sql/create-admin-user.sql"
named_args:
full_name: "{{ lookup('users', 'administrator').firstname }} {{ lookup('users', 'administrator').lastname }}"
email: "{{ lookup('users', 'administrator').email }}"
mask_values: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
changed_when: true
- name: "🟢 Fider | Activate tenant (set status=1 to bypass email confirmation)"
database_query:
config: "{{ lookup('database', application_id) }}"
query_file: "{{ role_path }}/files/sql/activate-tenants.sql"
mask_values: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
changed_when: true