1
0
mirror of https://github.com/bitwarden/server synced 2025-12-06 00:03:34 +00:00

[PM-25372] Filter out DefaultUserCollections from CiphersController.GetAssignedOrganizationCiphers (#6274)

Co-authored-by: Jimmy Vo <huynhmaivo82@gmail.com>
This commit is contained in:
Rui Tomé
2025-09-17 17:43:27 +01:00
committed by GitHub
parent 57f891f391
commit d83395aeb0
8 changed files with 162 additions and 2 deletions

View File

@@ -8,6 +8,7 @@ public interface ICollectionCipherRepository
{
Task<ICollection<CollectionCipher>> GetManyByUserIdAsync(Guid userId);
Task<ICollection<CollectionCipher>> GetManyByOrganizationIdAsync(Guid organizationId);
Task<ICollection<CollectionCipher>> GetManySharedByOrganizationIdAsync(Guid organizationId);
Task<ICollection<CollectionCipher>> GetManyByUserIdCipherIdAsync(Guid userId, Guid cipherId);
Task UpdateCollectionsAsync(Guid cipherId, Guid userId, IEnumerable<Guid> collectionIds);
Task UpdateCollectionsForAdminAsync(Guid cipherId, Guid organizationId, IEnumerable<Guid> collectionIds);

View File

@@ -24,7 +24,7 @@ public class OrganizationCiphersQuery : IOrganizationCiphersQuery
var orgCiphers = ciphers.Where(c => c.OrganizationId == organizationId).ToList();
var orgCipherIds = orgCiphers.Select(c => c.Id);
var collectionCiphers = await _collectionCipherRepository.GetManyByOrganizationIdAsync(organizationId);
var collectionCiphers = await _collectionCipherRepository.GetManySharedByOrganizationIdAsync(organizationId);
var collectionCiphersGroupDict = collectionCiphers
.Where(c => orgCipherIds.Contains(c.CipherId))
.GroupBy(c => c.CipherId).ToDictionary(s => s.Key);

View File

@@ -45,6 +45,19 @@ public class CollectionCipherRepository : BaseRepository, ICollectionCipherRepos
}
}
public async Task<ICollection<CollectionCipher>> GetManySharedByOrganizationIdAsync(Guid organizationId)
{
using (var connection = new SqlConnection(ConnectionString))
{
var results = await connection.QueryAsync<CollectionCipher>(
"[dbo].[CollectionCipher_ReadSharedByOrganizationId]",
new { OrganizationId = organizationId },
commandType: CommandType.StoredProcedure);
return results.ToList();
}
}
public async Task<ICollection<CollectionCipher>> GetManyByUserIdCipherIdAsync(Guid userId, Guid cipherId)
{
using (var connection = new SqlConnection(ConnectionString))

View File

@@ -47,6 +47,21 @@ public class CollectionCipherRepository : BaseEntityFrameworkRepository, ICollec
}
}
public async Task<ICollection<CollectionCipher>> GetManySharedByOrganizationIdAsync(Guid organizationId)
{
using (var scope = ServiceScopeFactory.CreateScope())
{
var dbContext = GetDatabaseContext(scope);
var data = await (from cc in dbContext.CollectionCiphers
join c in dbContext.Collections
on cc.CollectionId equals c.Id
where c.OrganizationId == organizationId
&& c.Type == Core.Enums.CollectionType.SharedCollection
select cc).ToArrayAsync();
return data;
}
}
public async Task<ICollection<CollectionCipher>> GetManyByUserIdAsync(Guid userId)
{
using (var scope = ServiceScopeFactory.CreateScope())

View File

@@ -0,0 +1,17 @@
CREATE PROCEDURE [dbo].[CollectionCipher_ReadSharedByOrganizationId]
@OrganizationId UNIQUEIDENTIFIER
AS
BEGIN
SET NOCOUNT ON
SELECT
CC.[CollectionId],
CC.[CipherId]
FROM
[dbo].[CollectionCipher] CC
INNER JOIN
[dbo].[Collection] C ON C.[Id] = CC.[CollectionId]
WHERE
C.[OrganizationId] = @OrganizationId
AND C.[Type] = 0 -- SharedCollections only
END

View File

@@ -14,6 +14,6 @@ GO
CREATE NONCLUSTERED INDEX [IX_Collection_OrganizationId_IncludeAll]
ON [dbo].[Collection]([OrganizationId] ASC)
INCLUDE([CreationDate], [Name], [RevisionDate]);
INCLUDE([CreationDate], [Name], [RevisionDate], [Type]);
GO

View File

@@ -0,0 +1,84 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.Billing.Enums;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Repositories;
using Bit.Core.Vault.Entities;
using Bit.Core.Vault.Enums;
using Bit.Core.Vault.Repositories;
using Xunit;
namespace Bit.Infrastructure.IntegrationTest.Vault.Repositories;
public class CollectionCipherRepositoryTests
{
[Theory, DatabaseData]
public async Task GetManySharedByOrganizationIdAsync_OnlyReturnsSharedCollections(
IOrganizationRepository organizationRepository,
ICollectionRepository collectionRepository,
ICipherRepository cipherRepository,
ICollectionCipherRepository collectionCipherRepository)
{
// Arrange
var organization = await organizationRepository.CreateAsync(new Organization
{
Name = "Test Org",
PlanType = PlanType.EnterpriseAnnually,
Plan = "Enterprise",
BillingEmail = "billing@example.com"
});
var sharedCollection = await collectionRepository.CreateAsync(new Collection
{
Name = "Shared Collection",
OrganizationId = organization.Id,
Type = CollectionType.SharedCollection
});
var defaultUserCollection = await collectionRepository.CreateAsync(new Collection
{
Name = "Default User Collection",
OrganizationId = organization.Id,
Type = CollectionType.DefaultUserCollection
});
var sharedCipher = await cipherRepository.CreateAsync(new Cipher
{
Type = CipherType.Login,
OrganizationId = organization.Id,
Data = ""
});
var defaultCipher = await cipherRepository.CreateAsync(new Cipher
{
Type = CipherType.Login,
OrganizationId = organization.Id,
Data = ""
});
await collectionCipherRepository.AddCollectionsForManyCiphersAsync(
organization.Id,
new[] { sharedCipher.Id },
new[] { sharedCollection.Id });
await collectionCipherRepository.AddCollectionsForManyCiphersAsync(
organization.Id,
new[] { defaultCipher.Id },
new[] { defaultUserCollection.Id });
// Act
var result = await collectionCipherRepository.GetManySharedByOrganizationIdAsync(organization.Id);
// Assert
Assert.Single(result);
Assert.Equal(sharedCollection.Id, result.First().CollectionId);
Assert.DoesNotContain(result, cc => cc.CollectionId == defaultUserCollection.Id);
// Cleanup
await cipherRepository.DeleteAsync(sharedCipher);
await cipherRepository.DeleteAsync(defaultCipher);
await collectionRepository.DeleteAsync(sharedCollection);
await collectionRepository.DeleteAsync(defaultUserCollection);
await organizationRepository.DeleteAsync(organization);
}
}

View File

@@ -0,0 +1,30 @@
CREATE OR ALTER PROCEDURE [dbo].[CollectionCipher_ReadSharedByOrganizationId]
@OrganizationId UNIQUEIDENTIFIER
AS
BEGIN
SET NOCOUNT ON
SELECT
CC.[CollectionId],
CC.[CipherId]
FROM
[dbo].[CollectionCipher] CC
INNER JOIN
[dbo].[Collection] C ON C.[Id] = CC.[CollectionId]
WHERE
C.[OrganizationId] = @OrganizationId
AND C.[Type] = 0 -- SharedCollections only
END
GO
-- Update [IX_Collection_OrganizationId_IncludeAll] index to include [Type] column
IF EXISTS(SELECT name FROM sys.indexes WHERE name = 'IX_Collection_OrganizationId_IncludeAll' AND object_id = OBJECT_ID('[dbo].[Collection]'))
BEGIN
DROP INDEX [IX_Collection_OrganizationId_IncludeAll] ON [dbo].[Collection]
END
GO
CREATE NONCLUSTERED INDEX [IX_Collection_OrganizationId_IncludeAll]
ON [dbo].[Collection]([OrganizationId] ASC)
INCLUDE([CreationDate], [Name], [RevisionDate], [Type])
GO