Skip to content

security(key_management): stop Key Management Admin group from granting system admin - #329

Merged
gonzalesedwin1123 merged 7 commits into
19.0-staging-sec-batch1from
security-key-admin-group-implies-system
Jul 28, 2026
Merged

security(key_management): stop Key Management Admin group from granting system admin#329
gonzalesedwin1123 merged 7 commits into
19.0-staging-sec-batch1from
security-key-admin-group-implies-system

Conversation

@gonzalesedwin1123

@gonzalesedwin1123 gonzalesedwin1123 commented Jul 23, 2026

Copy link
Copy Markdown
Member

Do not merge yet — held for human review.

Summary

spp_key_management.group_key_admin ("Key Management Admin") was defined with implied_ids linking base.group_system. In Odoo, implied_ids grants the implied groups to members — it is not a membership precondition — so assigning the key management role silently made that user a full Settings/System administrator. The link shipped with the initial platform export and is present in both released tags (v19.0.2.0.0, 2026.07). This is the same bug shape fixed for the DCI group in #327; a repo-wide sweep (multi-line-aware) confirms these were the only two instances.

Unlike the DCI case, the escalation was load-bearing: spp.encryption.key.encrypted_key (the KMS-wrapped key material) is field-gated, and the AWS KMS / Azure Key Vault providers read and write it in the calling user's context (the database provider sudo's; those two don't) — so key admins could only use those providers because they were escalated to system.

Fix

  • Remove the escalation: group_key_admin no longer implies base.group_system. It implies group_key_operator_officer instead (admin ⊇ operator; operator implies base.group_user, so a key admin stays an internal user). The groups file is reordered so the operator group is defined before the admin group that now references it.
  • Deliberately NOT linked into spp_security.group_spp_admin (divergence from security(dci): stop DCI Administrator group from granting system admin #327): system admins already hold their own explicit ACL rows on every key model and the menu lists both groups, so they lose nothing — while the conventional link would newly grant key rotation and provider configuration to every OpenSPP admin. Least privilege wins for the encryption control-plane.
  • Preserve key-admin function on the cloud providers: encrypted_key's field gate now includes group_key_admin. This is a strict reduction of that role's access (from "entire system" to "key models + KMS-wrapped ciphertext"); plaintext key material is never stored in that field.
  • Migration for released databases: migrations/19.0.2.0.1/post-migration.py strips the link via the ORM and logs the count of affected users. Removing the XML line alone never unlinks an existing relation (the file is not noupdate, but data loads only add Command.link entries); Odoo 19 computes membership as a transitive closure, so the migration revokes the escalation immediately. The new admin→operator implication is applied by the regular data load.
  • Menu reachability (adversarial-review finding): the Key Management root menu moved from Settings to top level. The Settings root is gated to base.group_erp_manager and Odoo prunes menus whose ancestors are invisible, so a dedicated key admin (no longer a system admin) had no way to reach Key Management in the UI. Pinned by a load_menus-based test; disclosed in HISTORY.
  • Version bump 19.0.2.0.0 → 19.0.2.0.1 + HISTORY.md fragment.

Tests (TDD — red commit first; the module had no group tests at all)

New tests/test_key_admin_group.py:

  • test_key_admin_does_not_grant_system: link absent from implied_ids and all_implied_ids (blocks reintroduction through an intermediate group); behavioral: a user granted the group does not has_group('base.group_system').
  • test_key_admin_implies_operator: operator and internal-user membership via the closure.
  • test_key_admin_reads_encrypted_key: key admin reads the wrapped key field (guards the AWS/Azure provider paths); operator is denied (pins the field gate).
  • test_migration_removes_escalation: recreates the released state, runs migrate() under assertLogs, asserts the link and its closure effects are gone and the operator-facing warning is emitted.
  • Green: spp_key_management 75/75, dependents spp_encryption 26/26 and spp_claim_169 26/26. Lint (ruff/prettier/pylint-odoo) clean; security audit 0 errors / 0 warnings.

Operational note for deployments

On v19.0.2.0.0 / 2026.07 installs, anyone granted Key Management Admin has been a de-facto system admin since the grant. The migration revokes the privilege on upgrade, but operators should audit actions taken by those users while escalated. Additionally — unlike the DCI case — such users had system-level access to KMS-wrapped key material and provider configuration, so if key-custody policy assumes separation between platform admins and key admins, consider rotating data keys after the upgrade.

Known gap (pre-existing, not addressed here)

Key operators cannot use the AWS KMS / Azure Key Vault provider paths (model read ACL but no field access to encrypted_key, and those providers don't sudo like the database provider does). This predates this PR — those paths only ever worked for system-level users — and will be filed separately rather than mixing a functional repair into this security fix.

Reviewed

Adversarial staff review: the group rework, migration, and tests were confirmed sound against Odoo 19 source (sequential ref() resolution makes the operator-first reorder necessary and sufficient; MigrationManager bounds run the script exactly on the 2.0.0 → 2.0.1 upgrade; field-level groups raise AccessError on non-member attribute reads even with a warm cache, so the operator-denied test cannot false-pass; the operator implication grants nothing beyond internal-user status — its other ACL rows were already internal-user-readable metadata). On the critical field-gate question: for the database provider the stored value is AES-GCM ciphertext under a KEK from env/config unreachable via the ORM, and key admins already receive plaintext DEKs through the sanctioned KeyManager API (_check_key_access allows them all operations), so field access to ciphertext is strictly weaker than what the role legitimately has — not an escalation. The review's one confirmed problem — key admins losing all UI access to Key Management because the menu lived under the erp-manager-gated Settings root — is fixed in this PR (menu re-parented to top level, test-pinned, disclosed in HISTORY); its minor notes (GCP KMS also reads the field in user context; migration log counting; migration idempotence coverage) are addressed. The pre-existing asymmetric-key field-gate inconsistency and operator-on-KMS gap remain out of scope as documented.

Merge order & release-note note

Per the cross-PR interaction analysis (2026-07-24, internal/plans/security-prs-interaction-analysis.md): no hard ordering — every combination with the other open security PRs commutes (verified: key-management workflows don't regress; key admins have their own ACL rows and _check_key_access checks the group independently; the two system-gated points are fixed in this PR). Soft preference: land this early (or in the same release as #336/#265) — until the implication is stripped, key admins still transit base.group_system and pass those PRs' new gates.

Release notes must state: Key Management Admin no longer grants system admin; in combination with #265, OAuth signing-key rotation now requires explicit base.group_system (the two key domains are deliberately separate — OAuth JWT keys live in ir.config_parameter via spp_oauth, not in spp.encryption.key). The migration logs a warning, but it belongs in the notes.

@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 23, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 70.67%. Comparing base (ec4493d) to head (180c7fc).

Additional details and impacted files

Impacted file tree graph

@@                     Coverage Diff                     @@
##           19.0-staging-sec-batch1     #329      +/-   ##
===========================================================
- Coverage                    75.84%   70.67%   -5.17%     
===========================================================
  Files                          188      219      +31     
  Lines                        12492    17348    +4856     
===========================================================
+ Hits                          9474    12261    +2787     
- Misses                        3018     5087    +2069     
Flag Coverage Δ
spp_attachment_av_scan 83.98% <ø> (?)
spp_base_common 91.07% <ø> (ø)
spp_claim_169 58.18% <ø> (?)
spp_dci_compliance ?
spp_dci_demo 94.28% <ø> (ø)
spp_encryption 55.79% <ø> (?)
spp_key_management 38.52% <ø> (?)
spp_mis_demo_v2 70.99% <ø> (?)
spp_programs 65.27% <ø> (ø)
spp_registry 86.94% <ø> (ø)
spp_security 69.56% <ø> (ø)

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

Files with missing lines Coverage Δ
spp_key_management/models/encryption_key.py 91.66% <ø> (ø)

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

@gonzalesedwin1123

Copy link
Copy Markdown
Member Author

Staff-review follow-ups filed as separate issues (all pre-existing, deliberately out of this PR's scope):

TDD red: group_key_admin currently implies base.group_system, escalating
any Key Management Admin to full Settings/System admin. New tests assert
the implication is gone (direct and transitive), that admin implies
operator instead, that key admins keep field access to the KMS-wrapped
key material (the AWS/Azure providers read it in user context), and that
the 19.0.2.0.1 migration strips the link from existing databases.
…tem admin

spp_key_management.group_key_admin listed base.group_system in
implied_ids. In Odoo, implied_ids grants the implied groups to members -
it is not a membership precondition - so assigning the key management
role silently made the user a full Settings/System administrator.

- replace the base.group_system link with group_key_operator_officer
  (admin is a superset of operator and stays an internal user); system
  admins keep access through their own explicit ACL rows on every key
  model, so nothing is lost and nothing new is granted
- extend the encrypted_key field gate to include group_key_admin: the
  AWS KMS / Azure Key Vault providers read the cached wrapped key in the
  calling user's context, which previously only worked because key
  admins were escalated to system; the field holds KMS-wrapped
  ciphertext, never plaintext key material
- ship migrations/19.0.2.0.1/post-migration.py to strip the unsafe link
  from databases installed from the v19.0.2.0.0 / 2026.07 releases
  (removing the XML line alone never unlinks an existing relation);
  membership is a computed transitive closure, so removal revokes the
  escalation immediately
- reorder the groups file so the operator group is defined before the
  admin group that now references it
- bump to 19.0.2.0.1 with HISTORY fragment
TDD red: the menu root is parented under Settings, which Odoo only
shows to base.group_erp_manager; load_menus prunes menus whose
ancestors are invisible, so a user holding only the key admin role has
no way to reach Key Management in the UI once the group no longer
escalates to system admin.
…admins

Review follow-ups:
- move the Key Management root menu from Settings to top level: the
  Settings root is gated to base.group_erp_manager and Odoo prunes
  menus with invisible ancestors, so dedicated key admins (no longer
  system admins) had no way to reach the menu; disclosed in HISTORY
- the encrypted_key field-gate comment also names GCP KMS, which reads
  the cached wrapped key in user context like AWS/Azure
- the migration logs all_user_ids (includes members of any implying
  group), and its test now pins idempotence with assertNoLogs
…on; pin admin retention

Staff-review polish:
- security_groups.xml now states that group_key_admin is deliberately
  NOT linked into spp_security.group_spp_admin (key custody separated
  from platform administration), so a future consistency sweep doesn't
  reintroduce the over-grant
- add a test that a system admin (without the key group) retains field
  read access and menu visibility - pinning the other half of the design
@gonzalesedwin1123
gonzalesedwin1123 force-pushed the security-key-admin-group-implies-system branch from 6e450ac to 180c7fc Compare July 28, 2026 08:01
@gonzalesedwin1123
gonzalesedwin1123 marked this pull request as ready for review July 28, 2026 08:46
@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.

@gonzalesedwin1123
gonzalesedwin1123 merged commit a92701b into 19.0-staging-sec-batch1 Jul 28, 2026
25 checks passed
@gonzalesedwin1123
gonzalesedwin1123 deleted the security-key-admin-group-implies-system branch July 28, 2026 08:47
gonzalesedwin1123 added a commit that referenced this pull request Aug 13, 2026
…g keys, GRM rule ACL (#327, #329, #265, #266) (#399)

* security(dci): stop DCI Administrator group from granting system admin (#327)

Reviewed head: d171acb

* security(key_management): stop Key Management Admin group from granting system admin (#329)

Reviewed head: 180c7fc

* security(oauth): restrict OAuth signing-key settings to system admins (#265)

Reviewed head: e9dc451

* security(grm): restrict GRM automation rules to GRM staff (drop portal write/create) (#266)

Reviewed head: bebd609

* fix(spp_oauth): restore @api.model on default_get, field-gate signing keys (#399)

Review response on PR #399 (findings 1-2):

- default_get() override lacked @api.model. call_kw reads the dispatch
  marker off the most-derived method, so every external RPC call to
  res.config.settings.default_get crashed with a TypeError once this
  module was installed (web Settings UI unaffected - it resolves
  defaults in-process). Regression-tested through the real dispatcher.
- oauth_priv_key/oauth_pub_key now carry groups="base.group_system":
  a settings save by an unauthorized principal fails closed with
  AccessError instead of silently deleting the stored parameters via
  set_param(False). The default_get pop stays - field groups are not
  enforced in default_get. Defence-in-depth: core's ACL already limits
  the model to system admins; the gate holds if that is ever re-widened.

* fix(spp_dci): make DCI PII visibility opt-in per admin, harden migration (#399)

Review response on PR #399 (findings 3, 7, 8):

- Drop the spp_security.group_spp_admin -> group_dci_admin implication:
  PII rendering is now opt-in per administrator via an explicit,
  reviewable Access Rights grant instead of an automatic side effect
  of adminship, mirroring the deliberate key-custody separation in
  spp_key_management. The pinning test is inverted to guard against a
  consistency sweep reintroducing the link. The link never shipped in
  any release and no production DB is built from this staging branch,
  so no unlink migration is needed; the upgrade-test seed DB is
  rebuilt for the refreshed evidence run.
- Migration comment refresh now strips only the stale sentence
  ("Members must already be system administrators") so operator
  rewrites and extensions survive, honouring noupdate.
- Migration warning counts all_user_ids (transitive membership),
  matching the spp_key_management migration's counting basis - safe
  now that nothing implies the group.
- Declare the spp_security dependency explicitly: the security tests
  pin design decisions against spp_security.group_spp_admin, and the
  dependency was previously only transitive via spp_registry.

* fix(spp_key_management): correct encrypted_key wrap comment, add app icon (#399)

Review response on PR #399 (findings 5, 6):

- The encrypted_key field comment claimed the value is always KMS
  ciphertext; for the database provider it is wrapped by the master
  KEK, which in zero-config setups is derived from database.uuid.
  State both provider families accurately. The field gate itself is
  unchanged (the KMS-provider sudo rework is tracked in #330/#331).
- The top-level Key Management menu now carries the module icon -
  every other top-level app menu sets web_icon; without it the app
  switcher shows a generic placeholder tile. Pinned in the existing
  load_menus test via web_icon_data.

* test(spp_grm_cel): internal manager fixture, escalation counter regression (#399)

Review response on PR #399 (findings 10, 11, 12):

- The GRM manager fixture now links base.group_user: the spp_grm group
  chain carries no user-type group, so the manager was created as a
  share=True external principal and the staff-retention tests proved
  less than intended.
- New regression test: a caller with read-only rule access gets a
  fully applied escalation with the counter incremented. The actor is
  a portal user - the population the sudo'd counter write serves in
  practice (officers/managers hold rule write; internal base users and
  GRM viewers are read-only too but do not drive ticket flow).
  Reverting the 19.0.2.0.1 sudo fix now fails loudly instead of
  passing the suite.
- Portal-read docstrings reworded: the read row is a current
  implementation dependency (rule evaluation runs as the acting user,
  reachable by portal via direct-RPC ticket create/stage-write), not a
  security requirement. Tightening it - sudo evaluation, dropping the
  read rows, and the missing portal record rule on spp.grm.ticket - is
  tracked in #413.
- HISTORY: scope the half-way claim to the counter/chatter state this
  fix addresses; notification/case partial failures are pre-existing.

* docs: regenerate READMEs from CI's pinned generator (#399)

Applied verbatim from the pre-commit CI run's printed diff (run
31679116024) - local regeneration is not byte-reproducible against
CI's hook env, so CI output is the sole authority for generated files.

* fix(spp_dci): put noupdate on the odoo root, drop deprecated data node (#399)

With the group_spp_admin record removed, the noupdate <data> wrapper
became the file's sole element, which oca-checks-odoo-module flags as
xml-deprecated-data-node (the failing 'Checks for Odoo modules' CI
hook). Same noupdate semantics, modern form.
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