mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-08 11:46:35 +00:00
fix(sys-front-inj-matomo): keep the admin token out of the request URL
The SitesManager.getSitesIdFromSiteUrl lookup carried token_auth in the query string of a uri_retry whose default is 30 attempts, against matomo_index_php_url - which resolves to the public Matomo URL, not an internal one. One unresolved site therefore wrote up to 31 lines holding the administrator token into Matomo's own access log, the reverse-proxy access log, and any URL-based telemetry on the path. no_log cannot reach those: it masks Ansible's output, not files on the managed host, and local deploys run with MASK_CREDENTIALS_IN_LOGS false anyway. The call now uses the POST form body its siblings in web-app-matomo/tasks/07_config.yml already use. Query-string call sites across the repo: 1 -> 0. Site resolution moves out of inject.yml into tasks/resolve_site.yml and runs before the inj_enabled gate; inject_apply.yml is absorbed into it. That lets matomo_site_id default to an empty string instead of staying undefined, so main.yml gates injection on the resolved id rather than on the token's presence, and a failed resolution disables injection instead of raising 'matomo_site_id is undefined' later. addSite is tightened the same way. It built its body as a raw interpolated string, which the uri module does not URL-encode, and it trusted HTTP 200 - but Matomo answers API errors with 200 and a JSON mapping, so the following set_fact raised "'dict object' has no attribute 'value'" at a confusing place. It now sends the dict body and carries a failed_when on json.value, which surfaces Matomo's own error text. The tracking spec counted bare /setSiteId/ occurrences, which a README excerpt, an admin page or a code sample satisfies, so a page carrying no tracker could read as tracked. It counts the _paq.push call instead. Residuals: whether Matomo accepts this read method over POST is not executed. 07_config.yml proves POST with a dict body against the same index.php for write methods, but that is analogy. If it does not, no site ever resolves, inj_enabled.matomo goes false, and every tracked site silently loses its tracker - closed, but invisible. And no lint pins the fix, so a later call site can put a credential back in a URL. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
1e8be113f3
commit
7309d50575
5 files changed
+100
-79
No files matched your search
@@ -1,25 +1,16 @@
|
||||
---
|
||||
- name: "🔎 Check if site {{ domain }} is already registered at Matomo"
|
||||
uri_retry:
|
||||
url: "{{ matomo_index_php_url }}?module=API&method=SitesManager.getSitesIdFromSiteUrl&url={{ matomo_site_url }}&format=json&token_auth={{ matomo_effective_token
|
||||
}}"
|
||||
method: GET
|
||||
return_content: true
|
||||
status_code: 200
|
||||
validate_certs: true
|
||||
register: site_check
|
||||
failed_when: false
|
||||
- name: 📄 Load Matomo tracking JS template
|
||||
set_fact:
|
||||
matomo_tracking_code: "{{ lookup('template','matomo-tracking.js.j2') }}"
|
||||
|
||||
- name: 🗜️ Collapse Matomo code into one-liner
|
||||
set_fact:
|
||||
matomo_tracking_code_one_liner: "{{ matomo_tracking_code | to_one_liner }}"
|
||||
|
||||
- name: ➕ Append Matomo CSP hash
|
||||
set_fact:
|
||||
webserver_csp_hashes_extra_by_app: >-
|
||||
{{ webserver_csp_hashes_extra_by_app | default({})
|
||||
| add_csp_hash(application_id, 'script-src-elem', matomo_tracking_code_one_liner) }}
|
||||
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
|
||||
|
||||
- name: "🔴 Disable Matomo tracking for '{{ domain }}' (API not ready, reapplied later)"
|
||||
ansible.builtin.set_fact:
|
||||
inj_enabled: "{{ inj_enabled | combine({'matomo': false}) }}"
|
||||
when: >-
|
||||
site_check.status is not defined or site_check.status != 200
|
||||
or site_check.json is not defined or site_check.json is mapping
|
||||
|
||||
- name: "📝 Register site + inject Matomo tracking for '{{ domain }}'"
|
||||
ansible.builtin.include_tasks: inject_apply.yml
|
||||
when: >-
|
||||
site_check.status is defined and site_check.status == 200
|
||||
and site_check.json is defined and site_check.json is not mapping
|
||||
changed_when: false
|
||||
@@ -1,45 +0,0 @@
|
||||
---
|
||||
- name: 🔧 Set matomo_site_id to Null
|
||||
set_fact:
|
||||
matomo_site_id:
|
||||
|
||||
- name: 🔧 Set fact for site ID if site already exists
|
||||
set_fact:
|
||||
matomo_site_id: "{{ site_check.json[0].idsite }}"
|
||||
when: "site_check.json is not mapping and (site_check.json | length) > 0"
|
||||
changed_when: false
|
||||
|
||||
- name: ➕ Add site to Matomo and get ID if not exists
|
||||
uri_retry:
|
||||
url: "{{ matomo_index_php_url }}"
|
||||
method: POST
|
||||
body: "module=API&method=SitesManager.addSite&siteName={{ matomo_site_domain }}&urls={{ matomo_site_url }}&format=json&token_auth={{ matomo_effective_token }}"
|
||||
body_format: form-urlencoded
|
||||
status_code: 200
|
||||
return_content: true
|
||||
validate_certs: true
|
||||
register: add_site
|
||||
when: "matomo_site_id is not defined or matomo_site_id is none"
|
||||
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
|
||||
|
||||
- name: 🔧 Set fact for site ID if site was added
|
||||
set_fact:
|
||||
matomo_site_id: "{{ add_site.json.value }}"
|
||||
when: "matomo_site_id is not defined or matomo_site_id is none"
|
||||
changed_when: false
|
||||
|
||||
- name: 📄 Load Matomo tracking JS template
|
||||
set_fact:
|
||||
matomo_tracking_code: "{{ lookup('template','matomo-tracking.js.j2') }}"
|
||||
|
||||
- name: 🗜️ Collapse Matomo code into one-liner
|
||||
set_fact:
|
||||
matomo_tracking_code_one_liner: "{{ matomo_tracking_code | to_one_liner }}"
|
||||
|
||||
- name: ➕ Append Matomo CSP hash
|
||||
set_fact:
|
||||
webserver_csp_hashes_extra_by_app: >-
|
||||
{{ webserver_csp_hashes_extra_by_app | default({})
|
||||
| add_csp_hash(application_id, 'script-src-elem', matomo_tracking_code_one_liner) }}
|
||||
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
|
||||
changed_when: false
|
||||
@@ -7,20 +7,13 @@
|
||||
# the Matomo body snippet (avoiding 'matomo_site_id is undefined') and inject.yml is not run.
|
||||
# See docs/contributing/design/exceptions.md — "Matomo Token Not Yet Available".
|
||||
|
||||
- name: "🔧 Resolve effective Matomo API token for '{{ domain }}'"
|
||||
ansible.builtin.set_fact:
|
||||
matomo_effective_token: >-
|
||||
{{
|
||||
(matomo_token_override | default('') | string | trim)
|
||||
if (matomo_token_override | default('') | string | trim) != ''
|
||||
else (lookup('users', 'administrator').tokens['web-app-matomo'] | default('') | string | trim)
|
||||
}}
|
||||
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
|
||||
- name: "🔎 Resolve the Matomo site id for '{{ domain }}'"
|
||||
include_tasks: resolve_site.yml
|
||||
|
||||
- name: "🔴 Disable Matomo in inj_enabled for '{{ domain }}' — token not yet available"
|
||||
- name: "🔴 Disable Matomo in inj_enabled for '{{ domain }}' — no site id yet"
|
||||
ansible.builtin.set_fact:
|
||||
inj_enabled: "{{ inj_enabled | combine({'matomo': false}) }}"
|
||||
when: matomo_effective_token == ""
|
||||
when: matomo_site_id == ""
|
||||
|
||||
- name: "💉 Inject Matomo tracking for '{{ domain }}'"
|
||||
include_tasks: inject.yml
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
---
|
||||
- name: "🔧 Resolve effective Matomo API token for '{{ domain }}'"
|
||||
ansible.builtin.set_fact:
|
||||
matomo_effective_token: >-
|
||||
{{
|
||||
(matomo_token_override | default('') | string | trim)
|
||||
if (matomo_token_override | default('') | string | trim) != ''
|
||||
else (lookup('users', 'administrator').tokens['web-app-matomo'] | default('') | string | trim)
|
||||
}}
|
||||
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
|
||||
|
||||
- name: "🔧 Start from an unresolved Matomo site id for '{{ domain }}'"
|
||||
ansible.builtin.set_fact:
|
||||
matomo_site_id: ""
|
||||
|
||||
- name: "🔎 Check if site {{ domain }} is already registered at Matomo"
|
||||
uri_retry:
|
||||
url: "{{ matomo_index_php_url }}"
|
||||
method: POST
|
||||
body_format: form-urlencoded
|
||||
body:
|
||||
module: API
|
||||
method: SitesManager.getSitesIdFromSiteUrl
|
||||
url: "{{ matomo_site_url }}"
|
||||
format: json
|
||||
token_auth: "{{ matomo_effective_token }}"
|
||||
return_content: true
|
||||
status_code: 200
|
||||
validate_certs: true
|
||||
register: site_check
|
||||
failed_when: false
|
||||
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
|
||||
when: matomo_effective_token != ""
|
||||
|
||||
- block:
|
||||
- name: "🔧 Set fact for site ID if site already exists"
|
||||
ansible.builtin.set_fact:
|
||||
matomo_site_id: "{{ site_check.json[0].idsite }}"
|
||||
when: (site_check.json | length) > 0
|
||||
changed_when: false
|
||||
|
||||
- name: "➕ Add site to Matomo and get ID if not exists"
|
||||
uri_retry:
|
||||
url: "{{ matomo_index_php_url }}"
|
||||
method: POST
|
||||
body_format: form-urlencoded
|
||||
body:
|
||||
module: API
|
||||
method: SitesManager.addSite
|
||||
siteName: "{{ matomo_site_domain }}"
|
||||
urls: "{{ matomo_site_url }}"
|
||||
format: json
|
||||
token_auth: "{{ matomo_effective_token }}"
|
||||
status_code: 200
|
||||
return_content: true
|
||||
validate_certs: true
|
||||
register: add_site
|
||||
when: matomo_site_id == ""
|
||||
failed_when: add_site.json is not defined or add_site.json.value is not defined
|
||||
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
|
||||
|
||||
- name: "🔧 Set fact for site ID if site was added"
|
||||
ansible.builtin.set_fact:
|
||||
matomo_site_id: "{{ add_site.json.value }}"
|
||||
when: matomo_site_id == ""
|
||||
changed_when: false
|
||||
when: >-
|
||||
site_check.status is defined and site_check.status == 200
|
||||
and site_check.json is defined and site_check.json is not mapping
|
||||
@@ -13,6 +13,12 @@ const {
|
||||
} = require("./_shared");
|
||||
const { assertInjectedAssetLoadsWithoutCspBlock, gotoOnion } = require("./personas");
|
||||
|
||||
const SET_SITE_ID_CALL = /_paq\s*\.\s*push\s*\(\s*\[\s*['"]setSiteId['"]/g;
|
||||
|
||||
function countTrackers(html) {
|
||||
return (String(html).match(SET_SITE_ID_CALL) || []).length;
|
||||
}
|
||||
|
||||
test.use({ ignoreHTTPSErrors: true });
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
@@ -99,15 +105,22 @@ for (const target of matomoTargetRoles) {
|
||||
html,
|
||||
`Expected matomo tracker '_paq' marker in ${target.id} HTML body`
|
||||
).toContain("_paq");
|
||||
|
||||
expect(
|
||||
html,
|
||||
`Expected matomo tracker URL ('matomo.php') in ${target.id} HTML body`
|
||||
).toContain("matomo.php");
|
||||
|
||||
if (matomoCanonicalDomain) {
|
||||
expect(
|
||||
html,
|
||||
`Expected matomo host '${matomoCanonicalDomain}' referenced in ${target.id} HTML body`
|
||||
).toContain(matomoCanonicalDomain);
|
||||
}
|
||||
|
||||
expect(
|
||||
countTrackers(html),
|
||||
`Exactly one Matomo tracker must reach ${target.id}. Two means the snippet was injected twice and every visit is double-counted.`
|
||||
).toBe(1);
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user