security(dci): purge stale compliance bearer token on upgrade#326
security(dci): purge stale compliance bearer token on upgrade#326gonzalesedwin1123 wants to merge 7 commits into
Conversation
Earlier versions of spp_dci_client_compliance created an spp.dci.data.source record carrying the well-known token 'compliance-test-api-key-12345'. The 19.0.1.0.1 post-migration removed only the ir.config_parameter copy, so on upgraded databases the retained data source kept working with the known token once the compliance gate was enabled - and since the trigger routes are auth='none', an unauthenticated caller could drive DCI client operations through it. - Add a 19.0.1.0.2 post-migration that deletes any compliance data source still holding the default token (records re-keyed with a real token are left untouched), via a new _purge_default_compliance_bearer_token() helper. - Harden _get_test_data_source() to refuse serving a data source that still holds the default token, so a skipped migration cannot re-expose it. - Add regression tests for the purge helper and the controller guard. - Bump version to 19.0.1.0.2 and add HISTORY fragment.
|
Caution The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased. |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## 19.0 #326 +/- ##
==========================================
- Coverage 74.28% 68.36% -5.92%
==========================================
Files 372 99 -273
Lines 25385 8462 -16923
==========================================
- Hits 18857 5785 -13072
+ Misses 6528 2677 -3851
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
…ngelog Applies CI's pinned oca-gen output verbatim for the new HISTORY fragment.
…uard An empty recordset already makes the warning loop, len(), and unlink() no-ops, so the explicit 'if not stale: return 0' early return is dead weight.
|
Filed the server-side follow-up as #328: |
|
Live upgrade-path test: PASS (beyond the unit suite, this verifies the migration wiring — folder discovery, Procedure: fresh-installed the module at 19.0.1.0.2 into a dedicated DB, seeded the pre-fix state (data source with Result: Post-upgrade assertions: default-token records = 0; re-keyed control record untouched ( |
…create path Staff-engineer review found the search-path guard left the create path open: _get_compliance_bearer_token only checked the token was non-empty, so an operator following the module docs (which suggested the public default) would mint a fresh data source carrying it - recreating the exposure the migration purges. Reject the default token there, fix the misleading DESCRIPTION.md instruction, and add create-path + token-only-match regression tests.
…ord masking Second review pass found two more gaps: - The purge searched with default active_test=True, so an ARCHIVED data source still holding the default token survived the migration (secret at rest, still usable by non-controller consumers via get_headers). Purge now runs with active_test=False. - The controller filtered the default token AFTER a limit=1 search, so a stale record sorting ahead of a legitimately re-keyed one (same name/flag) masked the valid record and forced a needless create. Exclude the default token in the search domain instead, replacing the post-filter helper. Adds regression tests for the archived purge and the stale+rekeyed coexistence.
…n guidance Applies CI's pinned oca-gen output verbatim for the updated DESCRIPTION/HISTORY fragments (private-token requirement + create-path rejection).
…xclusion change The record is now dropped by the default-token exclusion in the search domain, not found-then-rejected by a post-filter.
Summary
Fixes an upgrade-path exposure in
spp_dci_client_compliance: the19.0.1.0.1hardening was meant to fail closed and retire the old shared compliance bearer
token, but it only deleted the
ir.config_parametercopy. Earlier versions(
19.0.1.0.0) also created anspp.dci.data.sourcerecord withauth_type='bearer'andbearer_token='compliance-test-api-key-12345'. Thatrecord survives the upgrade (a runtime-created one has no xml_id, so Odoo's
orphan cleanup never removes it), and
_get_test_data_source()returned anexisting
is_compliance_testrecord before ever reaching the fail-closed_get_compliance_bearer_token(). Because the trigger routes areauth='none',an unauthenticated network caller could drive DCI client operations
(search/subscribe/unsubscribe/txn_status) through the retained known credential
on upgraded deployments once the compliance gate is enabled.
Changes (final)
migrations/19.0.1.0.2/post-migration.py+_purge_default_compliance_bearer_token):deletes any
spp.dci.data.sourcestill holding the well-known default token,including archived (
active=False) records (searched withactive_test=False). A new version + folder ensures it runs for both affectedpopulations — DBs upgrading from
19.0.1.0.0and DBs already at19.0.1.0.1.Records an operator re-keyed with a real token are matched on the token value
alone, so they are left untouched.
_get_test_data_source): excludes the default token directlyin the search domain (
("bearer_token", "!=", DEFAULT)), so a retainedrecord is never served even if the migration is skipped — and a legitimately
re-keyed record is still found when a stale record would otherwise sort ahead
of it under
limit=1._get_compliance_bearer_token): rejects the well-knowndefault token when it is configured, so the create path cannot mint a fresh
data source carrying the public secret through the front door.
readme/DESCRIPTION.mdno longer instructs operators to use thedefault token; it now requires a private token and documents that the
default is rejected.
19.0.1.0.1→19.0.1.0.2+HISTORY.mdfragment;README.rst/
index.htmlregenerated by CI's pinned generator.Design decisions (confirmed with owner)
known public token) so the intended fail-closed create path governs.
rejection — guard every path to the sink, per the repo pitfall.
Post-review hardening
Two adversarial staff-engineer reviews ran on this branch. Beyond the original
migration + guard, they surfaced three gaps, all fixed here:
set it) → create-path rejection + doc fix.
active_test=True), leavingthe secret at rest and usable by non-controller consumers → purge with
active_test=False.limit=1filter could mask a valid re-keyed record when astale record sorted ahead → moved the exclusion into the search domain
(replacing the earlier
_reject_default_tokenpost-filter).Verified SOLID by the reviews: migration version mechanics, superuser
field-group visibility,
unlink()FK safety (only set-null refs),nosemgrep suppressions, exact-match rationale, tests fail without the fix.
Testing
./spp t spp_dci_client_compliance→ 39 passed, 0 failed, 0 errors(33 original + 6 review-driven), confirmed locally and by CI's own
test (spp_dci_client_compliance)job.19.0.1.0.0state (active stale +archived stale + re-keyed control), rolled the installed version back, ran
odoo -u spp_dci_client_compliance. Log: both migrations execute, thenRemoved 2 DCI compliance data source(s) that still held the default bearer token.Post-upgrade assertions: default-token records = 0 (active andarchived), re-keyed record untouched, module at
19.0.1.0.2.Notes for reviewers
regen is not byte-reproducible per repo guidance).
spp_dci_compliance/__init__.py_post_init_hookpresets the same secret asan accepted inbound
dci.api_tokensvalue and disables signature checks —a separate module and exposure.
Draft: awaiting human security review before undraft/merge.