Skip to content

security(dci): make OAuth token methods non-RPC and lock the token cache#269

Open
gonzalesedwin1123 wants to merge 4 commits into
19.0from
security-dci-oauth-token-mint
Open

security(dci): make OAuth token methods non-RPC and lock the token cache#269
gonzalesedwin1123 wants to merge 4 commits into
19.0from
security-dci-oauth-token-mint

Conversation

@gonzalesedwin1123

Copy link
Copy Markdown
Member

Do not merge yet — held for human review.

Summary

spp.dci.data.source grants base.group_user read, and get_oauth2_token() / get_headers() were public model methods that self.sudo() to read the administrator-only oauth2_client_secret and return a live OAuth access token / Authorization: Bearer header. Public Odoo model methods are callable over RPC (call_kw) by any user who can browse the record, so a low-privilege internal user could mint a token and impersonate the OpenSPP DCI client to the external registry for whatever scopes the OAuth client holds. The token cache fields (_oauth2_access_token / _oauth2_token_expires_at) had no field-level groups=, so a minted token was also readable by every internal user via search_read.

Fix

  • Make the credential methods non-RPC-callable: rename get_oauth2_token_get_oauth2_token and get_headers_get_headers. Odoo's RPC dispatcher rejects call_kw to underscore-prefixed methods (service/model.py::get_public_method raises AccessError), so they can no longer be invoked over RPC — while the trusted server-side DCIClient service (a plain Python class, the documented public API) still calls them internally. Internal callers updated (_get_headers, test_connection, services/client.py).
  • Lock the token cache fields to groups="base.group_system", and read them via sudo inside _get_oauth2_token so the cache still hits for non-admin internal callers.
  • test_connection (and its action_test_connection button alias) now require self.check_access("write"), so a read-only user cannot trigger a credentialed outbound call.

The sudo() for the client secret stays — now safe, since _get_oauth2_token is reachable only from trusted server-side code.

Why ir.model.access.csv is unchanged

The legit DCIClient caller services (sr / crvs / dr / ibr / compliance / indicators) fetch the data source in the current user's context (env[...].get_by_code(...), no sudo) and call the header method as that user. Removing base.group_user read would break non-admin DCI lookups (and gating the methods by group/su would too). With the credentials and cached token protected by field groups and minting no longer RPC-reachable, config-metadata read is not the exposure.

Tests

  • Token methods are private (public get_oauth2_token/get_headers names removed).
  • The cached token field is hidden from a regular user (fields_get) and unreadable (readAccessError), while an admin can see it.
  • test_connection and action_test_connection raise AccessError for a read-only base.group_user.
  • Regression: admin/DCIClient still mint and cache tokens (existing HTTP-mocked tests, updated to the private names).
  • Green: spp_dci_client 152/152; downstream spp_dci_client_crvs 98/98, spp_dci_client_sr 134/134. Lint/semgrep clean.

Reviewed

Adversarial staff review: APPROVE, no blockers. It verified the RPC block against the running Odoo 19 source, inventoried every public method (none mint/leak; clear_oauth2_token_cache is blocked by the write ACL), confirmed the cache fields are unreadable via all read paths, and found no stale callers/overrides repo-wide. All three review nits addressed.

spp.dci.data.source grants base.group_user read, and get_oauth2_token()/
get_headers() were public model methods that sudo() to read the admin-only
oauth2_client_secret and RETURN a live access token / Bearer header. So any
internal user who can browse a data source could call them over RPC to mint a
token and impersonate the DCI client to the external registry. The token cache
fields (_oauth2_access_token/_oauth2_token_expires_at) had no field-group, so a
minted token was also readable by every internal user via search_read.

- Rename get_oauth2_token -> _get_oauth2_token and get_headers -> _get_headers.
  Odoo rejects RPC call_kw to underscore-prefixed methods, so they are no longer
  reachable via RPC while the trusted server-side DCIClient service (the public
  API) still calls them internally. Callers updated (data_source, client.py).
- Restrict _oauth2_access_token/_oauth2_token_expires_at to base.group_system
  and read them via sudo in _get_oauth2_token so the cache still hits.
- test_connection now requires check_access('write') so a read-only user cannot
  trigger a credentialed outbound call.
The sudo() for the client secret stays (safe: the method is now internal-only).
ir.model.access.csv is unchanged: base.group_user read is required by legit
user-context DCIClient callers, and credentials + cache are now protected.

Tests: token methods are private (public names removed); the cache token field
is hidden from a regular user and unreadable; test_connection raises AccessError
for a read-only user. spp_dci_client 151/151; crvs 98/98, sr 134/134.
Post-review nits on PR (DCI OAuth token minting):
- Update the stale '_get_headers()' debug log string (was 'get_headers()').
- Clarify test_token_methods_are_private docstring (the hasattr check proves the
  public entrypoints are removed; the RPC block for underscore methods is a
  framework guarantee).
- Add test_action_test_connection_requires_write_access — the public button
  alias inherits the check_access('write') gate. spp_dci_client 152/152.
Comment thread spp_dci_client/models/data_source.py Fixed

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request enhances security by restricting access to sensitive OAuth2 token cache fields to system administrators, renaming authentication methods to private (underscore-prefixed) to prevent RPC access, and adding a write access check to connection testing. It also introduces a security test suite. The review feedback highlights critical access control issues where non-admin users will encounter AccessError exceptions: first, when clearing the token cache automatically upon receiving a 401 response, and second, when retrieving headers for bearer-authenticated data sources. The reviewer suggests using sudo() in these contexts and adding corresponding test cases to verify standard user access.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread spp_dci_client/models/data_source.py
Comment thread spp_dci_client/models/data_source.py
Comment thread spp_dci_client/tests/test_data_source_security.py
@codecov

codecov Bot commented Jul 2, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 75.53%. Comparing base (f260ffd) to head (55009e9).
⚠️ Report is 9 commits behind head on 19.0.

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             19.0     #269      +/-   ##
==========================================
+ Coverage   75.31%   75.53%   +0.22%     
==========================================
  Files        1098     1115      +17     
  Lines       65317    66397    +1080     
==========================================
+ Hits        49191    50151     +960     
- Misses      16126    16246     +120     
Flag Coverage Δ
spp_base_common 90.26% <ø> (ø)
spp_dci_client 90.23% <100.00%> (+1.14%) ⬆️
spp_dci_client_compliance 98.51% <ø> (?)
spp_dci_client_crvs 92.33% <ø> (?)
spp_dci_client_dr 85.60% <ø> (ø)
spp_dci_client_ibr 92.27% <ø> (ø)
spp_dci_client_sr 97.89% <ø> (ø)
spp_dci_compliance 92.76% <ø> (ø)
spp_dci_demo 69.23% <ø> (ø)
spp_dci_indicators 95.83% <ø> (ø)
spp_dci_server 90.32% <ø> (ø)
spp_dci_server_social 89.38% <ø> (ø)
spp_farmer_registry_demo 60.97% <ø> (+8.23%) ⬆️
spp_mis_demo_v2 68.08% <ø> (ø)
spp_programs 65.27% <ø> (ø)
spp_registry 86.83% <ø> (ø)
spp_security 66.66% <ø> (ø)
spp_starter_farmer_registry 0.00% <ø> (ø)
spp_starter_social_registry 0.00% <ø> (ø)
spp_starter_sp_mis 81.25% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
spp_dci_client/__manifest__.py 0.00% <ø> (ø)
spp_dci_client/models/data_source.py 91.17% <100.00%> (+2.61%) ⬆️
spp_dci_client/services/client.py 88.92% <100.00%> (ø)

... and 19 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

CodeQL (clear-text logging, data_source.py): the cached-token info log included
_oauth2_token_expires_at, a token-named cache field, tripping the sensitive-data
logging heuristic. Log only the data source code (the expiry is not needed).

Gemini (both HIGH — restricting the cache fields broke legit non-admin flows):
- clear_oauth2_token_cache -> _clear_oauth2_token_cache (private, not RPC, so no
  low-priv force-remint) and write via sudo, so the 401-retry cache clear works
  regardless of the current user's privilege. Caller in client.py updated.
- _get_headers bearer branch now reads the admin-only bearer_token via sudo
  (matching the OAuth2 branch), so a non-admin internal caller can build the
  header. Both sudos carry nosemgrep: odoo-sudo-without-context.

Tests: a non-admin user context can clear the token cache and build bearer
headers. spp_dci_client 154/154.
return self._oauth2_access_token
# Do not log the token expiry field (it is a credential-adjacent
# cache field); log only the data source code.
_logger.info("Using cached OAuth2 token for data source: %s", self.code)
Record the OAuth token-minting hardening in the module version and HISTORY
changelog (regenerated README).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants