Skip to content

Conversation

jaasen-livefront
Copy link
Collaborator

@jaasen-livefront jaasen-livefront commented Jun 20, 2025

🎟️ Tracking

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

📔 Objective

This PR updates the GetOrganizationCiphers action in the ciphers controller to filter out the default user collections if the ExcludeDefaultUserCollections flag is enabled.

Summary of changes:

  • add feature flag ExcludeDefaultUserCollections
  • add sproc CipherOrganizationDetails_ReadByOrganizationIdExcludingDefaultCollections that gathers all the required ciphers and data and excludes the default collection ciphers

📸 Screenshots

⏰ Reminders before review

  • Contributor guidelines followed
  • All formatters and local linters executed and passed
  • Written new unit and / or integration tests where applicable
  • Protected functional changes with optionality (feature flags)
  • Used internationalization (i18n) for all UI strings
  • CI builds passed
  • Communicated to DevOps any deployment requirements
  • Updated any necessary documentation (Confluence, contributing docs) or informed the documentation team

🦮 Reviewer guidelines

  • 👍 (:+1:) or similar for great changes
  • 📝 (:memo:) or ℹ️ (:information_source:) for notes or general info
  • ❓ (:question:) for questions
  • 🤔 (:thinking:) or 💭 (:thought_balloon:) for more open inquiry that's not quite a confirmed issue and could potentially benefit from discussion
  • 🎨 (:art:) for suggestions / improvements
  • ❌ (:x:) or ⚠️ (:warning:) for more significant problems or concerns needing attention
  • 🌱 (:seedling:) or ♻️ (:recycle:) for future improvements or indications of technical debt
  • ⛏ (:pick:) for minor or nitpick changes

Copy link
Contributor

github-actions bot commented Jun 20, 2025

Logo
Checkmarx One – Scan Summary & Details2f287dc7-08aa-444d-bd34-5414096499dc

New Issues (5)

Checkmarx found the following issues in this Pull Request

Severity Issue Source File / Package Checkmarx Insight
MEDIUM CSRF /src/Api/Vault/Controllers/CiphersController.cs: 992
detailsMethod at line 992 of /src/Api/Vault/Controllers/CiphersController.cs gets a parameter from a user request from id. This parameter value flows t...
ID: Xaf55557GdFP5%2FOCkAzyednr1oo%3D
Attack Vector
MEDIUM CSRF /src/Api/Vault/Controllers/CiphersController.cs: 923
detailsMethod at line 923 of /src/Api/Vault/Controllers/CiphersController.cs gets a parameter from a user request from id. This parameter value flows t...
ID: dDkkgxJ%2Fa60PWzcSnQlMBTXOew0%3D
Attack Vector
MEDIUM CSRF /src/Api/AdminConsole/Controllers/OrganizationUsersController.cs: 557
detailsMethod at line 557 of /src/Api/AdminConsole/Controllers/OrganizationUsersController.cs gets a parameter from a user request from model. This par...
ID: bVhpH9B9I29J03V0RYbFCg94hQI%3D
Attack Vector
MEDIUM CSRF /src/Api/Vault/Controllers/CiphersController.cs: 1112
detailsMethod at line 1112 of /src/Api/Vault/Controllers/CiphersController.cs gets a parameter from a user request from organizationId. This parameter ...
ID: ewvRYIw2wYNFl3z%2FAg2itvIIWQ4%3D
Attack Vector
MEDIUM CSRF /src/Api/AdminConsole/Controllers/OrganizationUsersController.cs: 419
detailsMethod at line 419 of /src/Api/AdminConsole/Controllers/OrganizationUsersController.cs gets a parameter from a user request from model. This par...
ID: 2VsVEkxsA86XkPTRWjCQgqhrZJM%3D
Attack Vector
Fixed Issues (14)

Great job! The following issues were fixed in this Pull Request

Severity Issue Source File / Package
HIGH CVE-2022-37620 Npm-html-minifier-4.0.0
MEDIUM CSRF /src/Api/Vault/Controllers/CiphersController.cs: 1116
MEDIUM CSRF /src/Api/AdminConsole/Controllers/OrganizationUsersController.cs: 340
MEDIUM CSRF /src/Api/Billing/Controllers/VNext/OrganizationBillingVNextController.cs: 89
MEDIUM CSRF /src/Api/Billing/Controllers/VNext/ProviderBillingVNextController.cs: 81
MEDIUM CSRF /src/Api/Billing/Controllers/VNext/AccountBillingVNextController.cs: 57
MEDIUM CSRF /src/Api/Billing/Controllers/VNext/OrganizationBillingVNextController.cs: 43
MEDIUM CSRF /src/Api/Billing/Controllers/VNext/ProviderBillingVNextController.cs: 39
MEDIUM CSRF /src/Api/AdminConsole/Controllers/OrganizationUsersController.cs: 464
MEDIUM Use_Of_Hardcoded_Password /src/Identity/IdentityServer/RequestValidators/DeviceValidator.cs: 43
MEDIUM Use_Of_Hardcoded_Password /src/Core/KeyManagement/Sends/SendPasswordHasherServiceCollectionExtensions.cs: 14
MEDIUM Use_Of_Hardcoded_Password /src/Core/Constants.cs: 202
LOW CVE-2025-5889 Npm-brace-expansion-2.0.1
LOW CVE-2025-5889 Npm-brace-expansion-1.1.11

Copy link

codecov bot commented Jun 20, 2025

Codecov Report

❌ Patch coverage is 7.31707% with 76 lines in your changes missing coverage. Please review.
✅ Project coverage is 53.07%. Comparing base (f6cd661) to head (bd5024f).
⚠️ Report is 154 commits behind head on main.

Files with missing lines Patch % Lines
...tyFramework/Vault/Repositories/CipherRepository.cs 0.00% 41 Missing ⚠️
...ture.Dapper/Vault/Repositories/CipherRepository.cs 0.00% 33 Missing ⚠️
src/Api/Vault/Controllers/CiphersController.cs 60.00% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #5992      +/-   ##
==========================================
+ Coverage   47.85%   53.07%   +5.22%     
==========================================
  Files        1702     1770      +68     
  Lines       75534    78618    +3084     
  Branches     6799     6998     +199     
==========================================
+ Hits        36144    41724    +5580     
+ Misses      37923    35306    -2617     
- Partials     1467     1588     +121     

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

.Where(c =>
// select ciphers with no collections or none of its collections are default
!cipherGroups.TryGetValue(c.Id, out var grp)
|| !grp.Any(cc => defaultCollIds.Contains(cc.CollectionId))
Copy link
Member

Choose a reason for hiding this comment

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

⚠️ We should only exclude the cipher if it only belongs to default collection. If it belongs to multiple, then it should be included / visible.

(I think switching to !grp.All(...) should work, but there may be a better way)

public async Task<IEnumerable<CipherOrganizationDetailsWithCollections>>
GetAllOrganizationCiphersExcludingDefaultUserCollections(Guid orgId)
{
var defaultCollIds = (await _collectionRepository
Copy link
Member

Choose a reason for hiding this comment

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

💭 🎨 It would probably be beneficial to put this logic in its own sproc to avoid pulling all ciphers, collection ids, and collectionCiphers for the organization from the database every time. This method will be called semi frequently for admins in the AC.

@jaasen-livefront jaasen-livefront requested a review from a team as a code owner July 1, 2025 00:26
…tants.cs # # It looks like you may be committing a merge. # If this is not correct, please run # git update-ref -d MERGE_HEAD # and try again.
@jaasen-livefront
Copy link
Collaborator Author

@shane-melton Thanks for the feedback! Ready for another look when you have a moment.

Copy link
Member

Choose a reason for hiding this comment

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

🎨 Sorry, I probably should have been more clear. I think we should have the sproc perform the logic to filter out the ciphers that only belong to default collection. (e.g. using a CTE or JOINs on the Collection/CollectionCipher tables)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@shane-melton Ah ok gotcha. Updated. I kept the other sproc as I think it's still useful. Let me know if you feel otherwise.

Copy link
Member

Choose a reason for hiding this comment

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

I think there may still be some confusion. We should have a sproc like CipherOrganizationDetails_ReadByOrganizationIdWithoutDefaultCollections that queries all the ciphers that belong to an organization AND excludes those ciphers only belong to default collections.

Then the controller/query only has to make a single DB call and it gets exactly the result it needs to return without any filtering/rearranging at the API level.

Copy link
Contributor

@rkac-bw rkac-bw Jul 2, 2025

Choose a reason for hiding this comment

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

Maybe something like this? Would need to verify logic and test

-- View that provides organization cipher details with their collection associations
CREATE OR ALTER VIEW [dbo].[OrganizationCipherDetailsWithCollectionsView]
AS
   SELECT
       C.[Id],
       C.[UserId],
       C.[OrganizationId],
       C.[Type],
       C.[Data],
       C.[Attachments],
       C.[CreationDate],
       C.[RevisionDate],
       C.[DeletedDate],
       C.[Reprompt],
       C.[Key],
       CASE
           WHEN O.[UseTotp] = 1 THEN 1
           ELSE 0
       END AS [OrganizationUseTotp],
       CC.[CollectionId],          -- NULL when cipher has no collections (due to LEFT JOIN)
       COL.[Type] AS [CollectionType]  -- NULL when cipher has no collections OR when collection has no type set
   FROM [dbo].[Cipher] C
   INNER JOIN [dbo].[Organization] O ON C.[OrganizationId] = O.[Id]
   LEFT JOIN [dbo].[CollectionCipher] CC ON CC.[CipherId] = C.[Id]  -- LEFT JOIN ensures ciphers without collections are included
   LEFT JOIN [dbo].[Collection] COL ON CC.[CollectionId] = COL.[Id] -- LEFT JOIN to get collection details; NULL when no collection
   WHERE C.[UserId] IS NULL; -- Organization ciphers only (exclude user-owned ciphers)
GO


CREATE OR ALTER PROCEDURE
[dbo].[CipherOrganizationDetails_ReadByOrganizationIdExcludingDefaultCollections]
   @OrganizationId UNIQUEIDENTIFIER
AS
BEGIN
   SET NOCOUNT ON;

   WITH CiphersWithNoCollections AS (
       -- Ciphers with no collections at all
       SELECT
           [Id], [UserId], [OrganizationId], [Type], [Data], [Attachments],
           [CreationDate], [RevisionDate], [DeletedDate], [Reprompt], [Key], [OrganizationUseTotp]
       FROM [dbo].[OrganizationCipherDetailsWithCollectionsView]
       WHERE [OrganizationId] = @OrganizationId
           AND [CollectionId] IS NULL
   ),
   CiphersWithNullTypeCollections AS (
       -- Ciphers with collections that have NULL type (treated as non-default)
       SELECT
           [Id], [UserId], [OrganizationId], [Type], [Data], [Attachments],
           [CreationDate], [RevisionDate], [DeletedDate], [Reprompt], [Key], [OrganizationUseTotp]
       FROM [dbo].[OrganizationCipherDetailsWithCollectionsView]
       WHERE [OrganizationId] = @OrganizationId
           AND [CollectionId] IS NOT NULL
           AND [CollectionType] IS NULL
   ),
   CiphersWithNonDefaultCollections AS (
       -- Ciphers with collections that are explicitly non-default (Type != 1)
       SELECT
           [Id], [UserId], [OrganizationId], [Type], [Data], [Attachments],
           [CreationDate], [RevisionDate], [DeletedDate], [Reprompt], [Key], [OrganizationUseTotp]
       FROM [dbo].[OrganizationCipherDetailsWithCollectionsView]
       WHERE [OrganizationId] = @OrganizationId
           AND [CollectionId] IS NOT NULL
           AND [CollectionType] IS NOT NULL
           AND [CollectionType] != 1
   )
   SELECT DISTINCT
       [Id], [UserId], [OrganizationId], [Type], [Data], [Attachments],
       [CreationDate], [RevisionDate], [DeletedDate], [Reprompt], [Key], [OrganizationUseTotp]
   FROM (
       SELECT * FROM CiphersWithNoCollections
       UNION ALL  -- Use UNION ALL since we'll DISTINCT at the end
       SELECT * FROM CiphersWithNullTypeCollections
       UNION ALL
       SELECT * FROM CiphersWithNonDefaultCollections
   ) AS AllCiphers;
END
GO

Copy link
Contributor

@rkac-bw rkac-bw Jul 2, 2025

Choose a reason for hiding this comment

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

A filtered index for org only ciphers might help

CREATE INDEX IX_Cipher_OrganizationId_Filtered_OrgCiphersOnly
ON [dbo].[Cipher] ([OrganizationId]) 
INCLUDE ([Id], [Type], [Data], [Attachments], [CreationDate], [RevisionDate], [DeletedDate], [Reprompt], [Key])
WHERE [UserId] IS NULL;

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@rkac-bw @shane-melton Thanks for the feedback. I've got a solution that I think should suffice. Let me know what you think!

@jaasen-livefront jaasen-livefront requested a review from rkac-bw July 3, 2025 02:14
C.[RevisionDate],
C.[DeletedDate],
C.[Reprompt],
C.[Key],
Copy link
Contributor

Choose a reason for hiding this comment

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

So following our standard - Stored procedures should select from views and views select from tables, and this seems to duplicate the logic in the view
Separation of Concerns

Views (Complex Logic Layer):

  • Contains JOIN logic, business rules, calculated fields
  • Defines "what data looks like" - the data contract
  • Can be reused by multiple procedures
  • Changes here don't break procedure signatures

Procedures (Simple Access Layer):

  • Contains only parameterized filtering (WHERE clauses)
  • Defines "which data to get" - the access pattern
  • Focuses on security context and performance
  • Changes here don't affect data structure

Both the view and stored procedure have the same join logic

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@rkac-bw Ah yes ofc. Oversight on my part. I've trimmed out the unnecessary joins from the proc.

@jaasen-livefront jaasen-livefront requested a review from rkac-bw July 16, 2025 17:50
@jaasen-livefront
Copy link
Collaborator Author

@rkac-bw @shane-melton Ready for re-review!

rkac-bw
rkac-bw previously approved these changes Jul 25, 2025
Copy link
Contributor

@rkac-bw rkac-bw left a comment

Choose a reason for hiding this comment

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

LGTM

@jaasen-livefront jaasen-livefront removed the request for review from Jingo88 July 25, 2025 00:41
@jaasen-livefront
Copy link
Collaborator Author

@rkac-bw @shane-melton One last little tweak, updated the file name to today and added one more layer of filtering to the view:

AND O.[Enabled] = 1; -- Only enabled organizations

@jaasen-livefront
Copy link
Collaborator Author

@rkac-bw @shane-melton Should be ready for another look when you have time. Ty!

@jaasen-livefront
Copy link
Collaborator Author

@rkac-bw Thanks for the feedback! Resolved in 6155089

Copy link
Member

@shane-melton shane-melton left a comment

Choose a reason for hiding this comment

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

Looks good! Though we should update the migration script date again.

@jaasen-livefront
Copy link
Collaborator Author

Looks good! Though we should update the migration script date again.

Done!

shane-melton
shane-melton previously approved these changes Aug 6, 2025
Copy link
Member

@shane-melton shane-melton left a comment

Choose a reason for hiding this comment

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

Thank you!

END;
GO

CREATE NONCLUSTERED INDEX IX_Cipher_OrganizationId_Filtered_OrgCiphersOnly
Copy link
Contributor

Choose a reason for hiding this comment

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

Needs to be idempotent, should check if exists in case it needs to rerun:

IF NOT EXISTS (
    SELECT 1 
    FROM sys.indexes 
    WHERE name = 'IX_Cipher_OrganizationId_Filtered_OrgCiphersOnly' 
    AND object_id = OBJECT_ID('dbo.Cipher')
)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Done! 9794cbf

var query = from c in dbContext.Ciphers.AsNoTracking()
where c.UserId == null
&& c.OrganizationId == organizationId
&& (
Copy link
Contributor

Choose a reason for hiding this comment

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

Should this equal sql logic?
&& c.Organization.Enabled

from line 28 in view?
AND O.[Enabled] = 1; -- Only enabled organizations

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Agree! Done. 9794cbf

Copy link

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.

3 participants