mirror of
https://github.com/bitwarden/server
synced 2025-12-16 00:03:54 +00:00
[PM-25982] Restrict Ciphers being assigned to Default from Shared collections (#6382)
* validate that any change in collection does not allow only shared ciphers to migrate to a default cipher * refactor order of checks to avoid any unnecessary calls * remove unneeded conditional
This commit is contained in:
@@ -887,6 +887,9 @@ public class CiphersController : Controller
|
|||||||
[HttpPost("bulk-collections")]
|
[HttpPost("bulk-collections")]
|
||||||
public async Task PostBulkCollections([FromBody] CipherBulkUpdateCollectionsRequestModel model)
|
public async Task PostBulkCollections([FromBody] CipherBulkUpdateCollectionsRequestModel model)
|
||||||
{
|
{
|
||||||
|
var userId = _userService.GetProperUserId(User).Value;
|
||||||
|
await _cipherService.ValidateBulkCollectionAssignmentAsync(model.CollectionIds, model.CipherIds, userId);
|
||||||
|
|
||||||
if (!await CanModifyCipherCollectionsAsync(model.OrganizationId, model.CipherIds) ||
|
if (!await CanModifyCipherCollectionsAsync(model.OrganizationId, model.CipherIds) ||
|
||||||
!await CanEditItemsInCollections(model.OrganizationId, model.CollectionIds))
|
!await CanEditItemsInCollections(model.OrganizationId, model.CollectionIds))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -37,4 +37,5 @@ public interface ICipherService
|
|||||||
Task UploadFileForExistingAttachmentAsync(Stream stream, Cipher cipher, CipherAttachment.MetaData attachmentId);
|
Task UploadFileForExistingAttachmentAsync(Stream stream, Cipher cipher, CipherAttachment.MetaData attachmentId);
|
||||||
Task<AttachmentResponseData> GetAttachmentDownloadDataAsync(Cipher cipher, string attachmentId);
|
Task<AttachmentResponseData> GetAttachmentDownloadDataAsync(Cipher cipher, string attachmentId);
|
||||||
Task<bool> ValidateCipherAttachmentFile(Cipher cipher, CipherAttachment.MetaData attachmentData);
|
Task<bool> ValidateCipherAttachmentFile(Cipher cipher, CipherAttachment.MetaData attachmentData);
|
||||||
|
Task ValidateBulkCollectionAssignmentAsync(IEnumerable<Guid> collectionIds, IEnumerable<Guid> cipherIds, Guid userId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -170,6 +170,7 @@ public class CipherService : ICipherService
|
|||||||
{
|
{
|
||||||
ValidateCipherLastKnownRevisionDateAsync(cipher, lastKnownRevisionDate);
|
ValidateCipherLastKnownRevisionDateAsync(cipher, lastKnownRevisionDate);
|
||||||
cipher.RevisionDate = DateTime.UtcNow;
|
cipher.RevisionDate = DateTime.UtcNow;
|
||||||
|
await ValidateChangeInCollectionsAsync(cipher, collectionIds, savingUserId);
|
||||||
await ValidateViewPasswordUserAsync(cipher);
|
await ValidateViewPasswordUserAsync(cipher);
|
||||||
await _cipherRepository.ReplaceAsync(cipher);
|
await _cipherRepository.ReplaceAsync(cipher);
|
||||||
await _eventService.LogCipherEventAsync(cipher, Bit.Core.Enums.EventType.Cipher_Updated);
|
await _eventService.LogCipherEventAsync(cipher, Bit.Core.Enums.EventType.Cipher_Updated);
|
||||||
@@ -539,6 +540,7 @@ public class CipherService : ICipherService
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
await ValidateCipherCanBeShared(cipher, sharingUserId, organizationId, lastKnownRevisionDate);
|
await ValidateCipherCanBeShared(cipher, sharingUserId, organizationId, lastKnownRevisionDate);
|
||||||
|
await ValidateChangeInCollectionsAsync(cipher, collectionIds, sharingUserId);
|
||||||
|
|
||||||
// Sproc will not save this UserId on the cipher. It is used limit scope of the collectionIds.
|
// Sproc will not save this UserId on the cipher. It is used limit scope of the collectionIds.
|
||||||
cipher.UserId = sharingUserId;
|
cipher.UserId = sharingUserId;
|
||||||
@@ -678,6 +680,7 @@ public class CipherService : ICipherService
|
|||||||
{
|
{
|
||||||
throw new BadRequestException("Cipher must belong to an organization.");
|
throw new BadRequestException("Cipher must belong to an organization.");
|
||||||
}
|
}
|
||||||
|
await ValidateChangeInCollectionsAsync(cipher, collectionIds, savingUserId);
|
||||||
|
|
||||||
cipher.RevisionDate = DateTime.UtcNow;
|
cipher.RevisionDate = DateTime.UtcNow;
|
||||||
|
|
||||||
@@ -820,6 +823,15 @@ public class CipherService : ICipherService
|
|||||||
return restoringCiphers;
|
return restoringCiphers;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task ValidateBulkCollectionAssignmentAsync(IEnumerable<Guid> collectionIds, IEnumerable<Guid> cipherIds, Guid userId)
|
||||||
|
{
|
||||||
|
foreach (var cipherId in cipherIds)
|
||||||
|
{
|
||||||
|
var cipher = await _cipherRepository.GetByIdAsync(cipherId);
|
||||||
|
await ValidateChangeInCollectionsAsync(cipher, collectionIds, userId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async Task<bool> UserCanEditAsync(Cipher cipher, Guid userId)
|
private async Task<bool> UserCanEditAsync(Cipher cipher, Guid userId)
|
||||||
{
|
{
|
||||||
if (!cipher.OrganizationId.HasValue && cipher.UserId.HasValue && cipher.UserId.Value == userId)
|
if (!cipher.OrganizationId.HasValue && cipher.UserId.HasValue && cipher.UserId.Value == userId)
|
||||||
@@ -1038,6 +1050,44 @@ public class CipherService : ICipherService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validates that a cipher is not being added to a default collection when it is only currently only in shared collections
|
||||||
|
private async Task ValidateChangeInCollectionsAsync(Cipher updatedCipher, IEnumerable<Guid> newCollectionIds, Guid userId)
|
||||||
|
{
|
||||||
|
|
||||||
|
if (updatedCipher.Id == Guid.Empty || !updatedCipher.OrganizationId.HasValue)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var currentCollectionsForCipher = await _collectionCipherRepository.GetManyByUserIdCipherIdAsync(userId, updatedCipher.Id);
|
||||||
|
|
||||||
|
if (!currentCollectionsForCipher.Any())
|
||||||
|
{
|
||||||
|
// When a cipher is not currently in any collections it can be assigned to any type of collection
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var currentCollections = await _collectionRepository.GetManyByManyIdsAsync(currentCollectionsForCipher.Select(c => c.CollectionId));
|
||||||
|
|
||||||
|
var currentCollectionsContainDefault = currentCollections.Any(c => c.Type == CollectionType.DefaultUserCollection);
|
||||||
|
|
||||||
|
// When the current cipher already contains the default collection, no check is needed for if they added or removed
|
||||||
|
// a default collection, because it is already there.
|
||||||
|
if (currentCollectionsContainDefault)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var newCollections = await _collectionRepository.GetManyByManyIdsAsync(newCollectionIds);
|
||||||
|
var newCollectionsContainDefault = newCollections.Any(c => c.Type == CollectionType.DefaultUserCollection);
|
||||||
|
|
||||||
|
if (newCollectionsContainDefault)
|
||||||
|
{
|
||||||
|
// User is trying to add the default collection when the cipher is only in shared collections
|
||||||
|
throw new BadRequestException("The cipher(s) cannot be assigned to a default collection when only assigned to non-default collections.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private string SerializeCipherData(CipherData data)
|
private string SerializeCipherData(CipherData data)
|
||||||
{
|
{
|
||||||
return data switch
|
return data switch
|
||||||
|
|||||||
Reference in New Issue
Block a user