---
# 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