#SPDX-License-Identifier: MIT-0 --- # tasks file for opnform # ===================================================================== # 0. VALIDATION # ===================================================================== - name: Validate required secrets ansible.builtin.assert: that: - opnform_app_key | length > 0 - opnform_app_key is match('^base64:[A-Za-z0-9+/=]+$') - opnform_jwt_secret | length > 0 - opnform_front_api_secret | length > 0 - opnform_db_password | length > 0 fail_msg: >- OpnForm requires opnform_app_key, opnform_jwt_secret, opnform_front_api_secret and opnform_db_password. Generate with: opnform_app_key='base64:'$(openssl rand -base64 32) (the 'base64:' prefix is required); opnform_jwt_secret and opnform_front_api_secret via openssl rand -hex 32. Provide via OpenBao, Ansible Vault or extra-vars. success_msg: Secrets validation passed - name: Validate OIDC configuration when enabled ansible.builtin.assert: that: - opnform_oidc_client_secret | length > 0 - opnform_oidc_domain | length > 0 - opnform_admin_email | length > 0 - opnform_admin_password | length > 0 fail_msg: >- When opnform_oidc_enabled is true, you must set: - opnform_oidc_client_secret - opnform_oidc_domain (email domain that triggers OIDC) - opnform_admin_email / opnform_admin_password (the OIDC API requires an authenticated admin; the role logs in with these credentials to POST the connection) when: opnform_oidc_enabled | bool # ===================================================================== # 1. PREPARATION # ===================================================================== - name: Ensure required packages are installed ansible.builtin.package: name: - python3-docker state: present - name: Create docker compose directory ansible.builtin.file: path: "{{ opnform_docker_compose_dir }}" state: directory mode: '0755' - name: Create OpnForm data directories ansible.builtin.file: path: "{{ item }}" state: directory mode: "0755" loop: - "{{ opnform_docker_volume_dir }}" - "{{ opnform_storage_dir }}" - "{{ opnform_db_data_dir }}" - "{{ opnform_redis_data_dir }}" # ===================================================================== # 2. CONFIGURATION FILES # ===================================================================== - name: Deploy nginx ingress configuration ansible.builtin.template: src: nginx.conf.j2 dest: "{{ opnform_docker_compose_dir }}/nginx.conf" mode: '0644' notify: restart opnform # OIDC_FORCE_LOGIN disables OpnForm's password login — including the # password-based admin/OIDC bootstrap this role performs below. So the # first compose render always keeps force-login OFF; it is switched on # only after the bootstrap completes (see step 7). This keeps a first # deploy on a fresh host working even when opnform_oidc_force_login=true. - name: Render compose with force-login disabled during bootstrap ansible.builtin.set_fact: _opnform_force_login_effective: false - name: Deploy docker-compose file ansible.builtin.template: src: docker-compose.yml.j2 dest: "{{ opnform_docker_compose_dir }}/docker-compose.yml" mode: '0644' notify: restart opnform # ===================================================================== # 3. CONTAINER STARTUP # ===================================================================== - name: Start opnform containers community.docker.docker_compose_v2: project_src: "{{ opnform_docker_compose_dir }}" state: present wait: true wait_timeout: 180 # ===================================================================== # 4. WAIT FOR API READINESS # ===================================================================== - name: Wait for API container to be healthy ansible.builtin.command: cmd: docker inspect --format='{% raw %}{{.State.Health.Status}}{% endraw %}' opnform-api register: api_health until: api_health.stdout == "healthy" retries: 30 delay: 10 changed_when: false # ===================================================================== # 5. ADMIN BOOTSTRAP (optional) # ===================================================================== # Skips the self-hosted setup page by registering the first user via # OpnForm's /api/register endpoint. Idempotent: a successful login # attempt with the same credentials means the user already exists. - name: Check if OpnForm admin user already exists ansible.builtin.uri: url: "https://127.0.0.1/api/login" method: POST headers: Host: "{{ opnform_domain }}" body_format: json body: email: "{{ opnform_admin_email }}" password: "{{ opnform_admin_password }}" status_code: [200, 401, 422] validate_certs: false register: opnform_admin_login when: - opnform_admin_email | length > 0 - opnform_admin_password | length > 0 - name: Create OpnForm admin user via /api/register ansible.builtin.uri: url: "https://127.0.0.1/api/register" method: POST headers: Host: "{{ opnform_domain }}" body_format: json body: name: "{{ opnform_admin_name }}" email: "{{ opnform_admin_email }}" password: "{{ opnform_admin_password }}" password_confirmation: "{{ opnform_admin_password }}" hear_about_us: "{{ opnform_admin_hear_about_us }}" status_code: [200, 201] validate_certs: false no_log: true when: - opnform_admin_email | length > 0 - opnform_admin_password | length > 0 - opnform_admin_login.status != 200 # ===================================================================== # 6. OIDC IDENTITY CONNECTION (optional) # ===================================================================== # Provisions a single OIDC connection on the admin's default workspace. # OpnForm enforces one OIDC connection per workspace, so we GET the # existing connections first and then either POST a new one or PATCH the # existing one to the desired state. PATCHing (rather than skipping when # one exists) keeps inventory changes — e.g. a corrected issuer — applied # on re-runs instead of leaving stale values in the DB forever. - name: Log in as admin to obtain OIDC API token ansible.builtin.uri: url: "https://127.0.0.1/api/login" method: POST headers: Host: "{{ opnform_domain }}" body_format: json body: email: "{{ opnform_admin_email }}" password: "{{ opnform_admin_password }}" status_code: 200 validate_certs: false register: opnform_oidc_token no_log: true when: opnform_oidc_enabled | bool - name: Fetch admin's workspaces ansible.builtin.uri: url: "https://127.0.0.1/api/open/workspaces" method: GET headers: Host: "{{ opnform_domain }}" Authorization: "Bearer {{ opnform_oidc_token.json.token }}" status_code: 200 validate_certs: false register: opnform_workspaces no_log: true when: opnform_oidc_enabled | bool - name: Fetch existing OIDC connections for the default workspace ansible.builtin.uri: url: "https://127.0.0.1/api/open/workspaces/{{ opnform_workspaces.json[0].id }}/oidc-connections" method: GET headers: Host: "{{ opnform_domain }}" Authorization: "Bearer {{ opnform_oidc_token.json.token }}" status_code: 200 validate_certs: false register: opnform_existing_oidc no_log: true when: opnform_oidc_enabled | bool - name: Resolve OIDC group-role mappings ansible.builtin.set_fact: _opnform_oidc_group_role_mappings: >- {{ opnform_oidc_group_role_mappings if (opnform_oidc_group_role_mappings | length > 0) else ([{'idp_group': opnform_oidc_admin_group, 'role': 'admin'}] if (opnform_oidc_admin_group | length > 0) else []) }} when: opnform_oidc_enabled | bool # Desired connection state shared by both the create (POST) and update # (PATCH) calls below. client_secret is always sent: OpnForm's update # endpoint only persists it when present, and on create it is required. - name: Build desired OIDC connection body ansible.builtin.set_fact: _opnform_oidc_body: name: "{{ opnform_oidc_client_name }}" slug: "{{ opnform_oidc_slug }}" domain: "{{ opnform_oidc_domain }}" issuer: "{{ opnform_oidc_issuer }}" client_id: "{{ opnform_oidc_client_id }}" client_secret: "{{ opnform_oidc_client_secret }}" scopes: "{{ opnform_oidc_scopes }}" enabled: true options: require_state: true group_role_mappings: "{{ _opnform_oidc_group_role_mappings }}" no_log: true when: opnform_oidc_enabled | bool - name: Create OIDC identity connection ansible.builtin.uri: url: "https://127.0.0.1/api/open/workspaces/{{ opnform_workspaces.json[0].id }}/oidc-connections" method: POST headers: Host: "{{ opnform_domain }}" Authorization: "Bearer {{ opnform_oidc_token.json.token }}" body_format: json body: "{{ _opnform_oidc_body }}" status_code: [201] validate_certs: false no_log: true when: - opnform_oidc_enabled | bool - opnform_existing_oidc.json | length == 0 # An OIDC connection already exists: PATCH it to the desired state so # inventory changes (e.g. a corrected issuer) are applied. OpnForm allows # exactly one connection per workspace, so the first entry is ours. - name: Update existing OIDC identity connection ansible.builtin.uri: url: >- https://127.0.0.1/api/open/workspaces/{{ opnform_workspaces.json[0].id }}/oidc-connections/{{ opnform_existing_oidc.json[0].id }} method: PATCH headers: Host: "{{ opnform_domain }}" Authorization: "Bearer {{ opnform_oidc_token.json.token }}" body_format: json body: "{{ _opnform_oidc_body }}" status_code: [200] validate_certs: false no_log: true when: - opnform_oidc_enabled | bool - opnform_existing_oidc.json | length > 0 # ===================================================================== # 7. ENABLE FORCE LOGIN (optional, must run last) # ===================================================================== # OIDC_FORCE_LOGIN disables password login — including the password-based # admin/OIDC bootstrap above — so it is switched on only now, after the # connection is provisioned. OpnForm itself only enforces force-login when # an enabled OIDC connection exists, so the order matters: connection # first, force-login second. - name: Enable force login now that the OIDC connection exists when: - opnform_oidc_enabled | bool - opnform_oidc_force_login | bool block: - name: Re-render compose with force-login enabled ansible.builtin.set_fact: _opnform_force_login_effective: true - name: Deploy docker-compose file with force-login enabled ansible.builtin.template: src: docker-compose.yml.j2 dest: "{{ opnform_docker_compose_dir }}/docker-compose.yml" mode: '0644' register: _opnform_force_login_compose - name: Apply force-login by recreating the api containers community.docker.docker_compose_v2: project_src: "{{ opnform_docker_compose_dir }}" state: present wait: true wait_timeout: 180 when: _opnform_force_login_compose is changed - name: Display deployment info ansible.builtin.debug: msg: |- OpnForm deployed at {{ opnform_base_url }} {% if opnform_admin_email | length > 0 %} Admin user bootstrapped: Email: {{ opnform_admin_email }} Password: (from opnform_admin_password) {% else %} No admin bootstrap configured — visit {{ opnform_base_url }} and complete the self-hosted setup page to create the first user. Set opnform_admin_email + opnform_admin_password to automate this. {% endif %} {% if opnform_oidc_enabled %} OIDC: connection "{{ opnform_oidc_client_name }}" bootstrapped (slug: {{ opnform_oidc_slug }}, domain: {{ opnform_oidc_domain }}) Users with @{{ opnform_oidc_domain }} addresses will be redirected to {{ opnform_oidc_issuer }} on login. {% if opnform_oidc_sso_entrypoint %} Direct-SSO entrypoint: {{ opnform_base_url }}{{ opnform_oidc_sso_path }} (link users here to skip the email login form) {% endif %} {% else %} OIDC: disabled (set opnform_oidc_enabled=true to auto-configure) {% endif %}