Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 0 additions & 7 deletions src/Api/Auth/Controllers/AccountsController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
using Bit.Api.Models.Response;
using Bit.Core;
using Bit.Core.AdminConsole.Enums.Provider;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
using Bit.Core.AdminConsole.Repositories;
Expand Down Expand Up @@ -583,12 +582,6 @@ public async Task Delete([FromBody] SecretVerificationRequestModel model)
}
else
{
// Check if the user is claimed by any organization.
if (await _userService.IsClaimedByAnyOrganizationAsync(user.Id))
{
throw new BadRequestException(new CannotDeleteClaimedAccountError().Message);
}

var result = await _userService.DeleteAsync(user);
if (result.Succeeded)
{
Expand Down
6 changes: 6 additions & 0 deletions src/Core/Services/Implementations/UserService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.Models.Data;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccount;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Requests;
Expand Down Expand Up @@ -222,6 +223,11 @@ public async Task SaveUserAsync(User user, bool push = false)

public override async Task<IdentityResult> DeleteAsync(User user)
{
if (await IsClaimedByAnyOrganizationAsync(user.Id))
{
throw new BadRequestException(new CannotDeleteClaimedAccountError().Message);
}

// Check if user is the only owner of any organizations.
var onlyOwnerCount = await _organizationUserRepository.GetCountByOnlyOwnerAsync(user.Id);
if (onlyOwnerCount > 0)
Expand Down
70 changes: 68 additions & 2 deletions test/Api.Test/Auth/Controllers/AccountsControllerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -714,7 +714,8 @@ public async Task Delete_WithUserManagedByAnOrganization_ThrowsBadRequestExcepti
var user = GenerateExampleUser();
ConfigureUserServiceToReturnValidPrincipalFor(user);
ConfigureUserServiceToAcceptPasswordFor(user);
_userService.IsClaimedByAnyOrganizationAsync(user.Id).Returns(true);
_userService.DeleteAsync(user)
.ThrowsAsync(new BadRequestException(new CannotDeleteClaimedAccountError().Message));

var result = await Assert.ThrowsAsync<BadRequestException>(() => _sut.Delete(new SecretVerificationRequestModel()));

Expand All @@ -727,14 +728,79 @@ public async Task Delete_WithUserNotManagedByAnOrganization_ShouldSucceed()
var user = GenerateExampleUser();
ConfigureUserServiceToReturnValidPrincipalFor(user);
ConfigureUserServiceToAcceptPasswordFor(user);
_userService.IsClaimedByAnyOrganizationAsync(user.Id).Returns(false);
_userService.DeleteAsync(user).Returns(IdentityResult.Success);

await _sut.Delete(new SecretVerificationRequestModel());

await _userService.Received(1).DeleteAsync(user);
}

[Fact]
public async Task PostDeleteRecoverToken_WhenUserDoesNotExist_ShouldThrowUnauthorizedAccessException()
{
ConfigureUserServiceToReturnNullUserId();

await Assert.ThrowsAsync<UnauthorizedAccessException>(
() => _sut.PostDeleteRecoverToken(new VerifyDeleteRecoverRequestModel
{
UserId = Guid.NewGuid().ToString(),
Token = "token"
})
);
}

[Fact]
public async Task PostDeleteRecoverToken_WithValidToken_ShouldDeleteAccount()
{
var user = GenerateExampleUser();
ConfigureUserServiceToReturnValidIdFor(user);
_userService.DeleteAsync(user, "token").Returns(Task.FromResult(IdentityResult.Success));

await _sut.PostDeleteRecoverToken(new VerifyDeleteRecoverRequestModel
{
UserId = Guid.NewGuid().ToString(),
Token = "token"
});

await _userService.Received(1).DeleteAsync(user, "token");
}

[Fact]
public async Task PostDeleteRecoverToken_WithInvalidToken_ShouldThrowBadRequestException()
{
var user = GenerateExampleUser();
ConfigureUserServiceToReturnValidIdFor(user);
_userService.DeleteAsync(user, "token")
.Returns(Task.FromResult(IdentityResult.Failed(new IdentityError { Description = "Invalid token." })));

await Assert.ThrowsAsync<BadRequestException>(
() => _sut.PostDeleteRecoverToken(new VerifyDeleteRecoverRequestModel
{
UserId = Guid.NewGuid().ToString(),
Token = "token"
})
);
}

[Fact]
public async Task PostDeleteRecoverToken_WithClaimedAccount_ThrowsBadRequestException()
{
var user = GenerateExampleUser();
ConfigureUserServiceToReturnValidIdFor(user);
_userService.DeleteAsync(user, "token")
.ThrowsAsync(new BadRequestException(new CannotDeleteClaimedAccountError().Message));

var exception = await Assert.ThrowsAsync<BadRequestException>(
() => _sut.PostDeleteRecoverToken(new VerifyDeleteRecoverRequestModel
{
UserId = Guid.NewGuid().ToString(),
Token = "token"
})
);

Assert.Equal(new CannotDeleteClaimedAccountError().Message, exception.Message);
}

[Theory]
[BitAutoData]
public async Task SetVerifyDevices_WhenUserDoesNotExist_ShouldThrowUnauthorizedAccessException(
Expand Down
96 changes: 96 additions & 0 deletions test/Core.Test/Services/UserServiceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Requests;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
Expand Down Expand Up @@ -542,13 +543,39 @@ public async Task AdminResetPasswordAsync_EmptyOrWhitespaceResetPasswordKey_Thro
Assert.Equal("Organization User not valid", exception.Message);
}

[Theory, BitAutoData]
public async Task DeleteAsync_WithClaimedAccount_ThrowsBadRequestException(
User user,
Organization organization,
SutProvider<UserService> sutProvider)
{
organization.Enabled = true;
organization.UseOrganizationDomains = true;

sutProvider.GetDependency<IOrganizationRepository>()
.GetByVerifiedUserEmailDomainAsync(user.Id)
.Returns([organization]);

var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.DeleteAsync(user));

Assert.Equal(new CannotDeleteClaimedAccountError().Message, exception.Message);
await sutProvider.GetDependency<IOrganizationUserRepository>()
.DidNotReceive().GetCountByOnlyOwnerAsync(user.Id);
await sutProvider.GetDependency<IUserRepository>().DidNotReceive().DeleteAsync(user);
}

[Theory, BitAutoData]
public async Task DeleteAsync_WithGatewaySubscription_CallsSubscriberService(
User user,
SutProvider<UserService> sutProvider)
{
user.GatewaySubscriptionId = "sub_test";

sutProvider.GetDependency<IOrganizationRepository>()
.GetByVerifiedUserEmailDomainAsync(user.Id)
.Returns([]);

sutProvider.GetDependency<IOrganizationUserRepository>()
.GetCountByOnlyOwnerAsync(user.Id)
.Returns(0);
Expand Down Expand Up @@ -580,6 +607,10 @@ public async Task DeleteAsync_WithFileSends_DeletesFilesBeforeDbRecords(
// 3. File blob still exists but with no parent Send
user.GatewaySubscriptionId = null;

sutProvider.GetDependency<IOrganizationRepository>()
.GetByVerifiedUserEmailDomainAsync(user.Id)
.Returns([]);

sutProvider.GetDependency<IOrganizationUserRepository>()
.GetCountByOnlyOwnerAsync(user.Id)
.Returns(0);
Expand All @@ -606,6 +637,66 @@ await sutProvider.GetDependency<ISendFileStorageService>()
Assert.Equal(new[] { "file", "db" }, callOrder);
}

[Theory, BitAutoData]
public async Task DeleteAsync_WithTokenAndInvalidToken_ReturnsFailedResult(User user)
{
var sutProvider = new SutProvider<UserService>()
.CreateWithUserServiceCustomizations(user);

var result = await sutProvider.Sut.DeleteAsync(user, "not_the_right_token");

Assert.False(result.Succeeded);
await sutProvider.GetDependency<IOrganizationRepository>()
.DidNotReceive().GetByVerifiedUserEmailDomainAsync(Arg.Any<Guid>());
await sutProvider.GetDependency<IUserRepository>().DidNotReceive().DeleteAsync(user);
}

[Theory, BitAutoData]
public async Task DeleteAsync_WithTokenAndClaimedAccount_ThrowsBadRequestException(
User user, Organization organization)
{
organization.Enabled = true;
organization.UseOrganizationDomains = true;

var sutProvider = new SutProvider<UserService>()
.CreateWithUserServiceCustomizations(user);

sutProvider.GetDependency<IOrganizationRepository>()
.GetByVerifiedUserEmailDomainAsync(user.Id)
.Returns([organization]);

var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.DeleteAsync(user, "otp_token"));

Assert.Equal(new CannotDeleteClaimedAccountError().Message, exception.Message);
await sutProvider.GetDependency<IUserRepository>().DidNotReceive().DeleteAsync(user);
}

[Theory, BitAutoData]
public async Task DeleteAsync_WithTokenAndUnclaimedAccount_DeletesUser(User user)
{
user.GatewaySubscriptionId = null;

var sutProvider = new SutProvider<UserService>()
.CreateWithUserServiceCustomizations(user);

sutProvider.GetDependency<IOrganizationRepository>()
.GetByVerifiedUserEmailDomainAsync(user.Id)
.Returns([]);

sutProvider.GetDependency<IOrganizationUserRepository>()
.GetCountByOnlyOwnerAsync(user.Id)
.Returns(0);
sutProvider.GetDependency<IProviderUserRepository>()
.GetCountByOnlyOwnerAsync(user.Id)
.Returns(0);

var result = await sutProvider.Sut.DeleteAsync(user, "otp_token");

Assert.True(result.Succeeded);
await sutProvider.GetDependency<IUserRepository>().Received(1).DeleteAsync(user);
}

// PM-37165: locks in the legacy path's non-write of LastApiKeyRotationDate. Once the
// PM37165_RotateUserApiKeyCommand flag is cleaned up and this method is deleted, this
// test goes with it.
Expand Down Expand Up @@ -656,6 +747,11 @@ private static SutProvider<UserService> SetFakeTokenProvider(this SutProvider<Us
ProviderMap = new Dictionary<string, TokenProviderDescriptor>()
{
["Email"] = new TokenProviderDescriptor(typeof(IUserTwoFactorTokenProvider<User>))
{
ProviderInstance = fakeUserTwoFactorProvider,
},
// The delete-recover-token endpoint's DeleteAsync(User, string) looks up by "Default", not "Email".
[TokenOptions.DefaultProvider] = new TokenProviderDescriptor(typeof(IUserTwoFactorTokenProvider<User>))
{
ProviderInstance = fakeUserTwoFactorProvider,
}
Expand Down
Loading