Skip to content

security(storage): restrict backend credentials to storage administrators#352

Draft
gonzalesedwin1123 wants to merge 4 commits into
19.0from
security-storage-backend-credentials
Draft

security(storage): restrict backend credentials to storage administrators#352
gonzalesedwin1123 wants to merge 4 commits into
19.0from
security-storage-backend-credentials

Conversation

@gonzalesedwin1123

@gonzalesedwin1123 gonzalesedwin1123 commented Jul 24, 2026

Copy link
Copy Markdown
Member

Do not merge yet — held for human review.

Summary

spp.storage.backend stores S3 access/secret keys and Azure connection strings as plain Char fields, and ir.model.access.csv grants base.group_user model read. Because Odoo read access applies to every field unless a field has groups= (and password=True only masks the UI widget — it does not protect values from read/search_read/RPC), any internal user could read the raw cloud-storage credentials and use them to access, modify, or delete files in the configured bucket/container outside Odoo — potentially beneficiary documents or data exports. The module's own groups.xml says only "trusted system administrators" should manage these; the ACL contradicted that. Shipped in released tag 2026.07.

Fix

  • Field-level groups on the three credential fields — s3_access_key, s3_secret_key, azure_connection_string now carry groups="spp_storage_backend.group_storage_admin". Odoo strips group-restricted fields from read/search_read for non-members, and an explicit request for them raises AccessError. Applies to existing 2026.07 DBs automatically on module update (field metadata) — no migration (no schema/data change).
  • sudo() inside the client builders_get_s3_client and _get_azure_client read the credentials off self.sudo(), so a non-admin who is allowed to operate a backend can still store/retrieve without ever receiving the secret ("use without see"). The value is only used to build the boto3/Azure client server-side. # nosemgrep with justification on the sudo lines.
  • base.group_user model read kept — non-secret fields (name, type, bucket/container, endpoint) remain readable, supporting a future non-admin "resolve default backend + store/retrieve" flow. The credentials are protected at the field layer regardless of model read.
  • Version bump 19.0.2.0.0 → 19.0.2.0.1 + HISTORY.

Scope note

Nothing in the repo currently consumes or depends on spp.storage.backend, so this can't break a caller. The field-groups + sudo approach was chosen over dropping the base.group_user read row so the intended "internal users may use storage without seeing credentials" design keeps working. (If the team prefers, the read row could also be removed as belt-and-suspenders — noted for reviewers.)

Tests (TDD)

New tests/test_credential_access.py:

  • base.group_user cannot read s3_access_key / s3_secret_key / azure_connection_string (per-field read, and explicit search_read raises AccessError); default search_read excludes them.
  • The three fields carry a groups restriction (fields_get).
  • A group_storage_admin user reads them normally.
  • The S3 and Azure client builders resolve the real credentials for a base.group_user (sudo path) — pins "use without see" and that the fix doesn't break operation.
  • Green: spp_storage_backend 36/36; ruff/ruff-format/semgrep clean.

Operational remediation

On 2026.07 deployments where a backend was configured with real credentials, those credentials were readable by every internal user for the exposure window. After upgrading, operators should treat the affected S3 keys / Azure connection strings as compromised and rotate them, and review bucket/container access logs.

Reviewed

Independent staff review verified against Odoo 19 source that the credential exposure is fully closed: field-level groups= strips the three credential fields from default read/search_read and raises AccessError on explicit requests, sudo() bypasses for internal client construction only (never returns the secret), and no other leak path exists (the model does not inherit mail.thread, no related= field anywhere points at the credentials, copy()/export_data are ACL-gated). The review's two block items were fixed: (1) _get_azure_public_url read the connection string directly without sudo — extracted a shared _get_azure_connection_string() sudo helper used by both Azure read sites, with a regression test for the SAS-URL path; (2) regenerated README.rst/index.html. The sudo is pinned — removing it makes the client-builder tests fail. Non-blocking notes: non-secret config fields (bucket/endpoint/container/path/region) remain base.group_user-readable (info disclosure only, not credentials); test_connection() is RPC-reachable by non-admins but only returns a bool and discloses nothing.

…up_user

TDD red: spp.storage.backend grants base.group_user model read and the S3
access/secret keys + Azure connection string have no field-level groups,
so any internal user can read the raw credentials via read/search_read.
New tests assert base.group_user cannot read the three credential fields
(read + search_read), that they carry a group restriction, while storage
admins and the server-side client builders still resolve them.
spp.storage.backend stored S3 access/secret keys and Azure connection
strings as plain Char fields, and ir.model.access.csv grants
base.group_user model read, so any internal user could read the raw
credentials via read/search_read and use them to reach the configured
bucket/container outside Odoo (password=True only masks the widget).

- add groups="spp_storage_backend.group_storage_admin" to s3_access_key,
  s3_secret_key and azure_connection_string so Odoo strips them from
  read/search_read for non-admins (explicit requests raise AccessError)
- resolve the credentials via self.sudo() inside _get_s3_client and
  _get_azure_client so a non-admin who is allowed to operate a backend can
  still store/retrieve without being able to read the secret (use-without-see)
- keep base.group_user model read (non-secret fields stay usable)
- bump to 19.0.2.0.1 with HISTORY fragment; no migration (field metadata,
  no schema/data change)
@gemini-code-assist

Copy link
Copy Markdown

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

@codecov

codecov Bot commented Jul 24, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 68.43%. Comparing base (1caf794) to head (29f3112).
⚠️ Report is 3 commits behind head on 19.0.

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             19.0     #352      +/-   ##
==========================================
- Coverage   74.28%   68.43%   -5.86%     
==========================================
  Files         372       97     -275     
  Lines       25385     8578   -16807     
==========================================
- Hits        18857     5870   -12987     
+ Misses       6528     2708    -3820     
Flag Coverage Δ
spp_analytics ?
spp_api_v2_cycles ?
spp_api_v2_entitlements ?
spp_api_v2_gis ?
spp_api_v2_programs ?
spp_api_v2_simulation ?
spp_audit_programs ?
spp_base_common 91.07% <ø> (+0.80%) ⬆️
spp_case_demo ?
spp_case_entitlements ?
spp_case_programs ?
spp_cr_type_assign_program ?
spp_dci_compliance ?
spp_dci_demo ?
spp_dci_server_social ?
spp_demo ?
spp_demo_phl_luzon ?
spp_drims ?
spp_drims_sl ?
spp_drims_sl_demo ?
spp_farmer_registry_demo ?
spp_gis_report ?
spp_grm_demo ?
spp_indicator ?
spp_indicator_studio ?
spp_metric_service ?
spp_mis_demo_v2 ?
spp_programs 65.27% <ø> (+<0.01%) ⬆️
spp_registry 86.94% <ø> (+0.10%) ⬆️
spp_security 69.56% <ø> (+2.89%) ⬆️
spp_simulation ?
spp_storage_backend 88.34% <100.00%> (?)

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

Files with missing lines Coverage Δ
spp_storage_backend/models/storage_backend.py 88.25% <100.00%> (ø)

... and 280 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.

Staff review found _get_azure_public_url read self.azure_connection_string
directly (no sudo), so SAS-URL generation would fail with AccessError for
the non-admin operating users the fix supports (fails closed; latent as
nothing calls get_public_url yet). Extract a shared
_get_azure_connection_string() sudo helper and use it in both
_get_azure_client and _get_azure_public_url; add a regression test that
the SAS path resolves the credential for a base.group_user.
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.

1 participant