mirror of
https://github.com/bitwarden/server.git
synced 2025-06-30 07:36:14 -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:
@ -16,6 +16,7 @@ using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Tools.Services;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Core.Vault.Authorization.Permissions;
|
||||
using Bit.Core.Vault.Entities;
|
||||
using Bit.Core.Vault.Models.Data;
|
||||
using Bit.Core.Vault.Queries;
|
||||
@ -345,6 +346,77 @@ public class CiphersController : Controller
|
||||
return await CanEditCiphersAsync(organizationId, cipherIds);
|
||||
}
|
||||
|
||||
private async Task<bool> CanDeleteOrRestoreCipherAsAdminAsync(Guid organizationId, IEnumerable<Guid> cipherIds)
|
||||
{
|
||||
if (!_featureService.IsEnabled(FeatureFlagKeys.LimitItemDeletion))
|
||||
{
|
||||
return await CanEditCipherAsAdminAsync(organizationId, cipherIds);
|
||||
}
|
||||
|
||||
var org = _currentContext.GetOrganization(organizationId);
|
||||
|
||||
// If we're not an "admin", we don't need to check the ciphers
|
||||
if (org is not ({ Type: OrganizationUserType.Owner or OrganizationUserType.Admin } or { Permissions.EditAnyCollection: true }))
|
||||
{
|
||||
// Are we a provider user? If so, we need to be sure we're not restricted
|
||||
// Once the feature flag is removed, this check can be combined with the above
|
||||
if (await _currentContext.ProviderUserForOrgAsync(organizationId))
|
||||
{
|
||||
// Provider is restricted from editing ciphers, so we're not an "admin"
|
||||
if (_featureService.IsEnabled(FeatureFlagKeys.RestrictProviderAccess))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Provider is unrestricted, so we're an "admin", don't return early
|
||||
}
|
||||
else
|
||||
{
|
||||
// Not a provider or admin
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// If the user can edit all ciphers for the organization, just check they all belong to the org
|
||||
if (await CanEditAllCiphersAsync(organizationId))
|
||||
{
|
||||
// TODO: This can likely be optimized to only query the requested ciphers and then checking they belong to the org
|
||||
var orgCiphers = (await _cipherRepository.GetManyByOrganizationIdAsync(organizationId)).ToDictionary(c => c.Id);
|
||||
|
||||
// Ensure all requested ciphers are in orgCiphers
|
||||
return cipherIds.All(c => orgCiphers.ContainsKey(c));
|
||||
}
|
||||
|
||||
// The user cannot access any ciphers for the organization, we're done
|
||||
if (!await CanAccessOrganizationCiphersAsync(organizationId))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var user = await _userService.GetUserByPrincipalAsync(User);
|
||||
// Select all deletable ciphers for this user belonging to the organization
|
||||
var deletableOrgCipherList = (await _cipherRepository.GetManyByUserIdAsync(user.Id, true))
|
||||
.Where(c => c.OrganizationId == organizationId && c.UserId == null).ToList();
|
||||
|
||||
// Special case for unassigned ciphers
|
||||
if (await CanAccessUnassignedCiphersAsync(organizationId))
|
||||
{
|
||||
var unassignedCiphers =
|
||||
(await _cipherRepository.GetManyUnassignedOrganizationDetailsByOrganizationIdAsync(
|
||||
organizationId));
|
||||
|
||||
// Users that can access unassigned ciphers can also delete them
|
||||
deletableOrgCipherList.AddRange(unassignedCiphers.Select(c => new CipherDetails(c) { Manage = true }));
|
||||
}
|
||||
|
||||
var organizationAbility = await _applicationCacheService.GetOrganizationAbilityAsync(organizationId);
|
||||
var deletableOrgCiphers = deletableOrgCipherList
|
||||
.Where(c => NormalCipherPermissions.CanDelete(user, c, organizationAbility))
|
||||
.ToDictionary(c => c.Id);
|
||||
|
||||
return cipherIds.All(c => deletableOrgCiphers.ContainsKey(c));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// TODO: Move this to its own authorization handler or equivalent service - AC-2062
|
||||
/// </summary>
|
||||
@ -763,12 +835,12 @@ public class CiphersController : Controller
|
||||
|
||||
[HttpDelete("{id}/admin")]
|
||||
[HttpPost("{id}/delete-admin")]
|
||||
public async Task DeleteAdmin(string id)
|
||||
public async Task DeleteAdmin(Guid id)
|
||||
{
|
||||
var userId = _userService.GetProperUserId(User).Value;
|
||||
var cipher = await _cipherRepository.GetByIdAsync(new Guid(id));
|
||||
var cipher = await GetByIdAsync(id, userId);
|
||||
if (cipher == null || !cipher.OrganizationId.HasValue ||
|
||||
!await CanEditCipherAsAdminAsync(cipher.OrganizationId.Value, new[] { cipher.Id }))
|
||||
!await CanDeleteOrRestoreCipherAsAdminAsync(cipher.OrganizationId.Value, new[] { cipher.Id }))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
@ -808,7 +880,7 @@ public class CiphersController : Controller
|
||||
var cipherIds = model.Ids.Select(i => new Guid(i)).ToList();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(model.OrganizationId) ||
|
||||
!await CanEditCipherAsAdminAsync(new Guid(model.OrganizationId), cipherIds))
|
||||
!await CanDeleteOrRestoreCipherAsAdminAsync(new Guid(model.OrganizationId), cipherIds))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
@ -830,12 +902,12 @@ public class CiphersController : Controller
|
||||
}
|
||||
|
||||
[HttpPut("{id}/delete-admin")]
|
||||
public async Task PutDeleteAdmin(string id)
|
||||
public async Task PutDeleteAdmin(Guid id)
|
||||
{
|
||||
var userId = _userService.GetProperUserId(User).Value;
|
||||
var cipher = await _cipherRepository.GetByIdAsync(new Guid(id));
|
||||
var cipher = await GetByIdAsync(id, userId);
|
||||
if (cipher == null || !cipher.OrganizationId.HasValue ||
|
||||
!await CanEditCipherAsAdminAsync(cipher.OrganizationId.Value, new[] { cipher.Id }))
|
||||
!await CanDeleteOrRestoreCipherAsAdminAsync(cipher.OrganizationId.Value, new[] { cipher.Id }))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
@ -871,7 +943,7 @@ public class CiphersController : Controller
|
||||
var cipherIds = model.Ids.Select(i => new Guid(i)).ToList();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(model.OrganizationId) ||
|
||||
!await CanEditCipherAsAdminAsync(new Guid(model.OrganizationId), cipherIds))
|
||||
!await CanDeleteOrRestoreCipherAsAdminAsync(new Guid(model.OrganizationId), cipherIds))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
@ -899,12 +971,12 @@ public class CiphersController : Controller
|
||||
}
|
||||
|
||||
[HttpPut("{id}/restore-admin")]
|
||||
public async Task<CipherMiniResponseModel> PutRestoreAdmin(string id)
|
||||
public async Task<CipherMiniResponseModel> PutRestoreAdmin(Guid id)
|
||||
{
|
||||
var userId = _userService.GetProperUserId(User).Value;
|
||||
var cipher = await _cipherRepository.GetOrganizationDetailsByIdAsync(new Guid(id));
|
||||
var cipher = await GetByIdAsync(id, userId);
|
||||
if (cipher == null || !cipher.OrganizationId.HasValue ||
|
||||
!await CanEditCipherAsAdminAsync(cipher.OrganizationId.Value, new[] { cipher.Id }))
|
||||
!await CanDeleteOrRestoreCipherAsAdminAsync(cipher.OrganizationId.Value, new[] { cipher.Id }))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
@ -944,7 +1016,7 @@ public class CiphersController : Controller
|
||||
|
||||
var cipherIdsToRestore = new HashSet<Guid>(model.Ids.Select(i => new Guid(i)));
|
||||
|
||||
if (model.OrganizationId == default || !await CanEditCipherAsAdminAsync(model.OrganizationId, cipherIdsToRestore))
|
||||
if (model.OrganizationId == default || !await CanDeleteOrRestoreCipherAsAdminAsync(model.OrganizationId, cipherIdsToRestore))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
Reference in New Issue
Block a user