06_oidc.ymlΒΆ

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