Files
computer-playbook/roles/web-app-nextcloud/tasks/utils/mcp/token.yml
kevinveenbirkenbachandClaude Opus 5 938426da01 fix(mcp): hold three guarantees on every run, not only on the first
Home Assistant pinned the MCP service account to its read-only group when it created the account and never again, so an account promoted to administrator by hand kept every mutating tool that scope denies while the deploy stayed green, because nothing looked at the existing case. The group is now asserted on every run and a refusal aborts instead of passing.

Nextcloud minted an app password under a fixed managed name and never revoked the previous one, so every run that found the stored password rejected added another valid token while the old ones kept full account access, accumulating across deploys. The mint is now preceded by a listing and the revocation of every token carrying that name. Both new tasks mask their output, because the listing names the account and its token ids even though the JSON strips the secrets.

Prometheus ran with no query timeout, so its two-minute default applied: a query the adapter abandoned after fifteen seconds kept the server busy for the rest of it. The flag is now derived from the adapter's own limit and doubled, which gives the server room past the adapter's deadline while keeping the two from drifting apart.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-29 00:52:21 +02:00

147 lines
4.7 KiB
YAML

---
- name: "🔍 List the accounts this instance knows"
ansible.builtin.command:
argv:
- container
- exec
- -u
- "{{ NEXTCLOUD_DOCKER_USER }}"
- "{{ NEXTCLOUD_LOCAL_CID }}" # nocheck: container-exec-resolver
- php
- "{{ NEXTCLOUD_INTERNAL_OCC_COMMAND }}"
- user:list
- --output=json
register: nextcloud_mcp_accounts
changed_when: false
delegate_to: "{{ NEXTCLOUD_HOST_NODE }}"
run_once: true
- name: "👤 Create the account the MCP clients authenticate as"
ansible.builtin.command:
argv:
- container
- exec
- -u
- "{{ NEXTCLOUD_DOCKER_USER }}"
- -e
- "OC_PASS={{ NEXTCLOUD_MCP_USER_PASSWORD }}"
- "{{ NEXTCLOUD_LOCAL_CID }}" # nocheck: container-exec-resolver
- php
- "{{ NEXTCLOUD_INTERNAL_OCC_COMMAND }}"
- user:add
- --password-from-env
- "--display-name={{ NEXTCLOUD_MCP_USER_DISPLAY_NAME }}"
- "{{ NEXTCLOUD_MCP_USERNAME }}"
when: NEXTCLOUD_MCP_USERNAME not in (nextcloud_mcp_accounts.stdout | from_json)
changed_when: true
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
delegate_to: "{{ NEXTCLOUD_HOST_NODE }}"
run_once: true
- name: "🔑 Set the account's password to the one this deployment declares"
ansible.builtin.command:
argv:
- container
- exec
- -u
- "{{ NEXTCLOUD_DOCKER_USER }}"
- -e
- "OC_PASS={{ NEXTCLOUD_MCP_USER_PASSWORD }}"
- "{{ NEXTCLOUD_LOCAL_CID }}" # nocheck: container-exec-resolver
- php
- "{{ NEXTCLOUD_INTERNAL_OCC_COMMAND }}"
- user:resetpassword
- --password-from-env
- "{{ NEXTCLOUD_MCP_USERNAME }}"
changed_when: true
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
delegate_to: "{{ NEXTCLOUD_HOST_NODE }}"
run_once: true
- name: "🔍 List the app passwords the MCP account already holds"
ansible.builtin.command:
argv:
- container
- exec
- -u
- "{{ NEXTCLOUD_DOCKER_USER }}"
- "{{ NEXTCLOUD_LOCAL_CID }}" # nocheck: container-exec-resolver
- php
- "{{ NEXTCLOUD_INTERNAL_OCC_COMMAND }}"
- user:auth-tokens:list
- "{{ NEXTCLOUD_MCP_USERNAME }}"
- --output=json
register: nextcloud_mcp_tokens_held
changed_when: false
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
delegate_to: "{{ NEXTCLOUD_HOST_NODE }}"
run_once: true
- name: "🧹 Revoke the app passwords an earlier run minted under the managed name"
ansible.builtin.command:
argv:
- container
- exec
- -u
- "{{ NEXTCLOUD_DOCKER_USER }}"
- "{{ NEXTCLOUD_LOCAL_CID }}" # nocheck: container-exec-resolver
- php
- "{{ NEXTCLOUD_INTERNAL_OCC_COMMAND }}"
- user:auth-tokens:delete
- "{{ NEXTCLOUD_MCP_USERNAME }}"
- "{{ item | string }}"
loop: >-
{{ nextcloud_mcp_tokens_held.stdout | from_json
| selectattr('name', 'equalto', NEXTCLOUD_MCP_TOKEN_NAME)
| map(attribute='id') | list }}
changed_when: true
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
delegate_to: "{{ NEXTCLOUD_HOST_NODE }}"
run_once: true
- name: "🔑 Mint an app password for the MCP clients"
ansible.builtin.command:
argv:
- container
- exec
- -u
- "{{ NEXTCLOUD_DOCKER_USER }}"
- -e
- "NC_PASS={{ NEXTCLOUD_MCP_USER_PASSWORD }}"
- "{{ NEXTCLOUD_LOCAL_CID }}" # nocheck: container-exec-resolver
- php
- "{{ NEXTCLOUD_INTERNAL_OCC_COMMAND }}"
- user:auth-tokens:add
- "{{ NEXTCLOUD_MCP_USERNAME }}"
- --password-from-env
- "--name={{ NEXTCLOUD_MCP_TOKEN_NAME }}"
register: nextcloud_mcp_token_mint
changed_when: true
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
delegate_to: "{{ NEXTCLOUD_HOST_NODE }}"
run_once: true
- name: "🔧 Take the minted app password from the command output"
ansible.builtin.set_fact:
nextcloud_mcp_token: "{{ nextcloud_mcp_token_mint.stdout_lines | last | trim }}"
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
run_once: true
- name: "🛑 Refuse an instance that issued no usable app password"
ansible.builtin.assert:
that:
- nextcloud_mcp_token | length >= 64
fail_msg: >-
occ user:auth-tokens:add returned no app password, so every MCP client
would be configured with an empty bearer and fail at first use.
- name: "💾 Persist the app password so MCP clients can read it"
ansible.builtin.include_role:
name: sys-token-store
tasks_from: write.yml
vars:
sys_token_store_user_key: "{{ lookup('config', application_id, 'mcp.credential.owner') }}"
sys_token_store_app: "{{ application_id }}"
sys_token_store_token: "{{ nextcloud_mcp_token }}"
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"