mirror of
https://github.com/bitwarden/server.git
synced 2025-07-19 08:30:59 -05:00
[PM-18088] Implement LimitItemDeletion permission checks for all cipher operations (#5476)
* Implement enhanced cipher deletion and restore permissions with feature flag support - Add new method `CanDeleteOrRestoreCipherAsAdminAsync` in CiphersController - Update NormalCipherPermissions to support more flexible cipher type checking - Modify CipherService to use new permission checks with feature flag - Refactor test methods to support new permission logic - Improve authorization checks for organization cipher management * Refactor cipher methods to use CipherDetails and simplify type handling - Update CiphersController to use GetByIdAsync with userId - Modify NormalCipherPermissions to remove unnecessary type casting - Update ICipherService and CipherService method signatures to use CipherDetails - Remove redundant type checking in CipherService methods - Improve type consistency in cipher-related operations * Enhance CiphersControllerTests with detailed permission and feature flag scenarios - Add test methods for DeleteAdmin with edit and manage permission checks - Implement tests for LimitItemDeletion feature flag scenarios - Update test method names to reflect more precise permission conditions - Improve test coverage for admin cipher deletion with granular permission handling * Add comprehensive test coverage for admin cipher restore operations - Implement test methods for PutRestoreAdmin and PutRestoreManyAdmin - Add scenarios for owner and admin roles with LimitItemDeletion feature flag - Cover permission checks for manage and edit permissions - Enhance test coverage for single and bulk cipher restore admin operations - Verify correct invocation of RestoreAsync and RestoreManyAsync methods * Refactor CiphersControllerTests to remove redundant assertions and mocking - Remove unnecessary assertions for null checks - Simplify mocking setup for cipher repository and service methods - Clean up redundant type and data setup in test methods - Improve test method clarity by removing extraneous code * Add comprehensive test coverage for cipher restore, delete, and soft delete operations - Implement test methods for RestoreAsync with org admin override and LimitItemDeletion feature flag - Add scenarios for checking manage and edit permissions during restore operations - Extend test coverage for DeleteAsync with similar permission and feature flag checks - Enhance SoftDeleteAsync tests with org admin override and permission validation - Improve test method names to reflect precise permission conditions * Add comprehensive test coverage for cipher restore, delete, and soft delete operations - Extend test methods for RestoreManyAsync with various permission scenarios - Add test coverage for personal and organization ciphers in restore operations - Implement tests for RestoreManyAsync with LimitItemDeletion feature flag - Add detailed test scenarios for delete and soft delete operations - Improve test method names to reflect precise permission and feature flag conditions * Refactor authorization checks in CiphersController to use All() method for improved readability * Refactor filtering of ciphers in CipherService to streamline organization ability checks and improve readability
This commit is contained in:
@ -15,7 +15,7 @@ public interface ICipherService
|
||||
long requestLength, Guid savingUserId, bool orgAdmin = false);
|
||||
Task CreateAttachmentShareAsync(Cipher cipher, Stream stream, string fileName, string key, long requestLength,
|
||||
string attachmentId, Guid organizationShareId);
|
||||
Task DeleteAsync(Cipher cipher, Guid deletingUserId, bool orgAdmin = false);
|
||||
Task DeleteAsync(CipherDetails cipherDetails, Guid deletingUserId, bool orgAdmin = false);
|
||||
Task DeleteManyAsync(IEnumerable<Guid> cipherIds, Guid deletingUserId, Guid? organizationId = null, bool orgAdmin = false);
|
||||
Task<DeleteAttachmentResponseData> DeleteAttachmentAsync(Cipher cipher, string attachmentId, Guid deletingUserId, bool orgAdmin = false);
|
||||
Task PurgeAsync(Guid organizationId);
|
||||
@ -27,9 +27,9 @@ public interface ICipherService
|
||||
Task ShareManyAsync(IEnumerable<(Cipher cipher, DateTime? lastKnownRevisionDate)> ciphers, Guid organizationId,
|
||||
IEnumerable<Guid> collectionIds, Guid sharingUserId);
|
||||
Task SaveCollectionsAsync(Cipher cipher, IEnumerable<Guid> collectionIds, Guid savingUserId, bool orgAdmin);
|
||||
Task SoftDeleteAsync(Cipher cipher, Guid deletingUserId, bool orgAdmin = false);
|
||||
Task SoftDeleteAsync(CipherDetails cipherDetails, Guid deletingUserId, bool orgAdmin = false);
|
||||
Task SoftDeleteManyAsync(IEnumerable<Guid> cipherIds, Guid deletingUserId, Guid? organizationId = null, bool orgAdmin = false);
|
||||
Task RestoreAsync(Cipher cipher, Guid restoringUserId, bool orgAdmin = false);
|
||||
Task RestoreAsync(CipherDetails cipherDetails, Guid restoringUserId, bool orgAdmin = false);
|
||||
Task<ICollection<CipherOrganizationDetails>> RestoreManyAsync(IEnumerable<Guid> cipherIds, Guid restoringUserId, Guid? organizationId = null, bool orgAdmin = false);
|
||||
Task UploadFileForExistingAttachmentAsync(Stream stream, Cipher cipher, CipherAttachment.MetaData attachmentId);
|
||||
Task<AttachmentResponseData> GetAttachmentDownloadDataAsync(Cipher cipher, string attachmentId);
|
||||
|
@ -14,6 +14,7 @@ using Bit.Core.Tools.Enums;
|
||||
using Bit.Core.Tools.Models.Business;
|
||||
using Bit.Core.Tools.Services;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Core.Vault.Authorization.Permissions;
|
||||
using Bit.Core.Vault.Entities;
|
||||
using Bit.Core.Vault.Enums;
|
||||
using Bit.Core.Vault.Models.Data;
|
||||
@ -44,6 +45,7 @@ public class CipherService : ICipherService
|
||||
private readonly ICurrentContext _currentContext;
|
||||
private readonly IGetCipherPermissionsForUserQuery _getCipherPermissionsForUserQuery;
|
||||
private readonly IPolicyRequirementQuery _policyRequirementQuery;
|
||||
private readonly IApplicationCacheService _applicationCacheService;
|
||||
private readonly IFeatureService _featureService;
|
||||
|
||||
public CipherService(
|
||||
@ -64,6 +66,7 @@ public class CipherService : ICipherService
|
||||
ICurrentContext currentContext,
|
||||
IGetCipherPermissionsForUserQuery getCipherPermissionsForUserQuery,
|
||||
IPolicyRequirementQuery policyRequirementQuery,
|
||||
IApplicationCacheService applicationCacheService,
|
||||
IFeatureService featureService)
|
||||
{
|
||||
_cipherRepository = cipherRepository;
|
||||
@ -83,6 +86,7 @@ public class CipherService : ICipherService
|
||||
_currentContext = currentContext;
|
||||
_getCipherPermissionsForUserQuery = getCipherPermissionsForUserQuery;
|
||||
_policyRequirementQuery = policyRequirementQuery;
|
||||
_applicationCacheService = applicationCacheService;
|
||||
_featureService = featureService;
|
||||
}
|
||||
|
||||
@ -421,19 +425,19 @@ public class CipherService : ICipherService
|
||||
return response;
|
||||
}
|
||||
|
||||
public async Task DeleteAsync(Cipher cipher, Guid deletingUserId, bool orgAdmin = false)
|
||||
public async Task DeleteAsync(CipherDetails cipherDetails, Guid deletingUserId, bool orgAdmin = false)
|
||||
{
|
||||
if (!orgAdmin && !(await UserCanEditAsync(cipher, deletingUserId)))
|
||||
if (!orgAdmin && !await UserCanDeleteAsync(cipherDetails, deletingUserId))
|
||||
{
|
||||
throw new BadRequestException("You do not have permissions to delete this.");
|
||||
}
|
||||
|
||||
await _cipherRepository.DeleteAsync(cipher);
|
||||
await _attachmentStorageService.DeleteAttachmentsForCipherAsync(cipher.Id);
|
||||
await _eventService.LogCipherEventAsync(cipher, EventType.Cipher_Deleted);
|
||||
await _cipherRepository.DeleteAsync(cipherDetails);
|
||||
await _attachmentStorageService.DeleteAttachmentsForCipherAsync(cipherDetails.Id);
|
||||
await _eventService.LogCipherEventAsync(cipherDetails, EventType.Cipher_Deleted);
|
||||
|
||||
// push
|
||||
await _pushService.PushSyncCipherDeleteAsync(cipher);
|
||||
await _pushService.PushSyncCipherDeleteAsync(cipherDetails);
|
||||
}
|
||||
|
||||
public async Task DeleteManyAsync(IEnumerable<Guid> cipherIds, Guid deletingUserId, Guid? organizationId = null, bool orgAdmin = false)
|
||||
@ -450,8 +454,8 @@ public class CipherService : ICipherService
|
||||
else
|
||||
{
|
||||
var ciphers = await _cipherRepository.GetManyByUserIdAsync(deletingUserId);
|
||||
deletingCiphers = ciphers.Where(c => cipherIdsSet.Contains(c.Id) && c.Edit).Select(x => (Cipher)x).ToList();
|
||||
|
||||
var filteredCiphers = await FilterCiphersByDeletePermission(ciphers, cipherIdsSet, deletingUserId);
|
||||
deletingCiphers = filteredCiphers.Select(c => (Cipher)c).ToList();
|
||||
await _cipherRepository.DeleteAsync(deletingCiphers.Select(c => c.Id), deletingUserId);
|
||||
}
|
||||
|
||||
@ -703,33 +707,26 @@ public class CipherService : ICipherService
|
||||
await _pushService.PushSyncCipherUpdateAsync(cipher, collectionIds);
|
||||
}
|
||||
|
||||
public async Task SoftDeleteAsync(Cipher cipher, Guid deletingUserId, bool orgAdmin = false)
|
||||
public async Task SoftDeleteAsync(CipherDetails cipherDetails, Guid deletingUserId, bool orgAdmin = false)
|
||||
{
|
||||
if (!orgAdmin && !(await UserCanEditAsync(cipher, deletingUserId)))
|
||||
if (!orgAdmin && !await UserCanDeleteAsync(cipherDetails, deletingUserId))
|
||||
{
|
||||
throw new BadRequestException("You do not have permissions to soft delete this.");
|
||||
}
|
||||
|
||||
if (cipher.DeletedDate.HasValue)
|
||||
if (cipherDetails.DeletedDate.HasValue)
|
||||
{
|
||||
// Already soft-deleted, we can safely ignore this
|
||||
return;
|
||||
}
|
||||
|
||||
cipher.DeletedDate = cipher.RevisionDate = DateTime.UtcNow;
|
||||
cipherDetails.DeletedDate = cipherDetails.RevisionDate = DateTime.UtcNow;
|
||||
|
||||
if (cipher is CipherDetails details)
|
||||
{
|
||||
await _cipherRepository.UpsertAsync(details);
|
||||
}
|
||||
else
|
||||
{
|
||||
await _cipherRepository.UpsertAsync(cipher);
|
||||
}
|
||||
await _eventService.LogCipherEventAsync(cipher, EventType.Cipher_SoftDeleted);
|
||||
await _cipherRepository.UpsertAsync(cipherDetails);
|
||||
await _eventService.LogCipherEventAsync(cipherDetails, EventType.Cipher_SoftDeleted);
|
||||
|
||||
// push
|
||||
await _pushService.PushSyncCipherUpdateAsync(cipher, null);
|
||||
await _pushService.PushSyncCipherUpdateAsync(cipherDetails, null);
|
||||
}
|
||||
|
||||
public async Task SoftDeleteManyAsync(IEnumerable<Guid> cipherIds, Guid deletingUserId, Guid? organizationId, bool orgAdmin)
|
||||
@ -746,8 +743,8 @@ public class CipherService : ICipherService
|
||||
else
|
||||
{
|
||||
var ciphers = await _cipherRepository.GetManyByUserIdAsync(deletingUserId);
|
||||
deletingCiphers = ciphers.Where(c => cipherIdsSet.Contains(c.Id) && c.Edit).Select(x => (Cipher)x).ToList();
|
||||
|
||||
var filteredCiphers = await FilterCiphersByDeletePermission(ciphers, cipherIdsSet, deletingUserId);
|
||||
deletingCiphers = filteredCiphers.Select(c => (Cipher)c).ToList();
|
||||
await _cipherRepository.SoftDeleteAsync(deletingCiphers.Select(c => c.Id), deletingUserId);
|
||||
}
|
||||
|
||||
@ -762,34 +759,27 @@ public class CipherService : ICipherService
|
||||
await _pushService.PushSyncCiphersAsync(deletingUserId);
|
||||
}
|
||||
|
||||
public async Task RestoreAsync(Cipher cipher, Guid restoringUserId, bool orgAdmin = false)
|
||||
public async Task RestoreAsync(CipherDetails cipherDetails, Guid restoringUserId, bool orgAdmin = false)
|
||||
{
|
||||
if (!orgAdmin && !(await UserCanEditAsync(cipher, restoringUserId)))
|
||||
if (!orgAdmin && !await UserCanRestoreAsync(cipherDetails, restoringUserId))
|
||||
{
|
||||
throw new BadRequestException("You do not have permissions to delete this.");
|
||||
}
|
||||
|
||||
if (!cipher.DeletedDate.HasValue)
|
||||
if (!cipherDetails.DeletedDate.HasValue)
|
||||
{
|
||||
// Already restored, we can safely ignore this
|
||||
return;
|
||||
}
|
||||
|
||||
cipher.DeletedDate = null;
|
||||
cipher.RevisionDate = DateTime.UtcNow;
|
||||
cipherDetails.DeletedDate = null;
|
||||
cipherDetails.RevisionDate = DateTime.UtcNow;
|
||||
|
||||
if (cipher is CipherDetails details)
|
||||
{
|
||||
await _cipherRepository.UpsertAsync(details);
|
||||
}
|
||||
else
|
||||
{
|
||||
await _cipherRepository.UpsertAsync(cipher);
|
||||
}
|
||||
await _eventService.LogCipherEventAsync(cipher, EventType.Cipher_Restored);
|
||||
await _cipherRepository.UpsertAsync(cipherDetails);
|
||||
await _eventService.LogCipherEventAsync(cipherDetails, EventType.Cipher_Restored);
|
||||
|
||||
// push
|
||||
await _pushService.PushSyncCipherUpdateAsync(cipher, null);
|
||||
await _pushService.PushSyncCipherUpdateAsync(cipherDetails, null);
|
||||
}
|
||||
|
||||
public async Task<ICollection<CipherOrganizationDetails>> RestoreManyAsync(IEnumerable<Guid> cipherIds, Guid restoringUserId, Guid? organizationId = null, bool orgAdmin = false)
|
||||
@ -812,8 +802,8 @@ public class CipherService : ICipherService
|
||||
else
|
||||
{
|
||||
var ciphers = await _cipherRepository.GetManyByUserIdAsync(restoringUserId);
|
||||
restoringCiphers = ciphers.Where(c => cipherIdsSet.Contains(c.Id) && c.Edit).Select(c => (CipherOrganizationDetails)c).ToList();
|
||||
|
||||
var filteredCiphers = await FilterCiphersByDeletePermission(ciphers, cipherIdsSet, restoringUserId);
|
||||
restoringCiphers = filteredCiphers.Select(c => (CipherOrganizationDetails)c).ToList();
|
||||
revisionDate = await _cipherRepository.RestoreAsync(restoringCiphers.Select(c => c.Id), restoringUserId);
|
||||
}
|
||||
|
||||
@ -844,6 +834,34 @@ public class CipherService : ICipherService
|
||||
return await _cipherRepository.GetCanEditByIdAsync(userId, cipher.Id);
|
||||
}
|
||||
|
||||
private async Task<bool> UserCanDeleteAsync(CipherDetails cipher, Guid userId)
|
||||
{
|
||||
if (!_featureService.IsEnabled(FeatureFlagKeys.LimitItemDeletion))
|
||||
{
|
||||
return await UserCanEditAsync(cipher, userId);
|
||||
}
|
||||
|
||||
var user = await _userService.GetUserByIdAsync(userId);
|
||||
var organizationAbility = cipher.OrganizationId.HasValue ?
|
||||
await _applicationCacheService.GetOrganizationAbilityAsync(cipher.OrganizationId.Value) : null;
|
||||
|
||||
return NormalCipherPermissions.CanDelete(user, cipher, organizationAbility);
|
||||
}
|
||||
|
||||
private async Task<bool> UserCanRestoreAsync(CipherDetails cipher, Guid userId)
|
||||
{
|
||||
if (!_featureService.IsEnabled(FeatureFlagKeys.LimitItemDeletion))
|
||||
{
|
||||
return await UserCanEditAsync(cipher, userId);
|
||||
}
|
||||
|
||||
var user = await _userService.GetUserByIdAsync(userId);
|
||||
var organizationAbility = cipher.OrganizationId.HasValue ?
|
||||
await _applicationCacheService.GetOrganizationAbilityAsync(cipher.OrganizationId.Value) : null;
|
||||
|
||||
return NormalCipherPermissions.CanRestore(user, cipher, organizationAbility);
|
||||
}
|
||||
|
||||
private void ValidateCipherLastKnownRevisionDateAsync(Cipher cipher, DateTime? lastKnownRevisionDate)
|
||||
{
|
||||
if (cipher.Id == default || !lastKnownRevisionDate.HasValue)
|
||||
@ -1010,4 +1028,35 @@ public class CipherService : ICipherService
|
||||
cipher.Data = JsonSerializer.Serialize(newCipherData);
|
||||
}
|
||||
}
|
||||
|
||||
// This method is used to filter ciphers based on the user's permissions to delete them.
|
||||
// It supports both the old and new logic depending on the feature flag.
|
||||
private async Task<List<T>> FilterCiphersByDeletePermission<T>(
|
||||
IEnumerable<T> ciphers,
|
||||
HashSet<Guid> cipherIdsSet,
|
||||
Guid userId) where T : CipherDetails
|
||||
{
|
||||
if (!_featureService.IsEnabled(FeatureFlagKeys.LimitItemDeletion))
|
||||
{
|
||||
return ciphers.Where(c => cipherIdsSet.Contains(c.Id) && c.Edit).ToList();
|
||||
}
|
||||
|
||||
var user = await _userService.GetUserByIdAsync(userId);
|
||||
var organizationAbilities = await _applicationCacheService.GetOrganizationAbilitiesAsync();
|
||||
|
||||
var filteredCiphers = ciphers
|
||||
.Where(c => cipherIdsSet.Contains(c.Id))
|
||||
.GroupBy(c => c.OrganizationId)
|
||||
.SelectMany(group =>
|
||||
{
|
||||
var organizationAbility = group.Key.HasValue &&
|
||||
organizationAbilities.TryGetValue(group.Key.Value, out var ability) ?
|
||||
ability : null;
|
||||
|
||||
return group.Where(c => NormalCipherPermissions.CanDelete(user, c, organizationAbility));
|
||||
})
|
||||
.ToList();
|
||||
|
||||
return filteredCiphers;
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user