mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-06 10:59:59 +00:00
Stalwart v1.0.0 rejects unknown JMAP properties ("Invalid property" ->
notCreated/notUpdated), validates OIDC tokens more strictly and refuses to
open a data store from before 1.0. Port the role and protect existing
installs:
- Milter: hostname/timeoutConnect/timeoutCommand/timeoutData become
host/connectTimeout/commandTimeout/dataTimeout; the existing milter is
looked up by host.
- Onion relay route: address becomes host.
- OIDC directory: requireAudience is gone. Set skipAudienceValidation and
unscopedTokens=interoperable on create and update, so Keycloak tokens
(no Stalwart audience, no mail scope) keep working for the WebUI and
Roundcube XOAUTH2.
- WebUI: send oauthClientId only with SSO enabled; without SSO the WebUI
falls back to its own client metadata document.
- Store guard: before anything changes, a running Stalwart whose table u
still has the 0.16 bigint layout stops the deploy and points to the
stalwart-migrate steps, which the README now documents under
"Upgrading from 0.16".
- Updater: pin the WebUI git_tags source to v2.* to match server 1.x.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
99 lines
4.7 KiB
YAML
99 lines
4.7 KiB
YAML
---
|
|
# 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
|