mirror of
https://github.com/bitwarden/server
synced 2025-12-28 06:03:29 +00:00
[PM-24192] Move account recovery logic to command (#6184)
* Move account recovery logic to command (temporarily duplicated behind feature flag) * Move permission checks to authorization handler * Prevent user from recovering provider member account unless they are also provider member
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
// FIXME: Update this file to be null safe and then delete the line below
|
||||
// NOTE: This file is partially migrated to nullable reference types. Remove inline #nullable directives when addressing the FIXME.
|
||||
#nullable disable
|
||||
|
||||
using Bit.Api.AdminConsole.Authorization;
|
||||
@@ -11,6 +12,7 @@ using Bit.Api.Vault.AuthorizationHandlers.Collections;
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.AccountRecovery;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccount;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers;
|
||||
@@ -70,6 +72,7 @@ public class OrganizationUsersController : Controller
|
||||
private readonly IRestoreOrganizationUserCommand _restoreOrganizationUserCommand;
|
||||
private readonly IInitPendingOrganizationCommand _initPendingOrganizationCommand;
|
||||
private readonly IRevokeOrganizationUserCommand _revokeOrganizationUserCommand;
|
||||
private readonly IAdminRecoverAccountCommand _adminRecoverAccountCommand;
|
||||
|
||||
public OrganizationUsersController(IOrganizationRepository organizationRepository,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
@@ -97,7 +100,8 @@ public class OrganizationUsersController : Controller
|
||||
IRestoreOrganizationUserCommand restoreOrganizationUserCommand,
|
||||
IInitPendingOrganizationCommand initPendingOrganizationCommand,
|
||||
IRevokeOrganizationUserCommand revokeOrganizationUserCommand,
|
||||
IResendOrganizationInviteCommand resendOrganizationInviteCommand)
|
||||
IResendOrganizationInviteCommand resendOrganizationInviteCommand,
|
||||
IAdminRecoverAccountCommand adminRecoverAccountCommand)
|
||||
{
|
||||
_organizationRepository = organizationRepository;
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
@@ -126,6 +130,7 @@ public class OrganizationUsersController : Controller
|
||||
_restoreOrganizationUserCommand = restoreOrganizationUserCommand;
|
||||
_initPendingOrganizationCommand = initPendingOrganizationCommand;
|
||||
_revokeOrganizationUserCommand = revokeOrganizationUserCommand;
|
||||
_adminRecoverAccountCommand = adminRecoverAccountCommand;
|
||||
}
|
||||
|
||||
[HttpGet("{id}")]
|
||||
@@ -474,21 +479,27 @@ public class OrganizationUsersController : Controller
|
||||
|
||||
[HttpPut("{id}/reset-password")]
|
||||
[Authorize<ManageAccountRecoveryRequirement>]
|
||||
public async Task PutResetPassword(Guid orgId, Guid id, [FromBody] OrganizationUserResetPasswordRequestModel model)
|
||||
public async Task<IResult> PutResetPassword(Guid orgId, Guid id, [FromBody] OrganizationUserResetPasswordRequestModel model)
|
||||
{
|
||||
if (_featureService.IsEnabled(FeatureFlagKeys.AccountRecoveryCommand))
|
||||
{
|
||||
// TODO: remove legacy implementation after feature flag is enabled.
|
||||
return await PutResetPasswordNew(orgId, id, model);
|
||||
}
|
||||
|
||||
// Get the users role, since provider users aren't a member of the organization we use the owner check
|
||||
var orgUserType = await _currentContext.OrganizationOwner(orgId)
|
||||
? OrganizationUserType.Owner
|
||||
: _currentContext.Organizations?.FirstOrDefault(o => o.Id == orgId)?.Type;
|
||||
if (orgUserType == null)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
return TypedResults.NotFound();
|
||||
}
|
||||
|
||||
var result = await _userService.AdminResetPasswordAsync(orgUserType.Value, orgId, id, model.NewMasterPasswordHash, model.Key);
|
||||
if (result.Succeeded)
|
||||
{
|
||||
return;
|
||||
return TypedResults.Ok();
|
||||
}
|
||||
|
||||
foreach (var error in result.Errors)
|
||||
@@ -497,9 +508,45 @@ public class OrganizationUsersController : Controller
|
||||
}
|
||||
|
||||
await Task.Delay(2000);
|
||||
throw new BadRequestException(ModelState);
|
||||
return TypedResults.BadRequest(ModelState);
|
||||
}
|
||||
|
||||
#nullable enable
|
||||
// TODO: make sure the route and authorize attributes are maintained when the legacy implementation is removed.
|
||||
private async Task<IResult> PutResetPasswordNew(Guid orgId, Guid id, [FromBody] OrganizationUserResetPasswordRequestModel model)
|
||||
{
|
||||
var targetOrganizationUser = await _organizationUserRepository.GetByIdAsync(id);
|
||||
if (targetOrganizationUser == null || targetOrganizationUser.OrganizationId != orgId)
|
||||
{
|
||||
return TypedResults.NotFound();
|
||||
}
|
||||
|
||||
var authorizationResult = await _authorizationService.AuthorizeAsync(User, targetOrganizationUser, new RecoverAccountAuthorizationRequirement());
|
||||
if (!authorizationResult.Succeeded)
|
||||
{
|
||||
// Return an informative error to show in the UI.
|
||||
// The Authorize attribute already prevents enumeration by users outside the organization, so this can be specific.
|
||||
var failureReason = authorizationResult.Failure?.FailureReasons.FirstOrDefault()?.Message ?? RecoverAccountAuthorizationHandler.FailureReason;
|
||||
// This should be a 403 Forbidden, but that causes a logout on our client apps so we're using 400 Bad Request instead
|
||||
return TypedResults.BadRequest(new ErrorResponseModel(failureReason));
|
||||
}
|
||||
|
||||
var result = await _adminRecoverAccountCommand.RecoverAccountAsync(orgId, targetOrganizationUser, model.NewMasterPasswordHash, model.Key);
|
||||
if (result.Succeeded)
|
||||
{
|
||||
return TypedResults.Ok();
|
||||
}
|
||||
|
||||
foreach (var error in result.Errors)
|
||||
{
|
||||
ModelState.AddModelError(string.Empty, error.Description);
|
||||
}
|
||||
|
||||
await Task.Delay(2000);
|
||||
return TypedResults.BadRequest(ModelState);
|
||||
}
|
||||
#nullable disable
|
||||
|
||||
[HttpDelete("{id}")]
|
||||
[Authorize<ManageUsersRequirement>]
|
||||
public async Task Remove(Guid orgId, Guid id)
|
||||
|
||||
Reference in New Issue
Block a user