Skip to content

[PM-33041] Organization Ability: Refactor CipherResponseModel#7202

Open
JimmyVo16 wants to merge 8 commits intomainfrom
ac/pm-33041/organization-ability-refactor-cipherresponsemodel
Open

[PM-33041] Organization Ability: Refactor CipherResponseModel#7202
JimmyVo16 wants to merge 8 commits intomainfrom
ac/pm-33041/organization-ability-refactor-cipherresponsemodel

Conversation

@JimmyVo16
Copy link
Contributor

@JimmyVo16 JimmyVo16 commented Mar 11, 2026

🎟️ Tracking

https://bitwarden.atlassian.net/browse/PM-33041

📔 Objective

  1. Refactor CipherResponseModel to accept a single organizationAbility instead of a dictionary of organizationAbility.
  2. Refactor endpoints that reference CipherResponseModel to move off the deprecated GetOrganizationAbilitiesAsync(), which will enable us to migrate to the new caching pattern.
    This is the main point of the PR.
  3. Fix unit tests.

📸 Screenshots

Quick sanity tests on GET /sync.

image

@github-actions
Copy link
Contributor

github-actions bot commented Mar 11, 2026

Logo
Checkmarx One – Scan Summary & Detailse1075da7-2bb1-4b0b-9434-d86f1b6ee1f4


New Issues (2) Checkmarx found the following issues in this Pull Request
# Severity Issue Source File / Package Checkmarx Insight
1 MEDIUM CSRF /src/Api/Vault/Controllers/CiphersController.cs: 1545
detailsMethod at line 1545 of /src/Api/Vault/Controllers/CiphersController.cs gets a parameter from a user request from id. This parameter value flows ...
Attack Vector
2 MEDIUM CSRF /src/Api/Vault/Controllers/CiphersController.cs: 1379
detailsMethod at line 1379 of /src/Api/Vault/Controllers/CiphersController.cs gets a parameter from a user request from id. This parameter value flows ...
Attack Vector

Fixed Issues (2) Great job! The following issues were fixed in this Pull Request
Severity Issue Source File / Package
MEDIUM CSRF /src/Api/Vault/Controllers/CiphersController.cs: 738
MEDIUM Use_Of_Hardcoded_Password /src/Core/Constants.cs: 208

@JimmyVo16 JimmyVo16 added the ai-review Request a Claude code review label Mar 12, 2026
@JimmyVo16 JimmyVo16 self-assigned this Mar 12, 2026
@codecov
Copy link

codecov bot commented Mar 12, 2026

Codecov Report

❌ Patch coverage is 39.02439% with 50 lines in your changes missing coverage. Please review.
✅ Project coverage is 57.51%. Comparing base (0ad7a10) to head (ceb3dd3).
⚠️ Report is 16 commits behind head on main.

Files with missing lines Patch % Lines
src/Api/Vault/Controllers/CiphersController.cs 21.56% 40 Missing ⚠️
src/Api/Vault/Models/Response/SyncResponseModel.cs 36.36% 7 Missing ⚠️
...c/Api/Vault/Models/Response/CipherResponseModel.cs 50.00% 2 Missing ⚠️
.../Models/Response/CipherPermissionsResponseModel.cs 50.00% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #7202      +/-   ##
==========================================
+ Coverage   57.45%   57.51%   +0.06%     
==========================================
  Files        2032     2032              
  Lines       89415    89547     +132     
  Branches     7951     7965      +14     
==========================================
+ Hits        51369    51499     +130     
+ Misses      36203    36202       -1     
- Partials     1843     1846       +3     

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

@JimmyVo16 JimmyVo16 marked this pull request as ready for review March 12, 2026 20:38
@JimmyVo16 JimmyVo16 requested review from a team as code owners March 12, 2026 20:38
@claude
Copy link
Contributor

claude bot commented Mar 13, 2026

Claude finished @JimmyVo16's task in 2m 5s —— View job


Overall Assessment: APPROVE

Reviewed the latest commit (ceb3dd3) addressing prior code review feedback. The two issues raised have been resolved: PutPartial now re-fetches the cipher after UpdatePartialAsync to return fresh data, and the EmergencyAccessResponseModel comment correctly references organizationAbility (singular).

Code Review Details

No new findings. Prior findings have been addressed:

  • PutPartial stale data (previously flagged as IMPORTANT): Fixed. The cipher is now re-fetched after the partial update before building the response model.
  • Comment accuracy (raised by reviewer): Fixed. Comment now references organizationAbility instead of organizationAbilities.
  • DRY duplication (previously flagged as SUGGESTED): Still present across CiphersController, SyncController, and SyncResponseModel. This is a low-priority suggestion and does not block approval.

@claude
Copy link
Contributor

claude bot commented Mar 13, 2026

Pull Request Review

Objective: Refactor CipherResponseModel to accept a single OrganizationAbility instead of a dictionary, moving off the deprecated GetOrganizationAbilitiesAsync() to enable migration to the new caching pattern.

Overall the refactor direction is sound and aligns well with the stated goal. The SyncController batch approach is well thought out. I found one correctness issue and one performance concern detailed in inline comments below.

Findings

Severity File Description
⚠️ Important CiphersController.cs:709 PutPartial returns stale FolderId/Favorite after update due to removed re-fetch
🎨 Suggested CiphersController.cs:154 N+1 async cache lookups in list endpoints; consider batching like SyncController

@claude
Copy link
Contributor

claude bot commented Mar 13, 2026

test

@claude
Copy link
Contributor

claude bot commented Mar 13, 2026

Important -- Correctness: PutPartial returns stale data

File: src/Api/Vault/Controllers/CiphersController.cs, line 709

The previous code re-fetched the cipher after UpdatePartialAsync to return the updated state. The new code reuses the pre-update cipher object. Since CipherResponseModel reads FolderId and Favorite directly from the cipher object (CipherResponseModel.cs lines 126-127), the response will return stale values for both fields. The database is updated correctly, but the immediate API response will not reflect the change.

To fix, either re-fetch the cipher after the update (as before), or mutate the in-memory object before building the response:

cipher.FolderId = folderId;
cipher.Favorite = model.Favorite;


Suggested -- Performance: N+1 async cache lookups in list endpoints

File: src/Api/Vault/Controllers/CiphersController.cs, lines 151-156 (also 918, 1122)

In list endpoints (Get_ManyByListCiphers, bulk archive, bulk unarchive), each cipher triggers an individual GetOrganizationAbilityAsync call inside Task.WhenAll. While the underlying cache is in-memory, this creates N async state machines and N Task allocations instead of a single batch fetch. The SyncController took the right approach: extract distinct org IDs, batch-fetch into a dictionary via GetOrganizationAbilitiesAsync(orgIds), then do a simple dictionary lookup per cipher. Consider applying the same pattern in CiphersController for consistency and to avoid unnecessary overhead in large vaults.

user,
await _applicationCacheService.GetOrganizationAbilitiesAsync(),
_globalSettings);
var response = new CipherResponseModel(cipher, user, await GetOrganizationAbilityAsync(cipher), _globalSettings);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Important — Correctness: PutPartial returns stale FolderId/Favorite

This issue was flagged in the previous review and is still present. The cipher object is fetched before UpdatePartialAsync runs. Since CipherResponseModel reads FolderId and Favorite from the cipher object (lines 126-127 of CipherResponseModel.cs), the response will return the pre-update values.

The previous code re-fetched the cipher after the update. Either restore that re-fetch, or mutate the in-memory object before building the response:

Suggested change
var response = new CipherResponseModel(cipher, user, await GetOrganizationAbilityAsync(cipher), _globalSettings);
cipher.FolderId = folderId;
cipher.Favorite = model.Favorite;
var response = new CipherResponseModel(cipher, user, await GetOrganizationAbilityAsync(cipher), _globalSettings);

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good suggestion. Originally, I didn’t see that I accidentally deleted the updatedCipher and was confused. Re-adding the changes now.

Comment on lines +145 to +161
private async Task<IDictionary<Guid, OrganizationAbility>> GetOrganizationAbilitiesAsync(ICollection<CipherDetails> ciphers)
{
var orgIds = ciphers
.Where(c => c.OrganizationId.HasValue)
.Select(c => c.OrganizationId!.Value)
.Distinct()
.ToList();

if (orgIds.Count == 0)
{
return new Dictionary<Guid, OrganizationAbility>();
}

var organizationAbilities = await _applicationCacheService.GetOrganizationAbilitiesAsync(orgIds);

return organizationAbilities;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🎨 Suggested — DRY: Duplicated helper method

This method is nearly identical to CiphersController.GetOrganizationAbilitiesAsync (lines 1682-1697). Similarly, SyncResponseModel.GetOrganizationAbility duplicates CiphersController.GetOrganizationAbility. Consider extracting these into a shared static helper (e.g., on a base class or a small utility) to avoid maintaining three copies of the same logic.

Comment on lines +115 to 116
#nullable enable
public class CipherResponseModel : CipherMiniResponseModel
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Nitpick — Nullable scope: The #nullable enable here only covers CipherResponseModel and below, while CipherMiniResponseModel remains under #nullable disable. This is fine for this PR, but the split placement (disable at top of namespace, enable mid-file) is a bit unusual. The existing FIXME comment acknowledges this — just noting it for awareness.

cipher,
user,
organizationAbilities: null, // Emergency access only retrieves personal ciphers so organizationAbilities is not needed
null, // Emergency access only retrieves personal ciphers so organizationAbilities is not needed
Copy link
Contributor

Choose a reason for hiding this comment

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

The comment is no longer accurate: organizationAbilities was changed to organizationAbility?

var folderId = string.IsNullOrWhiteSpace(model.FolderId) ? null : (Guid?)new Guid(model.FolderId);
await _cipherRepository.UpdatePartialAsync(id, user.Id, folderId, model.Favorite);

var updatedCipher = await GetByIdAsync(id, user.Id);
Copy link
Contributor

Choose a reason for hiding this comment

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

❓ Was there an scenario that caused you to remove this call to get the updated cipher? Claude is also pointing out that this would lead to stale data.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Addressed this in the Claude's comment.

@sonarqubecloud
Copy link

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants