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:
@@ -8,6 +8,7 @@ public interface ICollectionCipherRepository
|
|||||||
{
|
{
|
||||||
Task<ICollection<CollectionCipher>> GetManyByUserIdAsync(Guid userId);
|
Task<ICollection<CollectionCipher>> GetManyByUserIdAsync(Guid userId);
|
||||||
Task<ICollection<CollectionCipher>> GetManyByOrganizationIdAsync(Guid organizationId);
|
Task<ICollection<CollectionCipher>> GetManyByOrganizationIdAsync(Guid organizationId);
|
||||||
|
Task<ICollection<CollectionCipher>> GetManySharedByOrganizationIdAsync(Guid organizationId);
|
||||||
Task<ICollection<CollectionCipher>> GetManyByUserIdCipherIdAsync(Guid userId, Guid cipherId);
|
Task<ICollection<CollectionCipher>> GetManyByUserIdCipherIdAsync(Guid userId, Guid cipherId);
|
||||||
Task UpdateCollectionsAsync(Guid cipherId, Guid userId, IEnumerable<Guid> collectionIds);
|
Task UpdateCollectionsAsync(Guid cipherId, Guid userId, IEnumerable<Guid> collectionIds);
|
||||||
Task UpdateCollectionsForAdminAsync(Guid cipherId, Guid organizationId, IEnumerable<Guid> collectionIds);
|
Task UpdateCollectionsForAdminAsync(Guid cipherId, Guid organizationId, IEnumerable<Guid> collectionIds);
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ public class OrganizationCiphersQuery : IOrganizationCiphersQuery
|
|||||||
var orgCiphers = ciphers.Where(c => c.OrganizationId == organizationId).ToList();
|
var orgCiphers = ciphers.Where(c => c.OrganizationId == organizationId).ToList();
|
||||||
var orgCipherIds = orgCiphers.Select(c => c.Id);
|
var orgCipherIds = orgCiphers.Select(c => c.Id);
|
||||||
|
|
||||||
var collectionCiphers = await _collectionCipherRepository.GetManyByOrganizationIdAsync(organizationId);
|
var collectionCiphers = await _collectionCipherRepository.GetManySharedByOrganizationIdAsync(organizationId);
|
||||||
var collectionCiphersGroupDict = collectionCiphers
|
var collectionCiphersGroupDict = collectionCiphers
|
||||||
.Where(c => orgCipherIds.Contains(c.CipherId))
|
.Where(c => orgCipherIds.Contains(c.CipherId))
|
||||||
.GroupBy(c => c.CipherId).ToDictionary(s => s.Key);
|
.GroupBy(c => c.CipherId).ToDictionary(s => s.Key);
|
||||||
|
|||||||
@@ -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)
|
public async Task<ICollection<CollectionCipher>> GetManyByUserIdCipherIdAsync(Guid userId, Guid cipherId)
|
||||||
{
|
{
|
||||||
using (var connection = new SqlConnection(ConnectionString))
|
using (var connection = new SqlConnection(ConnectionString))
|
||||||
|
|||||||
@@ -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)
|
public async Task<ICollection<CollectionCipher>> GetManyByUserIdAsync(Guid userId)
|
||||||
{
|
{
|
||||||
using (var scope = ServiceScopeFactory.CreateScope())
|
using (var scope = ServiceScopeFactory.CreateScope())
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -14,6 +14,6 @@ GO
|
|||||||
|
|
||||||
CREATE NONCLUSTERED INDEX [IX_Collection_OrganizationId_IncludeAll]
|
CREATE NONCLUSTERED INDEX [IX_Collection_OrganizationId_IncludeAll]
|
||||||
ON [dbo].[Collection]([OrganizationId] ASC)
|
ON [dbo].[Collection]([OrganizationId] ASC)
|
||||||
INCLUDE([CreationDate], [Name], [RevisionDate]);
|
INCLUDE([CreationDate], [Name], [RevisionDate], [Type]);
|
||||||
GO
|
GO
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
Reference in New Issue
Block a user