mirror of
https://github.com/bitwarden/server.git
synced 2024-11-21 12:05:42 +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:
parent
594b2a274d
commit
337eedcd2c
@ -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)
|
||||||
|
@ -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; }
|
||||||
|
}
|
@ -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)
|
||||||
|
Loading…
Reference in New Issue
Block a user