[PM-33940] Enforce per-secret access checks in SecretVersionsController#7823
[PM-33940] Enforce per-secret access checks in SecretVersionsController#7823mandreko-bitwarden wants to merge 1 commit into
Conversation
All five secret-version endpoints previously short-circuited the per-secret access check whenever the caller was a ServiceAccount or Organization client, falling back to only the coarse org-level AccessSecretsManager gate. That let any service account in the org read, restore, or delete version history for secrets it had no access policy on, including write impact via restore. Remove the bypass branches from GetVersionsBySecretIdAsync, GetByIdAsync, GetManyByIdsAsync, RestoreVersionAsync, and BulkDeleteAsync. Every endpoint now runs AccessToSecretAsync/AccessToSecretsAsync against the caller's AccessClientType, matching the pattern in SecretsController. RestoreVersionAsync keeps a SA vs User split only for snapshot attribution (EditorServiceAccountId vs EditorOrganizationUserId) after the write check has passed. Tests: - Fix two pre-existing BulkDelete tests that mocked the wrong repository method and were passing for the wrong reason. - Add SA-path regression tests for all five endpoints, plus a mixed-batch bulk-read case.
🤖 Bitwarden Claude Code ReviewOverall Assessment: APPROVE This PR removes an authorization bypass in Code Review DetailsNo findings. Notes from verification:
|
|
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #7823 +/- ##
==========================================
- Coverage 65.67% 61.28% -4.39%
==========================================
Files 2195 2195
Lines 97542 97511 -31
Branches 8807 8801 -6
==========================================
- Hits 64061 59762 -4299
- Misses 31260 35625 +4365
+ Partials 2221 2124 -97 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|



🎟️ Tracking
PM-33940
📔 Objective
All five secret-version endpoints previously short-circuited the per-secret access check whenever the caller was a
ServiceAccountorOrganizationclient, falling back to only the coarse org-levelAccessSecretsManagergate. That let any service account in the org read, restore, or delete version history for secrets it had no access policy on, including write impact via the restore endpoint.The fix removes the bypass branches from
GetVersionsBySecretIdAsync,GetByIdAsync,GetManyByIdsAsync,RestoreVersionAsync, andBulkDeleteAsync. Every endpoint now runsAccessToSecretAsync/AccessToSecretsAsyncagainst the caller'sAccessClientType, matching the pattern used bySecretsController.GetAsync.RestoreVersionAsynckeeps aServiceAccountvsUsersplit only for snapshot attribution (EditorServiceAccountIdvsEditorOrganizationUserId), after the write check has passed.Why this approach over the alternative
SecretAuthorizationHandler.CanUpdateSecretAsyncwould also have worked, butSecretsController.GetAsyncalready callsAccessToSecretAsyncdirectly. Matching that pattern keeps the controller readable and the diff minimal. Both paths land in the sameBuildSecretAccessQuery.Tests
BulkDeletetests that mockedGetByIdAsync/AccessToSecretAsyncwhile the controller callsGetManyByIdsAsync/AccessToSecretsAsync. They were passing for the wrong reason.SecretVersionsControllerTestspass.Not addressed in this PR
CurrentContext.AccessSecretsManagerreturnstruefor any service account in the same org. It's used very broadly across the codebase, and changing it would have a large blast radius. The controller-layer per-secret check is the correct place to enforce, and matches how every other Secrets Manager endpoint already works.📸 Screenshots
N/A, no UI changes.