1
0
mirror of https://github.com/bitwarden/server.git synced 2024-11-24 12:35:25 +01:00

[PM-10321/PM-10322] Add Endpoints for Deleting Single and Multiple Organization-Managed Users (#4727)

* Add HasVerifiedDomainsAsync method to IOrganizationDomainService

* Add GetManagedUserIdsByOrganizationIdAsync method to IOrganizationUserRepository and the corresponding queries

* Fix case on the sproc OrganizationUser_ReadManagedIdsByOrganizationId parameter

* Update the EF query to use the Email from the User table

* dotnet format

* Fix IOrganizationDomainService.HasVerifiedDomainsAsync by checking that domains have been Verified and add unit tests

* Rename IOrganizationUserRepository.GetManagedUserIdsByOrganizationAsync

* Fix domain queries

* Add OrganizationUserRepository integration tests

* Add summary to IOrganizationDomainService.HasVerifiedDomainsAsync

* chore: Rename IOrganizationUserRepository.GetManagedUserIdsByOrganizationAsync to GetManyIdsManagedByOrganizationIdAsync

* Add IsManagedByAnyOrganizationAsync method to IUserRepository

* Add integration tests for UserRepository.IsManagedByAnyOrganizationAsync

* Refactor to IUserService.IsManagedByAnyOrganizationAsync and IOrganizationService.GetUsersOrganizationManagementStatusAsync

* chore: Refactor IsManagedByAnyOrganizationAsync method in UserService

* Refactor IOrganizationService.GetUsersOrganizationManagementStatusAsync to return IDictionary<Guid, bool>

* Extract IOrganizationService.GetUsersOrganizationManagementStatusAsync into a query

* Update comments in OrganizationDomainService to use proper capitalization

* Move OrganizationDomainService to AdminConsole ownership and update namespace

* feat: Add support for organization domains in enterprise plans

* feat: Add HasOrganizationDomains property to OrganizationAbility class

* refactor: Update GetOrganizationUsersManagementStatusQuery to use IApplicationCacheService

* Remove HasOrganizationDomains and use UseSso to check if Organization can have Verified Domains

* Refactor UserService.IsManagedByAnyOrganizationAsync to simply check the UseSso flag

* Add new event types for organization user deletion and voluntary departure

* Add DeleteManagedOrganizationUserAccountCommand to remove user and delete account

* Refactor DeleteManagedOrganizationUserAccountCommand to use orgUser.Id instead of orgUser.UserId.Value

* Add DeleteManagedOrganizationUserAccountCommandTests

* Add an endpoint to the OrganizationUsersController to delete a user account managed by an organization

* Add unit tests for OrganizationUsersController.DeleteAccount

* Add an endpoint to the OrganizationUsersController to bulk delete user accounts managed by an organization

* Add unit tests for OrganizationUsersController.BulkDeleteAccount

* Gate new endpoints behind feature flag

* Remove duplicate migration

* Remove unnecessary _userService.GetProperUserId
This commit is contained in:
Rui Tomé 2024-10-01 15:45:23 +01:00 committed by GitHub
parent 594b2a274d
commit 337eedcd2c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 202 additions and 1 deletions

View File

@ -1,5 +1,6 @@
using Bit.Api.AdminConsole.Models.Request.Organizations; using Bit.Api.AdminConsole.Models.Request.Organizations;
using Bit.Api.AdminConsole.Models.Response.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.Request.Organizations;
using Bit.Api.Models.Response; using Bit.Api.Models.Response;
using Bit.Api.Vault.AuthorizationHandlers.Collections; using Bit.Api.Vault.AuthorizationHandlers.Collections;
@ -51,6 +52,7 @@ public class OrganizationUsersController : Controller
private readonly ISsoConfigRepository _ssoConfigRepository; private readonly ISsoConfigRepository _ssoConfigRepository;
private readonly IOrganizationUserUserDetailsQuery _organizationUserUserDetailsQuery; private readonly IOrganizationUserUserDetailsQuery _organizationUserUserDetailsQuery;
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery; private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
private readonly IDeleteManagedOrganizationUserAccountCommand _deleteManagedOrganizationUserAccountCommand;
public OrganizationUsersController( public OrganizationUsersController(
@ -71,7 +73,8 @@ public class OrganizationUsersController : Controller
IFeatureService featureService, IFeatureService featureService,
ISsoConfigRepository ssoConfigRepository, ISsoConfigRepository ssoConfigRepository,
IOrganizationUserUserDetailsQuery organizationUserUserDetailsQuery, IOrganizationUserUserDetailsQuery organizationUserUserDetailsQuery,
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery) ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
IDeleteManagedOrganizationUserAccountCommand deleteManagedOrganizationUserAccountCommand)
{ {
_organizationRepository = organizationRepository; _organizationRepository = organizationRepository;
_organizationUserRepository = organizationUserRepository; _organizationUserRepository = organizationUserRepository;
@ -91,6 +94,7 @@ public class OrganizationUsersController : Controller
_ssoConfigRepository = ssoConfigRepository; _ssoConfigRepository = ssoConfigRepository;
_organizationUserUserDetailsQuery = organizationUserUserDetailsQuery; _organizationUserUserDetailsQuery = organizationUserUserDetailsQuery;
_twoFactorIsEnabledQuery = twoFactorIsEnabledQuery; _twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
_deleteManagedOrganizationUserAccountCommand = deleteManagedOrganizationUserAccountCommand;
} }
[HttpGet("{id}")] [HttpGet("{id}")]
@ -541,6 +545,59 @@ public class OrganizationUsersController : Controller
new OrganizationUserBulkResponseModel(r.Item1.Id, r.Item2))); 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<ListResponseModel<OrganizationUserBulkResponseModel>> 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<OrganizationUserBulkResponseModel>(results.Select(r =>
new OrganizationUserBulkResponseModel(r.OrganizationUserId, r.ErrorMessage)));
}
[HttpPatch("{id}/revoke")] [HttpPatch("{id}/revoke")]
[HttpPut("{id}/revoke")] [HttpPut("{id}/revoke")]
public async Task RevokeAsync(Guid orgId, Guid id) public async Task RevokeAsync(Guid orgId, Guid id)

