Files
computer-playbook/roles/web-app-stalwart/tasks/jmap_call.yml
kevinveenbirkenbachandClaude Opus 5.5 731f3b240d fix(web-app-stalwart): fail on a refused app password destroy and match the ban tracer by its events
Two findings of the CodeRabbit review of #869.

jmap_call.yml counted notCreated and notUpdated as a refused call, but
not notDestroyed. A destroy that Stalwart rejected therefore passed, and
the post-load flush cleared stalwart_app_password_stale while the retired
app password stayed valid. A notDestroyed entry now fails the call,
except notFound, where the password is already gone. The password keeps
its retired description, so the next deploy collects it again and
retries the destroy.

tracing.yml took any Stdout tracer with an include policy for the ban
tracer, so one that includes other events kept the role from creating
its own and bans stayed off the container output. The selector now also
requires every event of STALWART_BAN_TRACER.

Verified with the real task files, a stubbed container command and
canned JMAP responses: a forbidden destroy now ends with failed=1, while
notFound and a clean destroy pass; with an unrelated include tracer the
role makes a second JMAP call that creates the ban tracer, with a
complete one it does not. The previous files pass the forbidden destroy
and skip the creation in both tracer cases.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-13 21:32:42 +02:00

49 lines
2.4 KiB
YAML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
# Reusable management JMAP call: in `stalwart_jmap_body` (JSON), out `stalwart_jmap`.
# Param: stalwart_jmap_tolerate_rejected - true when the caller reads a set-level rejection itself (e.g. alreadyExists).
# Param: stalwart_jmap_act_as - account address the recovery admin acts for; unset runs the call as the admin itself.
- name: "🔧 Stalwart JMAP call"
ansible.builtin.shell:
cmd: >-
container exec -i {{ STALWART_CONTAINER_ADDRESS }}
{{ 30 | curl }} -sS -f -K -
-H "Content-Type:application/json"
{{ STALWART_JMAP_URL }}
stdin: |-
user = {{ ((stalwart_jmap_act_as ~ '%' if stalwart_jmap_act_as is defined else '') ~ STALWART_ADMIN_USER ~ ':' ~ STALWART_ADMIN_PASSWORD) | to_json(ensure_ascii=False) }}
data-binary = {{ stalwart_jmap_body | to_json(ensure_ascii=False) }}
register: stalwart_jmap_raw
changed_when: false
retries: 12
delay: 5
until: >-
stalwart_jmap_raw.rc == 0
and (
(stalwart_jmap_raw.stdout | from_json).methodResponses[0][0] != 'error'
or (stalwart_jmap_raw.stdout | from_json).methodResponses[0][1].type
not in STALWART_JMAP_TRANSIENT_ERRORS
)
timeout: "{{ lookup('timeout', 120) | int }}"
# nocheck: include-uniform-apply-keyword — seven call sites include this file; the in-file no_log is the SPOT, per-site apply would repeat it 7×
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
- name: "🔧 Parse Stalwart JMAP response"
ansible.builtin.set_fact:
stalwart_jmap: "{{ stalwart_jmap_raw.stdout | from_json }}"
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
- name: "✅ Ensure Stalwart accepted the JMAP call"
ansible.builtin.assert:
that:
- stalwart_jmap.methodResponses[0][0] != 'error'
- stalwart_jmap_rejected | length == 0
fail_msg: "Stalwart refused the JMAP call {{ stalwart_jmap.methodResponses[0][0] }}: {{ stalwart_jmap_rejected or stalwart_jmap.methodResponses[0][1] }}"
quiet: true
vars:
stalwart_jmap_rejected: >-
{{ {} if (stalwart_jmap_tolerate_rejected | default(false) | bool)
else (stalwart_jmap.methodResponses[0][1].notCreated | default({}, true))
| combine(stalwart_jmap.methodResponses[0][1].notUpdated | default({}, true))
| combine(stalwart_jmap.methodResponses[0][1].notDestroyed | default({}, true)
| dict2items | rejectattr('value.type', 'equalto', 'notFound') | items2dict) }}