Skip to content

[Sm-1588] Event logging for secret versioning restore event - #7533

Open
cd-bitwarden wants to merge 12 commits into
mainfrom
Sm-1588-EventLogging
Open

[Sm-1588] Event logging for secret versioning restore event#7533
cd-bitwarden wants to merge 12 commits into
mainfrom
Sm-1588-EventLogging

Conversation

@cd-bitwarden

Copy link
Copy Markdown
Contributor

🎟️ Tracking

https://bitwarden.atlassian.net/browse/SM-1588?assignee=625cb516fd06270069beaf5d&search_id=dd3af888-5ba7-4252-9b3a-7e557eca28fc

📔 Objective

Track when a restore event occurs, it should create a secret edited event log. Also ensure the version date is correct.

@github-actions

github-actions Bot commented Apr 23, 2026

Copy link
Copy Markdown
Contributor

Logo
Checkmarx One – Scan Summary & Detailsbacfee88-43ac-411b-a493-2008df64b4e0

Great job! No new security vulnerabilities introduced in this pull request

@codecov

codecov Bot commented Apr 23, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 58.62069% with 12 lines in your changes missing coverage. Please review.
✅ Project coverage is 59.50%. Comparing base (28bd286) to head (907f27f).
⚠️ Report is 25 commits behind head on main.

Files with missing lines Patch % Lines
...etsManager/Controllers/SecretVersionsController.cs 68.42% 5 Missing and 1 partial ⚠️
...pi/SecretsManager/Controllers/SecretsController.cs 40.00% 5 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #7533      +/-   ##
==========================================
- Coverage   59.50%   59.50%   -0.01%     
==========================================
  Files        2089     2089              
  Lines       92421    92442      +21     
  Branches     8214     8217       +3     
==========================================
+ Hits        54994    55005      +11     
- Misses      35484    35493       +9     
- Partials     1943     1944       +1     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 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.

justindbaur
justindbaur previously approved these changes Apr 23, 2026
@sonarqubecloud

Copy link
Copy Markdown

@cd-bitwarden cd-bitwarden added the ai-review Request a Claude code review label Sep 9, 2026
@github-actions

github-actions Bot commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

🤖 Bitwarden Claude Code Review

Overall Assessment: REQUEST CHANGES

This PR adds Secret_Edited event logging to the secret version restore endpoint and corrects the VersionDate/old-value capture when a secret version snapshot is created. The event-logging additions and the SecretsController.UpdateSecretAsync fix (capturing originalValue/valueRevisionDate before ToSecret mutates the entity in place) are sound. However, the new IdentityClientType.ServiceAccount / Organization short-circuits in SecretVersionsController remove the per-secret access checks for machine accounts, and the branch currently does not compile.

Code Review Details
  • ❌ : Service account bypass — restore skips the per-secret access.Write check, letting any machine account in the org overwrite any secret
    • src/Api/SecretsManager/Controllers/SecretVersionsController.cs:192
  • ❌ : Service account / organization bypass — version listing skips the per-secret access.Read check, exposing full secret version values
    • src/Api/SecretsManager/Controllers/SecretVersionsController.cs:53
  • ❌ : currentValue declared twice in the same scope (CS0128) — the Api build and test jobs are failing
    • src/Api/SecretsManager/Controllers/SecretVersionsController.cs:188 (duplicate of :235)
  • ⚠️ : VersionDate fix applied only to the service account path; the user restore path still records DateTime.UtcNow
    • src/Api/SecretsManager/Controllers/SecretVersionsController.cs:204 (vs :260)
  • ♻️ : The IdentityClientType.User and IdentityClientType.Organization branches are byte-identical and can be collapsed into one condition
    • src/Api/SecretsManager/Controllers/SecretsController.cs:214-229

Two existing tests already assert the behavior the new short-circuits remove — RestoreVersion_ServiceAccount_NoWriteAccess_Throws and GetVersionsBySecretId_ServiceAccount_NoReadAccess_Throws in test/Api.Test/SecretsManager/Controllers/SecretVersionsControllerTests.cs. No test changes accompany the new event-logging or the new identity-type branches; per the repository guidance, new feature work should ship with unit tests covering LogSecretEventAsync on the restore path.

Several of these defects (the duplicate declaration and the reverted VersionDate) appear to originate from the conflict resolution in merge commit 027a6fa, which regressed code that was correct on the pre-merge branch. Re-checking that merge against 027a6fa^1 may be faster than fixing the symptoms individually.

@cd-bitwarden cd-bitwarden added the t:feature Change Type - Feature Development label Sep 9, 2026
}

// Store the current value before restoration
var currentValue = secret.Value;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

CRITICAL: currentValue is declared twice in the same method scope — the Api project does not compile.

Details and fix