View File

@ -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<Guid> Ids { get; set; }
}

View File

@ -1,10 +1,12 @@
using System.Security.Claims; using System.Security.Claims;
using Bit.Api.AdminConsole.Controllers; using Bit.Api.AdminConsole.Controllers;
using Bit.Api.AdminConsole.Models.Request.Organizations; using Bit.Api.AdminConsole.Models.Request.Organizations;
using Bit.Api.Auth.Models.Request.Accounts;
using Bit.Api.Vault.AuthorizationHandlers.Collections; using Bit.Api.Vault.AuthorizationHandlers.Collections;
using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Auth.Entities; using Bit.Core.Auth.Entities;
using Bit.Core.Auth.Repositories; using Bit.Core.Auth.Repositories;
@ -234,6 +236,138 @@ public class OrganizationUsersControllerTests
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.GetAccountRecoveryDetails(organizationId, bulkRequestModel)); await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.GetAccountRecoveryDetails(organizationId, bulkRequestModel));
} }
[Theory]
[BitAutoData]
public async Task DeleteAccount_WhenUserCanManageUsers_Success(
Guid orgId,
Guid id,
SecretVerificationRequestModel model,
User currentUser,
SutProvider<OrganizationUsersController> sutProvider)
{
sutProvider.GetDependency<ICurrentContext>().ManageUsers(orgId).Returns(true);
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(currentUser);
sutProvider.GetDependency<IUserService>().VerifySecretAsync(currentUser, model.Secret).Returns(true);
await sutProvider.Sut.DeleteAccount(orgId, id, model);
await sutProvider.GetDependency<IDeleteManagedOrganizationUserAccountCommand>()
.Received(1)
.DeleteUserAsync(orgId, id, currentUser.Id);
}
[Theory]
[BitAutoData]
public async Task DeleteAccount_WhenUserCannotManageUsers_ThrowsNotFoundException(
Guid orgId,
Guid id,
SecretVerificationRequestModel model,
SutProvider<OrganizationUsersController> sutProvider)
{
sutProvider.GetDependency<ICurrentContext>().ManageUsers(orgId).Returns(false);
await Assert.ThrowsAsync<NotFoundException>(() =>
sutProvider.Sut.DeleteAccount(orgId, id, model));
}
[Theory]
[BitAutoData]
public async Task DeleteAccount_WhenCurrentUserNotFound_ThrowsUnauthorizedAccessException(
Guid orgId,
Guid id,
SecretVerificationRequestModel model,
SutProvider<OrganizationUsersController> sutProvider)
{
sutProvider.GetDependency<ICurrentContext>().ManageUsers(orgId).Returns(true);
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs((User)null);
await Assert.ThrowsAsync<UnauthorizedAccessException>(() =>
sutProvider.Sut.DeleteAccount(orgId, id, model));
}
[Theory]
[BitAutoData]
public async Task DeleteAccount_WhenSecretVerificationFails_ThrowsBadRequestException(
Guid orgId,
Guid id,
SecretVerificationRequestModel model,
User currentUser,
SutProvider<OrganizationUsersController> sutProvider)
{
sutProvider.GetDependency<ICurrentContext>().ManageUsers(orgId).Returns(true);
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(currentUser);
sutProvider.GetDependency<IUserService>().VerifySecretAsync(currentUser, model.Secret).Returns(false);
await Assert.ThrowsAsync<BadRequestException>(() => 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<OrganizationUsersController> sutProvider)
{
sutProvider.GetDependency<ICurrentContext>().ManageUsers(orgId).Returns(true);
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(currentUser);
sutProvider.GetDependency<IUserService>().VerifySecretAsync(currentUser, model.Secret).Returns(true);
sutProvider.GetDependency<IDeleteManagedOrganizationUserAccountCommand>()
.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<IDeleteManagedOrganizationUserAccountCommand>()
.Received(1)
.DeleteManyUsersAsync(orgId, model.Ids, currentUser.Id);
}
[Theory]
[BitAutoData]
public async Task BulkDeleteAccount_WhenUserCannotManageUsers_ThrowsNotFoundException(
Guid orgId,
SecureOrganizationUserBulkRequestModel model,
SutProvider<OrganizationUsersController> sutProvider)
{
sutProvider.GetDependency<ICurrentContext>().ManageUsers(orgId).Returns(false);
await Assert.ThrowsAsync<NotFoundException>(() =>
sutProvider.Sut.BulkDeleteAccount(orgId, model));
}
[Theory]
[BitAutoData]
public async Task BulkDeleteAccount_WhenCurrentUserNotFound_ThrowsUnauthorizedAccessException(
Guid orgId,
SecureOrganizationUserBulkRequestModel model,
SutProvider<OrganizationUsersController> sutProvider)
{
sutProvider.GetDependency<ICurrentContext>().ManageUsers(orgId).Returns(true);
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs((User)null);
await Assert.ThrowsAsync<UnauthorizedAccessException>(() =>
sutProvider.Sut.BulkDeleteAccount(orgId, model));
}
[Theory]
[BitAutoData]
public async Task BulkDeleteAccount_WhenSecretVerificationFails_ThrowsBadRequestException(
Guid orgId,
SecureOrganizationUserBulkRequestModel model,
User currentUser,
SutProvider<OrganizationUsersController> sutProvider)
{
sutProvider.GetDependency<ICurrentContext>().ManageUsers(orgId).Returns(true);
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(currentUser);
sutProvider.GetDependency<IUserService>().VerifySecretAsync(currentUser, model.Secret).Returns(false);
await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.BulkDeleteAccount(orgId, model));
}
private void Get_Setup(OrganizationAbility organizationAbility, private void Get_Setup(OrganizationAbility organizationAbility,
ICollection<OrganizationUserUserDetails> organizationUsers, ICollection<OrganizationUserUserDetails> organizationUsers,
SutProvider<OrganizationUsersController> sutProvider) SutProvider<OrganizationUsersController> sutProvider)