--- # Point Stalwart's auth directory at Keycloak. The directory is Internal XOR one # external directory (no fallback), so SSO switches all interactive auth to OIDC # and disables password login — see the role README for the full rationale. # JMAP request bodies live in templates/jmap/*.json.j2. - name: "📖 List existing Stalwart directories" include_tasks: jmap_call.yml vars: stalwart_jmap_body: "{{ lookup('template', 'jmap/directory/get.json.j2') }}" - name: "🔍 Resolve existing Keycloak OIDC directory id (if any)" ansible.builtin.set_fact: stalwart_oidc_dir_id: >- {{ (stalwart_jmap.methodResponses[0][1].list | selectattr('@type', 'equalto', 'Oidc') | selectattr('issuerUrl', 'equalto', OIDC.CLIENT.ISSUER_URL) | map(attribute='id') | first) | default('') }} - name: "📁 Create the Keycloak OIDC authentication directory" include_tasks: jmap_call.yml vars: stalwart_jmap_body: "{{ lookup('template', 'jmap/directory/create_oidc.json.j2') }}" when: stalwart_oidc_dir_id | length == 0 - name: "📖 Capture the newly created OIDC directory id" ansible.builtin.set_fact: stalwart_oidc_dir_id: "{{ stalwart_jmap.methodResponses[0][1].created.kc.id }}" when: stalwart_oidc_dir_id | length == 0 - name: "🔧 Accept Keycloak tokens without a Stalwart audience or mail scope" include_tasks: jmap_call.yml vars: stalwart_jmap_body: "{{ lookup('template', 'jmap/directory/token_validation.json.j2') }}" when: stalwart_oidc_dir_id | length > 0 - name: "📖 List Stalwart built-in roles" include_tasks: jmap_call.yml vars: stalwart_jmap_body: "{{ lookup('template', 'jmap/role_get.json.j2') }}" - name: "🔍 Resolve built-in Stalwart role ids by description" ansible.builtin.set_fact: stalwart_role_user: "{{ stalwart_jmap.methodResponses[0][1].list | selectattr('description', 'equalto', 'User') | map(attribute='id') | first | default('') }}" stalwart_role_group: "{{ stalwart_jmap.methodResponses[0][1].list | selectattr('description', 'equalto', 'Group') | map(attribute='id') | first | default('') }}" stalwart_role_tenant: "{{ stalwart_jmap.methodResponses[0][1].list | selectattr('description', 'equalto', 'Tenant Administrator') | map(attribute='id') | first | default('') }}" stalwart_role_admin: "{{ stalwart_jmap.methodResponses[0][1].list | selectattr('description', 'equalto', 'System Administrator') | map(attribute='id') | first | default('') }}" - name: "✅ Ensure the built-in Stalwart 'User' role exists (grants mailbox permissions)" ansible.builtin.assert: that: stalwart_role_user | length > 0 fail_msg: "Stalwart built-in 'User' role not found; cannot grant IMAP/SMTP mailbox permissions." - name: "🔧 Assemble default role-id maps for the authentication singleton" ansible.builtin.set_fact: stalwart_auth_defaults: directoryId: "{{ stalwart_oidc_dir_id }}" defaultUserRoleIds: "{{ {stalwart_role_user: true} }}" defaultGroupRoleIds: "{{ {stalwart_role_group: true} if stalwart_role_group | length > 0 else {stalwart_role_user: true} }}" defaultTenantRoleIds: "{{ {stalwart_role_tenant: true} | combine({stalwart_role_user: true}) if stalwart_role_tenant | length > 0 else {stalwart_role_user: true} }}" defaultAdminRoleIds: "{{ {stalwart_role_admin: true} | combine({stalwart_role_user: true}) if stalwart_role_admin | length > 0 else {stalwart_role_user: true} }}" - name: "📖 Read the Stalwart authentication singleton" include_tasks: jmap_call.yml vars: stalwart_jmap_body: "{{ lookup('template', 'jmap/authentication/get.json.j2') }}" - name: "🔧 Record whether the authentication singleton exists" ansible.builtin.set_fact: stalwart_auth_exists: "{{ (stalwart_jmap.methodResponses[0][1].list | length) > 0 }}" - name: "📁 Create the authentication singleton (OIDC directory + default roles)" include_tasks: jmap_call.yml vars: stalwart_jmap_body: "{{ lookup('template', 'jmap/authentication/create.json.j2') }}" when: not (stalwart_auth_exists | bool) - name: "🔗 Point the authentication singleton at Keycloak + seed default roles" include_tasks: jmap_call.yml vars: stalwart_jmap_body: "{{ lookup('template', 'jmap/authentication/update.json.j2') }}" when: stalwart_auth_exists | bool - name: "📖 Read the RCPT stage singleton" include_tasks: jmap_call.yml vars: stalwart_jmap_body: "{{ lookup('template', 'jmap/rcpt/get.json.j2') }}" - name: "🔒 Restrict relaying to authenticated sessions" include_tasks: jmap_call.yml vars: stalwart_jmap_body: "{{ lookup('template', 'jmap/rcpt/update.json.j2') }}" when: (stalwart_jmap.methodResponses[0][1].list | length) > 0