1
0
mirror of https://github.com/bitwarden/server synced 2025-12-10 13:23:27 +00:00

[PM-20010] Fix purge logic to skip claimed user check for organization vault (#6107)

* Implement unit tests for PostPurge method in CiphersController to handle various scenarios

* Refactor PostPurge method in CiphersController to use Guid for organizationId parameter and update related unit tests

* Refactor PostPurge method in CiphersController to skip checking if user is claimed if its purging the org vault
This commit is contained in:
Rui Tomé
2025-07-29 16:17:16 +01:00
committed by GitHub
parent 47237fa88f
commit 43372b7168
2 changed files with 129 additions and 12 deletions

View File

@@ -1113,7 +1113,7 @@ public class CiphersController : Controller
}
[HttpPost("purge")]
public async Task PostPurge([FromBody] SecretVerificationRequestModel model, string organizationId = null)
public async Task PostPurge([FromBody] SecretVerificationRequestModel model, Guid? organizationId = null)
{
var user = await _userService.GetUserByPrincipalAsync(User);
if (user == null)
@@ -1128,24 +1128,22 @@ public class CiphersController : Controller
throw new BadRequestException(ModelState);
}
if (organizationId == null)
{
// Check if the user is claimed by any organization.
if (await _userService.IsClaimedByAnyOrganizationAsync(user.Id))
{
throw new BadRequestException("Cannot purge accounts owned by an organization. Contact your organization administrator for additional details.");
}
if (string.IsNullOrWhiteSpace(organizationId))
{
await _cipherRepository.DeleteByUserIdAsync(user.Id);
}
else
{
var orgId = new Guid(organizationId);
if (!await _currentContext.EditAnyCollection(orgId))
if (!await _currentContext.EditAnyCollection(organizationId!.Value))
{
throw new NotFoundException();
}
await _cipherService.PurgeAsync(orgId);
await _cipherService.PurgeAsync(organizationId!.Value);
}
}

View File

@@ -1,5 +1,6 @@
using System.Security.Claims;
using System.Text.Json;
using Bit.Api.Auth.Models.Request.Accounts;
using Bit.Api.Vault.Controllers;
using Bit.Api.Vault.Models;
using Bit.Api.Vault.Models.Request;
@@ -1789,5 +1790,123 @@ public class CiphersControllerTests
);
}
}
[Theory, BitAutoData]
public async Task PostPurge_WhenUserNotFound_ThrowsUnauthorizedAccessException(
SecretVerificationRequestModel model,
SutProvider<CiphersController> sutProvider)
{
sutProvider.GetDependency<IUserService>()
.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>())
.Returns((User)null);
await Assert.ThrowsAsync<UnauthorizedAccessException>(() => sutProvider.Sut.PostPurge(model));
}
[Theory, BitAutoData]
public async Task PostPurge_WhenUserVerificationFails_ThrowsBadRequestException(
User user,
SecretVerificationRequestModel model,
SutProvider<CiphersController> sutProvider)
{
sutProvider.GetDependency<IUserService>()
.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>())
.Returns(user);
sutProvider.GetDependency<IUserService>()
.VerifySecretAsync(user, model.Secret)
.Returns(false);
await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.PostPurge(model));
}
[Theory, BitAutoData]
public async Task PostPurge_UserPurge_WithClaimedUser_ThrowsBadRequestException(
User user,
SecretVerificationRequestModel model,
SutProvider<CiphersController> sutProvider)
{
sutProvider.GetDependency<IUserService>()
.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>())
.Returns(user);
sutProvider.GetDependency<IUserService>()
.VerifySecretAsync(user, model.Secret)
.Returns(true);
sutProvider.GetDependency<IUserService>()
.IsClaimedByAnyOrganizationAsync(user.Id)
.Returns(true);
await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.PostPurge(model));
}
[Theory, BitAutoData]
public async Task PostPurge_UserPurge_WithUnclaimedUser_Successful(
User user,
SecretVerificationRequestModel model,
SutProvider<CiphersController> sutProvider)
{
sutProvider.GetDependency<IUserService>()
.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>())
.Returns(user);
sutProvider.GetDependency<IUserService>()
.VerifySecretAsync(user, model.Secret)
.Returns(true);
sutProvider.GetDependency<IUserService>()
.IsClaimedByAnyOrganizationAsync(user.Id)
.Returns(false);
await sutProvider.Sut.PostPurge(model);
await sutProvider.GetDependency<ICipherRepository>()
.Received(1)
.DeleteByUserIdAsync(user.Id);
}
[Theory, BitAutoData]
public async Task PostPurge_OrganizationPurge_WithEditAnyCollectionPermission_Successful(
User user,
SecretVerificationRequestModel model,
Guid organizationId,
SutProvider<CiphersController> sutProvider)
{
sutProvider.GetDependency<IUserService>()
.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>())
.Returns(user);
sutProvider.GetDependency<IUserService>()
.VerifySecretAsync(user, model.Secret)
.Returns(true);
sutProvider.GetDependency<IUserService>()
.IsClaimedByAnyOrganizationAsync(user.Id)
.Returns(true);
sutProvider.GetDependency<ICurrentContext>()
.EditAnyCollection(organizationId)
.Returns(true);
await sutProvider.Sut.PostPurge(model, organizationId);
await sutProvider.GetDependency<ICipherService>()
.Received(1)
.PurgeAsync(organizationId);
}
[Theory, BitAutoData]
public async Task PostPurge_OrganizationPurge_WithInsufficientPermissions_ThrowsNotFoundException(
User user,
Guid organizationId,
SecretVerificationRequestModel model,
SutProvider<CiphersController> sutProvider)
{
sutProvider.GetDependency<IUserService>()
.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>())
.Returns(user);
sutProvider.GetDependency<IUserService>()
.VerifySecretAsync(user, model.Secret)
.Returns(true);
sutProvider.GetDependency<IUserService>()
.IsClaimedByAnyOrganizationAsync(user.Id)
.Returns(false);
sutProvider.GetDependency<ICurrentContext>()
.EditAnyCollection(organizationId)
.Returns(false);
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.PostPurge(model, organizationId));
}
}