diff --git a/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs b/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs index 990a9d51c..cd6bdd6fa 100644 --- a/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs +++ b/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs @@ -1,5 +1,6 @@ using Bit.Api.AdminConsole.Models.Request.Organizations; using Bit.Api.AdminConsole.Models.Response.Organizations; +using Bit.Api.Auth.Models.Request.Accounts; using Bit.Api.Models.Request.Organizations; using Bit.Api.Models.Response; using Bit.Api.Vault.AuthorizationHandlers.Collections; @@ -51,6 +52,7 @@ public class OrganizationUsersController : Controller private readonly ISsoConfigRepository _ssoConfigRepository; private readonly IOrganizationUserUserDetailsQuery _organizationUserUserDetailsQuery; private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery; + private readonly IDeleteManagedOrganizationUserAccountCommand _deleteManagedOrganizationUserAccountCommand; public OrganizationUsersController( @@ -71,7 +73,8 @@ public class OrganizationUsersController : Controller IFeatureService featureService, ISsoConfigRepository ssoConfigRepository, IOrganizationUserUserDetailsQuery organizationUserUserDetailsQuery, - ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery) + ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery, + IDeleteManagedOrganizationUserAccountCommand deleteManagedOrganizationUserAccountCommand) { _organizationRepository = organizationRepository; _organizationUserRepository = organizationUserRepository; @@ -91,6 +94,7 @@ public class OrganizationUsersController : Controller _ssoConfigRepository = ssoConfigRepository; _organizationUserUserDetailsQuery = organizationUserUserDetailsQuery; _twoFactorIsEnabledQuery = twoFactorIsEnabledQuery; + _deleteManagedOrganizationUserAccountCommand = deleteManagedOrganizationUserAccountCommand; } [HttpGet("{id}")] @@ -541,6 +545,59 @@ public class OrganizationUsersController : Controller new OrganizationUserBulkResponseModel(r.Item1.Id, r.Item2))); } + [RequireFeature(FeatureFlagKeys.AccountDeprovisioning)] + [HttpDelete("{id}/delete-account")] + [HttpPost("{id}/delete-account")] + public async Task DeleteAccount(Guid orgId, Guid id, [FromBody] SecretVerificationRequestModel model) + { + if (!await _currentContext.ManageUsers(orgId)) + { + throw new NotFoundException(); + } + + var currentUser = await _userService.GetUserByPrincipalAsync(User); + if (currentUser == null) + { + throw new UnauthorizedAccessException(); + } + + if (!await _userService.VerifySecretAsync(currentUser, model.Secret)) + { + await Task.Delay(2000); + throw new BadRequestException(string.Empty, "User verification failed."); + } + + await _deleteManagedOrganizationUserAccountCommand.DeleteUserAsync(orgId, id, currentUser.Id); + } + + [RequireFeature(FeatureFlagKeys.AccountDeprovisioning)] + [HttpDelete("delete-account")] + [HttpPost("delete-account")] + public async Task> BulkDeleteAccount(Guid orgId, [FromBody] SecureOrganizationUserBulkRequestModel model) + { + if (!await _currentContext.ManageUsers(orgId)) + { + throw new NotFoundException(); + } + + var currentUser = await _userService.GetUserByPrincipalAsync(User); + if (currentUser == null) + { + throw new UnauthorizedAccessException(); + } + + if (!await _userService.VerifySecretAsync(currentUser, model.Secret)) + { + await Task.Delay(2000); + throw new BadRequestException(string.Empty, "User verification failed."); + } + + var results = await _deleteManagedOrganizationUserAccountCommand.DeleteManyUsersAsync(orgId, model.Ids, currentUser.Id); + + return new ListResponseModel(results.Select(r => + new OrganizationUserBulkResponseModel(r.OrganizationUserId, r.ErrorMessage))); + } + [HttpPatch("{id}/revoke")] [HttpPut("{id}/revoke")] public async Task RevokeAsync(Guid orgId, Guid id) diff --git a/src/Api/AdminConsole/Models/Request/Organizations/SecureOrganizationUserBulkRequestModel.cs b/src/Api/AdminConsole/Models/Request/Organizations/SecureOrganizationUserBulkRequestModel.cs new file mode 100644 index 000000000..f8edb08ba --- /dev/null +++ b/src/Api/AdminConsole/Models/Request/Organizations/SecureOrganizationUserBulkRequestModel.cs @@ -0,0 +1,10 @@ +using System.ComponentModel.DataAnnotations; +using Bit.Api.Auth.Models.Request.Accounts; + +namespace Bit.Api.AdminConsole.Models.Request.Organizations; + +public class SecureOrganizationUserBulkRequestModel : SecretVerificationRequestModel +{ + [Required] + public IEnumerable Ids { get; set; } +} diff --git a/test/Api.Test/AdminConsole/Controllers/OrganizationUsersControllerTests.cs b/test/Api.Test/AdminConsole/Controllers/OrganizationUsersControllerTests.cs index ef6896a6a..492112e5a 100644 --- a/test/Api.Test/AdminConsole/Controllers/OrganizationUsersControllerTests.cs +++ b/test/Api.Test/AdminConsole/Controllers/OrganizationUsersControllerTests.cs @@ -1,10 +1,12 @@ using System.Security.Claims; using Bit.Api.AdminConsole.Controllers; using Bit.Api.AdminConsole.Models.Request.Organizations; +using Bit.Api.Auth.Models.Request.Accounts; using Bit.Api.Vault.AuthorizationHandlers.Collections; using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Auth.Entities; using Bit.Core.Auth.Repositories; @@ -234,6 +236,138 @@ public class OrganizationUsersControllerTests await Assert.ThrowsAsync(async () => await sutProvider.Sut.GetAccountRecoveryDetails(organizationId, bulkRequestModel)); } + [Theory] + [BitAutoData] + public async Task DeleteAccount_WhenUserCanManageUsers_Success( + Guid orgId, + Guid id, + SecretVerificationRequestModel model, + User currentUser, + SutProvider sutProvider) + { + sutProvider.GetDependency().ManageUsers(orgId).Returns(true); + sutProvider.GetDependency().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(currentUser); + sutProvider.GetDependency().VerifySecretAsync(currentUser, model.Secret).Returns(true); + + await sutProvider.Sut.DeleteAccount(orgId, id, model); + + await sutProvider.GetDependency() + .Received(1) + .DeleteUserAsync(orgId, id, currentUser.Id); + } + + [Theory] + [BitAutoData] + public async Task DeleteAccount_WhenUserCannotManageUsers_ThrowsNotFoundException( + Guid orgId, + Guid id, + SecretVerificationRequestModel model, + SutProvider sutProvider) + { + sutProvider.GetDependency().ManageUsers(orgId).Returns(false); + + await Assert.ThrowsAsync(() => + sutProvider.Sut.DeleteAccount(orgId, id, model)); + } + + [Theory] + [BitAutoData] + public async Task DeleteAccount_WhenCurrentUserNotFound_ThrowsUnauthorizedAccessException( + Guid orgId, + Guid id, + SecretVerificationRequestModel model, + SutProvider sutProvider) + { + sutProvider.GetDependency().ManageUsers(orgId).Returns(true); + sutProvider.GetDependency().GetUserByPrincipalAsync(default).ReturnsForAnyArgs((User)null); + + await Assert.ThrowsAsync(() => + sutProvider.Sut.DeleteAccount(orgId, id, model)); + } + + [Theory] + [BitAutoData] + public async Task DeleteAccount_WhenSecretVerificationFails_ThrowsBadRequestException( + Guid orgId, + Guid id, + SecretVerificationRequestModel model, + User currentUser, + SutProvider sutProvider) + { + sutProvider.GetDependency().ManageUsers(orgId).Returns(true); + sutProvider.GetDependency().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(currentUser); + sutProvider.GetDependency().VerifySecretAsync(currentUser, model.Secret).Returns(false); + + await Assert.ThrowsAsync(() => sutProvider.Sut.DeleteAccount(orgId, id, model)); + } + + [Theory] + [BitAutoData] + public async Task BulkDeleteAccount_WhenUserCanManageUsers_Success( + Guid orgId, + SecureOrganizationUserBulkRequestModel model, + User currentUser, + List<(Guid, string)> deleteResults, + SutProvider sutProvider) + { + sutProvider.GetDependency().ManageUsers(orgId).Returns(true); + sutProvider.GetDependency().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(currentUser); + sutProvider.GetDependency().VerifySecretAsync(currentUser, model.Secret).Returns(true); + sutProvider.GetDependency() + .DeleteManyUsersAsync(orgId, model.Ids, currentUser.Id) + .Returns(deleteResults); + + var response = await sutProvider.Sut.BulkDeleteAccount(orgId, model); + + Assert.Equal(deleteResults.Count, response.Data.Count()); + Assert.True(response.Data.All(r => deleteResults.Any(res => res.Item1 == r.Id && res.Item2 == r.Error))); + await sutProvider.GetDependency() + .Received(1) + .DeleteManyUsersAsync(orgId, model.Ids, currentUser.Id); + } + + [Theory] + [BitAutoData] + public async Task BulkDeleteAccount_WhenUserCannotManageUsers_ThrowsNotFoundException( + Guid orgId, + SecureOrganizationUserBulkRequestModel model, + SutProvider sutProvider) + { + sutProvider.GetDependency().ManageUsers(orgId).Returns(false); + + await Assert.ThrowsAsync(() => + sutProvider.Sut.BulkDeleteAccount(orgId, model)); + } + + [Theory] + [BitAutoData] + public async Task BulkDeleteAccount_WhenCurrentUserNotFound_ThrowsUnauthorizedAccessException( + Guid orgId, + SecureOrganizationUserBulkRequestModel model, + SutProvider sutProvider) + { + sutProvider.GetDependency().ManageUsers(orgId).Returns(true); + sutProvider.GetDependency().GetUserByPrincipalAsync(default).ReturnsForAnyArgs((User)null); + + await Assert.ThrowsAsync(() => + sutProvider.Sut.BulkDeleteAccount(orgId, model)); + } + + [Theory] + [BitAutoData] + public async Task BulkDeleteAccount_WhenSecretVerificationFails_ThrowsBadRequestException( + Guid orgId, + SecureOrganizationUserBulkRequestModel model, + User currentUser, + SutProvider sutProvider) + { + sutProvider.GetDependency().ManageUsers(orgId).Returns(true); + sutProvider.GetDependency().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(currentUser); + sutProvider.GetDependency().VerifySecretAsync(currentUser, model.Secret).Returns(false); + + await Assert.ThrowsAsync(() => sutProvider.Sut.BulkDeleteAccount(orgId, model)); + } + private void Get_Setup(OrganizationAbility organizationAbility, ICollection organizationUsers, SutProvider sutProvider)