1
0
mirror of https://github.com/bitwarden/server synced 2026-02-12 06:23:28 +00:00

[PM-31745] Allow user to perm delete unassigned items (#6956)

* update DeleteAdmin to grab items that are unassigned and ciphersControllerTests
This commit is contained in:
Jason Ng
2026-02-09 10:46:30 -05:00
committed by GitHub
parent fb76e3f867
commit 2413ce10ab
2 changed files with 53 additions and 43 deletions

View File

@@ -976,14 +976,14 @@ public class CiphersController : Controller
public async Task DeleteAdmin(Guid id)
{
var userId = _userService.GetProperUserId(User).Value;
var cipher = await GetByIdAsync(id, userId);
var cipher = await GetByIdAsyncAdmin(id);
if (cipher == null || !cipher.OrganizationId.HasValue ||
!await CanDeleteOrRestoreCipherAsAdminAsync(cipher.OrganizationId.Value, new[] { cipher.Id }))
{
throw new NotFoundException();
}
await _cipherService.DeleteAsync(cipher, userId, true);
await _cipherService.DeleteAsync(new CipherDetails(cipher), userId, true);
}
[HttpPost("{id}/delete-admin")]

View File

@@ -158,9 +158,9 @@ public class CiphersControllerTests
[BitAutoData(OrganizationUserType.Custom, false, false)]
public async Task CanEditCiphersAsAdminAsync_FlexibleCollections_Success(
OrganizationUserType userType, bool allowAdminsAccessToAllItems, bool shouldSucceed,
CurrentContextOrganization organization, Guid userId, CipherDetails cipherDetails, SutProvider<CiphersController> sutProvider)
CurrentContextOrganization organization, Guid userId, CipherOrganizationDetails cipherOrgDetails, SutProvider<CiphersController> sutProvider)
{
cipherDetails.OrganizationId = organization.Id;
cipherOrgDetails.OrganizationId = organization.Id;
organization.Type = userType;
if (userType == OrganizationUserType.Custom)
{
@@ -171,9 +171,9 @@ public class CiphersControllerTests
sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(new User { Id = userId });
sutProvider.GetDependency<ICipherRepository>().GetByIdAsync(cipherDetails.Id, userId).Returns(cipherDetails);
sutProvider.GetDependency<ICipherRepository>().GetOrganizationDetailsByIdAsync(cipherOrgDetails.Id).Returns(cipherOrgDetails);
sutProvider.GetDependency<ICipherRepository>().GetManyByOrganizationIdAsync(organization.Id).Returns(new List<Cipher> { cipherDetails });
sutProvider.GetDependency<ICipherRepository>().GetManyByOrganizationIdAsync(organization.Id).Returns(new List<Cipher> { cipherOrgDetails });
sutProvider.GetDependency<IApplicationCacheService>().GetOrganizationAbilityAsync(organization.Id).Returns(new OrganizationAbility
{
@@ -183,13 +183,13 @@ public class CiphersControllerTests
if (shouldSucceed)
{
await sutProvider.Sut.DeleteAdmin(cipherDetails.Id);
await sutProvider.Sut.DeleteAdmin(cipherOrgDetails.Id);
await sutProvider.GetDependency<ICipherService>().ReceivedWithAnyArgs()
.DeleteAsync(default, default);
}
else
{
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.DeleteAdmin(cipherDetails.Id));
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.DeleteAdmin(cipherOrgDetails.Id));
await sutProvider.GetDependency<ICipherService>().DidNotReceiveWithAnyArgs()
.DeleteAsync(default, default);
}
@@ -199,25 +199,23 @@ public class CiphersControllerTests
[BitAutoData(OrganizationUserType.Owner)]
[BitAutoData(OrganizationUserType.Admin)]
public async Task DeleteAdmin_WithOwnerOrAdmin_WithManagePermission_DeletesCipher(
OrganizationUserType organizationUserType, CipherDetails cipherDetails, Guid userId,
OrganizationUserType organizationUserType, CipherOrganizationDetails cipherOrgDetails, Guid userId,
CurrentContextOrganization organization, SutProvider<CiphersController> sutProvider)
{
cipherDetails.UserId = null;
cipherDetails.OrganizationId = organization.Id;
cipherDetails.Edit = true;
cipherDetails.Manage = true;
cipherOrgDetails.UserId = null;
cipherOrgDetails.OrganizationId = organization.Id;
organization.Type = organizationUserType;
sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(new User { Id = userId });
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
sutProvider.GetDependency<ICipherRepository>().GetByIdAsync(cipherDetails.Id, userId).Returns(cipherDetails);
sutProvider.GetDependency<ICipherRepository>().GetOrganizationDetailsByIdAsync(cipherOrgDetails.Id).Returns(cipherOrgDetails);
sutProvider.GetDependency<ICipherRepository>()
.GetManyByUserIdAsync(userId)
.Returns(new List<CipherDetails>
{
cipherDetails
new CipherDetails(cipherOrgDetails) { Edit = true, Manage = true }
});
sutProvider.GetDependency<IApplicationCacheService>()
.GetOrganizationAbilityAsync(organization.Id)
@@ -227,34 +225,35 @@ public class CiphersControllerTests
LimitItemDeletion = true
});
await sutProvider.Sut.DeleteAdmin(cipherDetails.Id);
await sutProvider.Sut.DeleteAdmin(cipherOrgDetails.Id);
await sutProvider.GetDependency<ICipherService>().Received(1).DeleteAsync(cipherDetails, userId, true);
await sutProvider.GetDependency<ICipherService>().Received(1).DeleteAsync(
Arg.Is<CipherDetails>(c => c.Id == cipherOrgDetails.Id && c.OrganizationId == cipherOrgDetails.OrganizationId),
userId,
true);
}
[Theory]
[BitAutoData(OrganizationUserType.Owner)]
[BitAutoData(OrganizationUserType.Admin)]
public async Task DeleteAdmin_WithOwnerOrAdmin_WithoutManagePermission_ThrowsNotFoundException(
OrganizationUserType organizationUserType, CipherDetails cipherDetails, Guid userId,
OrganizationUserType organizationUserType, CipherOrganizationDetails cipherOrgDetails, Guid userId,
CurrentContextOrganization organization, SutProvider<CiphersController> sutProvider)
{
cipherDetails.UserId = null;
cipherDetails.OrganizationId = organization.Id;
cipherDetails.Edit = true;
cipherDetails.Manage = false;
cipherOrgDetails.UserId = null;
cipherOrgDetails.OrganizationId = organization.Id;
organization.Type = organizationUserType;
sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(new User { Id = userId });
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
sutProvider.GetDependency<ICipherRepository>().GetByIdAsync(cipherDetails.Id, userId).Returns(cipherDetails);
sutProvider.GetDependency<ICipherRepository>().GetOrganizationDetailsByIdAsync(cipherOrgDetails.Id).Returns(cipherOrgDetails);
sutProvider.GetDependency<ICipherRepository>()
.GetManyByUserIdAsync(userId)
.Returns(new List<CipherDetails>
{
cipherDetails
new CipherDetails(cipherOrgDetails) { Edit = true, Manage = false }
});
sutProvider.GetDependency<IApplicationCacheService>()
.GetOrganizationAbilityAsync(organization.Id)
@@ -264,7 +263,7 @@ public class CiphersControllerTests
LimitItemDeletion = true
});
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.DeleteAdmin(cipherDetails.Id));
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.DeleteAdmin(cipherOrgDetails.Id));
await sutProvider.GetDependency<ICipherService>().DidNotReceive().DeleteAsync(Arg.Any<CipherDetails>(), Arg.Any<Guid>(), Arg.Any<bool>());
}
@@ -273,21 +272,21 @@ public class CiphersControllerTests
[BitAutoData(OrganizationUserType.Owner)]
[BitAutoData(OrganizationUserType.Admin)]
public async Task DeleteAdmin_WithOwnerOrAdmin_WithAccessToUnassignedCipher_DeletesCipher(
OrganizationUserType organizationUserType, CipherDetails cipherDetails, Guid userId,
OrganizationUserType organizationUserType, CipherOrganizationDetails cipherOrgDetails, Guid userId,
CurrentContextOrganization organization, SutProvider<CiphersController> sutProvider)
{
cipherDetails.OrganizationId = organization.Id;
cipherOrgDetails.OrganizationId = organization.Id;
organization.Type = organizationUserType;
sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(new User { Id = userId });
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
sutProvider.GetDependency<ICipherRepository>().GetByIdAsync(cipherDetails.Id, userId).Returns(cipherDetails);
sutProvider.GetDependency<ICipherRepository>().GetOrganizationDetailsByIdAsync(cipherOrgDetails.Id).Returns(cipherOrgDetails);
sutProvider.GetDependency<ICipherRepository>()
.GetManyUnassignedOrganizationDetailsByOrganizationIdAsync(organization.Id)
.Returns(new List<CipherOrganizationDetails>
{
new() { Id = cipherDetails.Id, OrganizationId = cipherDetails.OrganizationId }
new() { Id = cipherOrgDetails.Id, OrganizationId = cipherOrgDetails.OrganizationId }
});
sutProvider.GetDependency<IApplicationCacheService>()
.GetOrganizationAbilityAsync(organization.Id)
@@ -297,54 +296,65 @@ public class CiphersControllerTests
LimitItemDeletion = true
});
await sutProvider.Sut.DeleteAdmin(cipherDetails.Id);
await sutProvider.Sut.DeleteAdmin(cipherOrgDetails.Id);
await sutProvider.GetDependency<ICipherService>().Received(1).DeleteAsync(cipherDetails, userId, true);
await sutProvider.GetDependency<ICipherService>().Received(1).DeleteAsync(Arg.Is<CipherDetails>(c => c.Id == cipherOrgDetails.Id && c.OrganizationId == cipherOrgDetails.OrganizationId),
userId,
true);
}
[Theory]
[BitAutoData(OrganizationUserType.Owner)]
[BitAutoData(OrganizationUserType.Admin)]
public async Task DeleteAdmin_WithAdminOrOwner_WithAccessToAllCollectionItems_DeletesCipher(
OrganizationUserType organizationUserType, CipherDetails cipherDetails, Guid userId,
OrganizationUserType organizationUserType, CipherOrganizationDetails cipherOrgDetails, Guid userId,
CurrentContextOrganization organization, SutProvider<CiphersController> sutProvider)
{
cipherDetails.OrganizationId = organization.Id;
organization.Type = organizationUserType;
cipherOrgDetails.OrganizationId = organization.Id;
organization.Type = organizationUserType;
sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
sutProvider.GetDependency<ICipherRepository>().GetByIdAsync(cipherDetails.Id, userId).Returns(cipherDetails);
sutProvider.GetDependency<ICipherRepository>().GetManyByOrganizationIdAsync(organization.Id).Returns(new List<Cipher> { cipherDetails });
sutProvider.GetDependency<ICipherRepository>().GetOrganizationDetailsByIdAsync(cipherOrgDetails.Id).Returns(cipherOrgDetails);
sutProvider.GetDependency<ICipherRepository>().GetManyByOrganizationIdAsync(organization.Id).Returns(new List<Cipher> { cipherOrgDetails });
sutProvider.GetDependency<IApplicationCacheService>().GetOrganizationAbilityAsync(organization.Id).Returns(new OrganizationAbility
{
Id = organization.Id,
AllowAdminAccessToAllCollectionItems = true
});
await sutProvider.Sut.DeleteAdmin(cipherDetails.Id);
await sutProvider.Sut.DeleteAdmin(cipherOrgDetails.Id);
await sutProvider.GetDependency<ICipherService>().Received(1).DeleteAsync(cipherDetails, userId, true);
await sutProvider.GetDependency<ICipherService>().Received(1).DeleteAsync(
Arg.Is<CipherDetails>(c => c.Id == cipherOrgDetails.Id && c.OrganizationId == cipherOrgDetails.OrganizationId),
userId,
true);
}
[Theory]
[BitAutoData]
public async Task DeleteAdmin_WithCustomUser_WithEditAnyCollectionTrue_DeletesCipher(
CipherDetails cipherDetails, Guid userId,
CipherOrganizationDetails cipherOrgDetails, Guid userId,
CurrentContextOrganization organization, SutProvider<CiphersController> sutProvider)
{
cipherDetails.OrganizationId = organization.Id;
cipherOrgDetails.OrganizationId = organization.Id;
organization.Type = OrganizationUserType.Custom;
organization.Permissions.EditAnyCollection = true;
sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
sutProvider.GetDependency<ICipherRepository>().GetByIdAsync(cipherDetails.Id, userId).Returns(cipherDetails);
sutProvider.GetDependency<ICipherRepository>().GetManyByOrganizationIdAsync(organization.Id).Returns(new List<Cipher> { cipherDetails });
sutProvider.GetDependency<ICipherRepository>().GetOrganizationDetailsByIdAsync(cipherOrgDetails.Id).Returns(cipherOrgDetails);
sutProvider.GetDependency<ICipherRepository>().GetManyByOrganizationIdAsync(organization.Id).Returns(new List<Cipher> { cipherOrgDetails });
await sutProvider.Sut.DeleteAdmin(cipherDetails.Id);
await sutProvider.Sut.DeleteAdmin(cipherOrgDetails.Id);
await sutProvider.GetDependency<ICipherService>().Received(1).DeleteAsync(cipherDetails, userId, true);
await sutProvider.GetDependency<ICipherService>().Received(1).DeleteAsync(
Arg.Is<CipherDetails>(c => c.Id == cipherOrgDetails.Id && c.OrganizationId == cipherOrgDetails.OrganizationId),
userId,
true);
}
[Theory]