var currentValue = secret.Value; is declared here at line 188, and again at line 235 further down in the same method body (no intervening scope). That is CS0128: A local variable named 'currentValue' is already defined in this scope.

This looks like a bad conflict resolution in the merge commit 027a6fa: on the pre-merge branch (027a6fa^1) there was a single declaration at line 207 that both paths shared, and line 255 simply reused it. The merge reintroduced the old declaration.

Fix: delete line 235 (var currentValue = secret.Value;) so the user path reuses the value captured here.

Corroboration: Build Docker images (Api, ./src, true, Core backend API) and Run tests are both currently failing on this PR.

var currentValueRevisionDate = secret.RevisionDate;

// For service accounts and organization API, skip user-level access checks
if (_currentContext.IdentityClientType == IdentityClientType.ServiceAccount)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

CRITICAL: This early return lets any service account overwrite any secret in the organization, bypassing the per-secret write check.

Details and fix

The only authorization performed before this branch is _currentContext.AccessSecretsManager(secret.OrganizationId) (line 176). For a machine account that check is organization-wide:

// CurrentContext.AccessSecretsManager
if (ServiceAccountOrganizationId.HasValue && ServiceAccountOrganizationId.Value == orgId)
{
    return true;
}

It says nothing about whether this service account has an access policy on this secret. By returning before line 229's AccessToSecretAsync(secretId, userId.Value, accessClient) / !access.Write gate, a machine account with no policy on the secret — or a read-only policy — can restore an arbitrary version and overwrite secret.Value.

AccessClientHelper.ToAccessClient maps IdentityClientType.ServiceAccount to AccessClientType.ServiceAccount, a real access-checked client, so the pre-existing path was already correct for service accounts; there is nothing here that the check below would have wrongly rejected.

Fix: drop this branch and let service accounts fall through to the existing access.Write check. The snapshot logic below already handles IdentityClientType.ServiceAccount at line 241.

This also regresses RestoreVersion_ServiceAccount_NoWriteAccess_Throws in test/Api.Test/SecretsManager/Controllers/SecretVersionsControllerTests.cs:338, which asserts exactly this behavior.

Comment on lines +53 to +60
if (_currentContext.IdentityClientType == IdentityClientType.ServiceAccount ||
_currentContext.IdentityClientType == IdentityClientType.Organization)
{
// Already verified Secrets Manager access above
var versionList = await _secretVersionRepository.GetManyBySecretIdAsync(secretId);
var responseList = versionList.Select(v => new SecretVersionResponseModel(v)).ToList();
return new ListResponseModel<SecretVersionResponseModel>(responseList);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

CRITICAL: This early return exposes every historical secret value to any service account in the organization, bypassing the per-secret read check.

Details and fix

The comment says "Already verified Secrets Manager access above", but line 47 only checked _currentContext.AccessSecretsManager(secret.OrganizationId), which returns true for any secret in the machine account's own organization:

// CurrentContext.AccessSecretsManager
if (ServiceAccountOrganizationId.HasValue && ServiceAccountOrganizationId.Value == orgId)
{
    return true;
}

Returning here skips the AccessToSecretAsync(...) / !access.Read gate at lines 71-75. SecretVersionResponseModel carries Value, so a service account with no access policy on the secret receives its full version history.

Fix: remove this branch. AccessClientHelper.ToAccessClient already maps ServiceAccount and Organization to access-checked client types, so the existing path handles both correctly — the same pattern used by SecretsController.GetAsync and by GetByIdAsync/GetManyByIdsAsync in this very file.

This also regresses GetVersionsBySecretId_ServiceAccount_NoReadAccess_Throws in test/Api.Test/SecretsManager/Controllers/SecretVersionsControllerTests.cs:316.

{
SecretId = secretId,
Value = currentValue!,
VersionDate = currentValueRevisionDate,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ IMPORTANT: The version-date fix was applied only to the service account path; the user restore path still stamps DateTime.UtcNow.

Details and fix

This branch correctly records VersionDate = currentValueRevisionDate — the revision date of the value being snapshotted — matching the fix made in SecretsController.UpdateSecretAsync (VersionDate = valueRevisionDate).

The user path at line 260 still uses VersionDate = DateTime.UtcNow, so the same endpoint records a different version date depending on the caller's identity type, and the user case still shows the restore time rather than the value's own revision date. That contradicts the objective "ensure the version date is correct".

The pre-merge branch (027a6fa^1, line 267) had VersionDate = currentValueRevisionDate here, so this looks like it was reverted by the same merge that introduced the duplicate currentValue declaration.

Fix: change line 260 to VersionDate = currentValueRevisionDate, — which also resolves once the duplicate declaration at line 235 is removed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ai-review Request a Claude code review t:feature Change Type - Feature Development

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants