mirror of
https://github.com/bitwarden/server
synced 2026-01-02 16:43:25 +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:
@@ -0,0 +1,79 @@
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Platform.Push;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.AccountRecovery;
|
||||
|
||||
public class AdminRecoverAccountCommand(IOrganizationRepository organizationRepository,
|
||||
IPolicyRepository policyRepository,
|
||||
IUserRepository userRepository,
|
||||
IMailService mailService,
|
||||
IEventService eventService,
|
||||
IPushNotificationService pushNotificationService,
|
||||
IUserService userService,
|
||||
TimeProvider timeProvider) : IAdminRecoverAccountCommand
|
||||
{
|
||||
public async Task<IdentityResult> RecoverAccountAsync(Guid orgId,
|
||||
OrganizationUser organizationUser, string newMasterPassword, string key)
|
||||
{
|
||||
// Org must be able to use reset password
|
||||
var org = await organizationRepository.GetByIdAsync(orgId);
|
||||
if (org == null || !org.UseResetPassword)
|
||||
{
|
||||
throw new BadRequestException("Organization does not allow password reset.");
|
||||
}
|
||||
|
||||
// Enterprise policy must be enabled
|
||||
var resetPasswordPolicy =
|
||||
await policyRepository.GetByOrganizationIdTypeAsync(orgId, PolicyType.ResetPassword);
|
||||
if (resetPasswordPolicy == null || !resetPasswordPolicy.Enabled)
|
||||
{
|
||||
throw new BadRequestException("Organization does not have the password reset policy enabled.");
|
||||
}
|
||||
|
||||
// Org User must be confirmed and have a ResetPasswordKey
|
||||
if (organizationUser == null ||
|
||||
organizationUser.Status != OrganizationUserStatusType.Confirmed ||
|
||||
organizationUser.OrganizationId != orgId ||
|
||||
string.IsNullOrEmpty(organizationUser.ResetPasswordKey) ||
|
||||
!organizationUser.UserId.HasValue)
|
||||
{
|
||||
throw new BadRequestException("Organization User not valid");
|
||||
}
|
||||
|
||||
var user = await userService.GetUserByIdAsync(organizationUser.UserId.Value);
|
||||
if (user == null)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
if (user.UsesKeyConnector)
|
||||
{
|
||||
throw new BadRequestException("Cannot reset password of a user with Key Connector.");
|
||||
}
|
||||
|
||||
var result = await userService.UpdatePasswordHash(user, newMasterPassword);
|
||||
if (!result.Succeeded)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
user.RevisionDate = user.AccountRevisionDate = timeProvider.GetUtcNow().UtcDateTime;
|
||||
user.LastPasswordChangeDate = user.RevisionDate;
|
||||
user.ForcePasswordReset = true;
|
||||
user.Key = key;
|
||||
|
||||
await userRepository.ReplaceAsync(user);
|
||||
await mailService.SendAdminResetPasswordEmailAsync(user.Email, user.Name, org.DisplayName());
|
||||
await eventService.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_AdminResetPassword);
|
||||
await pushNotificationService.PushLogOutAsync(user.Id);
|
||||
|
||||
return IdentityResult.Success;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Exceptions;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.AccountRecovery;
|
||||
|
||||
/// <summary>
|
||||
/// A command used to recover an organization user's account by an organization admin.
|
||||
/// </summary>
|
||||
public interface IAdminRecoverAccountCommand
|
||||
{
|
||||
/// <summary>
|
||||
/// Recovers an organization user's account by resetting their master password.
|
||||
/// </summary>
|
||||
/// <param name="orgId">The organization the user belongs to.</param>
|
||||
/// <param name="organizationUser">The organization user being recovered.</param>
|
||||
/// <param name="newMasterPassword">The user's new master password hash.</param>
|
||||
/// <param name="key">The user's new master-password-sealed user key.</param>
|
||||
/// <returns>An IdentityResult indicating success or failure.</returns>
|
||||
/// <exception cref="BadRequestException">When organization settings, policy, or user state is invalid.</exception>
|
||||
/// <exception cref="NotFoundException">When the user does not exist.</exception>
|
||||
Task<IdentityResult> RecoverAccountAsync(Guid orgId, OrganizationUser organizationUser,
|
||||
string newMasterPassword, string key);
|
||||
}
|
||||
@@ -142,6 +142,7 @@ public static class FeatureFlagKeys
|
||||
public const string CreateDefaultLocation = "pm-19467-create-default-location";
|
||||
public const string AutomaticConfirmUsers = "pm-19934-auto-confirm-organization-users";
|
||||
public const string PM23845_VNextApplicationCache = "pm-24957-refactor-memory-application-cache";
|
||||
public const string AccountRecoveryCommand = "pm-24192-account-recovery-command";
|
||||
|
||||
/* Auth Team */
|
||||
public const string TwoFactorExtensionDataPersistence = "pm-9115-two-factor-extension-data-persistence";
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
#nullable enable
|
||||
|
||||
using System.Security.Claims;
|
||||
using System.Security.Claims;
|
||||
using Bit.Core.AdminConsole.Context;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Auth.Identity;
|
||||
@@ -12,6 +10,14 @@ using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace Bit.Core.Context;
|
||||
|
||||
/// <summary>
|
||||
/// Provides information about the current HTTP request and the currently authenticated user (if any).
|
||||
/// This is often (but not exclusively) parsed from the JWT in the current request.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This interface suffers from having too much responsibility; consider whether any new code can go in a more
|
||||
/// specific class rather than adding it here.
|
||||
/// </remarks>
|
||||
public interface ICurrentContext
|
||||
{
|
||||
HttpContext HttpContext { get; set; }
|
||||
@@ -59,8 +65,20 @@ public interface ICurrentContext
|
||||
Task<bool> EditSubscription(Guid orgId);
|
||||
Task<bool> EditPaymentMethods(Guid orgId);
|
||||
Task<bool> ViewBillingHistory(Guid orgId);
|
||||
/// <summary>
|
||||
/// Returns true if the current user is a member of a provider that manages the specified organization.
|
||||
/// This generally gives the user administrative privileges for the organization.
|
||||
/// </summary>
|
||||
/// <param name="orgId"></param>
|
||||
/// <returns></returns>
|
||||
Task<bool> ProviderUserForOrgAsync(Guid orgId);
|
||||
/// <summary>
|
||||
/// Returns true if the current user is a Provider Admin of the specified provider.
|
||||
/// </summary>
|
||||
bool ProviderProviderAdmin(Guid providerId);
|
||||
/// <summary>
|
||||
/// Returns true if the current user is a member of the specified provider (with any role).
|
||||
/// </summary>
|
||||
bool ProviderUser(Guid providerId);
|
||||
bool ProviderManageUsers(Guid providerId);
|
||||
bool ProviderAccessEventLogs(Guid providerId);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Bit.Core.AdminConsole.OrganizationAuth;
|
||||
using Bit.Core.AdminConsole.OrganizationAuth.Interfaces;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.AccountRecovery;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Groups;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Groups.Interfaces;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Import;
|
||||
@@ -133,6 +134,7 @@ public static class OrganizationServiceCollectionExtensions
|
||||
services.AddScoped<IUpdateOrganizationUserCommand, UpdateOrganizationUserCommand>();
|
||||
services.AddScoped<IUpdateOrganizationUserGroupsCommand, UpdateOrganizationUserGroupsCommand>();
|
||||
services.AddScoped<IConfirmOrganizationUserCommand, ConfirmOrganizationUserCommand>();
|
||||
services.AddScoped<IAdminRecoverAccountCommand, AdminRecoverAccountCommand>();
|
||||
|
||||
services.AddScoped<IDeleteClaimedOrganizationUserAccountCommand, DeleteClaimedOrganizationUserAccountCommand>();
|
||||
services.AddScoped<IDeleteClaimedOrganizationUserAccountValidator, DeleteClaimedOrganizationUserAccountValidator>();
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
#nullable enable
|
||||
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Entities.Provider;
|
||||
using Bit.Core.Auth.Entities;
|
||||
using Bit.Core.Auth.Enums;
|
||||
@@ -92,7 +90,7 @@ public interface IMailService
|
||||
Task SendEmergencyAccessRecoveryReminder(EmergencyAccess emergencyAccess, string initiatingName, string email);
|
||||
Task SendEmergencyAccessRecoveryTimedOut(EmergencyAccess ea, string initiatingName, string email);
|
||||
Task SendEnqueuedMailMessageAsync(IMailQueueMessage queueMessage);
|
||||
Task SendAdminResetPasswordEmailAsync(string email, string userName, string orgName);
|
||||
Task SendAdminResetPasswordEmailAsync(string email, string? userName, string orgName);
|
||||
Task SendProviderSetupInviteEmailAsync(Provider provider, string token, string email);
|
||||
Task SendBusinessUnitConversionInviteAsync(Organization organization, string token, string email);
|
||||
Task SendProviderInviteEmailAsync(string providerName, ProviderUser providerUser, string token, string email);
|
||||
|
||||
@@ -674,7 +674,7 @@ public class HandlebarsMailService : IMailService
|
||||
await _mailDeliveryService.SendEmailAsync(message);
|
||||
}
|
||||
|
||||
public async Task SendAdminResetPasswordEmailAsync(string email, string userName, string orgName)
|
||||
public async Task SendAdminResetPasswordEmailAsync(string email, string? userName, string orgName)
|
||||
{
|
||||
var message = CreateDefaultMessage("Your admin has initiated account recovery", email);
|
||||
var model = new AdminResetPasswordViewModel()
|
||||
|
||||
@@ -221,7 +221,7 @@ public class NoopMailService : IMailService
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public Task SendAdminResetPasswordEmailAsync(string email, string userName, string orgName)
|
||||
public Task SendAdminResetPasswordEmailAsync(string email, string? userName, string orgName)
|
||||
{
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user