mirror of
https://github.com/bitwarden/server.git
synced 2025-07-01 08:02:49 -05:00
[PM-13322] [BEEEP] Add PolicyValidators and refactor policy save logic (#4877)
This commit is contained in:
@ -0,0 +1,43 @@
|
||||
#nullable enable
|
||||
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;
|
||||
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||
|
||||
/// <summary>
|
||||
/// Defines behavior and functionality for a given PolicyType.
|
||||
/// </summary>
|
||||
public interface IPolicyValidator
|
||||
{
|
||||
/// <summary>
|
||||
/// The PolicyType that this definition relates to.
|
||||
/// </summary>
|
||||
public PolicyType Type { get; }
|
||||
|
||||
/// <summary>
|
||||
/// PolicyTypes that must be enabled before this policy can be enabled, if any.
|
||||
/// These dependencies will be checked when this policy is enabled and when any required policy is disabled.
|
||||
/// </summary>
|
||||
public IEnumerable<PolicyType> RequiredPolicies { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Validates a policy before saving it.
|
||||
/// Do not use this for simple dependencies between different policies - see <see cref="RequiredPolicies"/> instead.
|
||||
/// Implementation is optional; by default it will not perform any validation.
|
||||
/// </summary>
|
||||
/// <param name="policyUpdate">The policy update request</param>
|
||||
/// <param name="currentPolicy">The current policy, if any</param>
|
||||
/// <returns>A validation error if validation was unsuccessful, otherwise an empty string</returns>
|
||||
public Task<string> ValidateAsync(PolicyUpdate policyUpdate, Policy? currentPolicy);
|
||||
|
||||
/// <summary>
|
||||
/// Performs side effects after a policy is validated but before it is saved.
|
||||
/// For example, this can be used to remove non-compliant users from the organization.
|
||||
/// Implementation is optional; by default it will not perform any side effects.
|
||||
/// </summary>
|
||||
/// <param name="policyUpdate">The policy update request</param>
|
||||
/// <param name="currentPolicy">The current policy, if any</param>
|
||||
public Task OnSaveSideEffectsAsync(PolicyUpdate policyUpdate, Policy? currentPolicy);
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;
|
||||
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||
|
||||
public interface ISavePolicyCommand
|
||||
{
|
||||
Task SaveAsync(PolicyUpdate policy);
|
||||
}
|
@ -0,0 +1,129 @@
|
||||
#nullable enable
|
||||
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Services;
|
||||
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.Implementations;
|
||||
|
||||
public class SavePolicyCommand : ISavePolicyCommand
|
||||
{
|
||||
private readonly IApplicationCacheService _applicationCacheService;
|
||||
private readonly IEventService _eventService;
|
||||
private readonly IPolicyRepository _policyRepository;
|
||||
private readonly IReadOnlyDictionary<PolicyType, IPolicyValidator> _policyValidators;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
public SavePolicyCommand(
|
||||
IApplicationCacheService applicationCacheService,
|
||||
IEventService eventService,
|
||||
IPolicyRepository policyRepository,
|
||||
IEnumerable<IPolicyValidator> policyValidators,
|
||||
TimeProvider timeProvider)
|
||||
{
|
||||
_applicationCacheService = applicationCacheService;
|
||||
_eventService = eventService;
|
||||
_policyRepository = policyRepository;
|
||||
_timeProvider = timeProvider;
|
||||
|
||||
var policyValidatorsDict = new Dictionary<PolicyType, IPolicyValidator>();
|
||||
foreach (var policyValidator in policyValidators)
|
||||
{
|
||||
if (!policyValidatorsDict.TryAdd(policyValidator.Type, policyValidator))
|
||||
{
|
||||
throw new Exception($"Duplicate PolicyValidator for {policyValidator.Type} policy.");
|
||||
}
|
||||
}
|
||||
|
||||
_policyValidators = policyValidatorsDict;
|
||||
}
|
||||
|
||||
public async Task SaveAsync(PolicyUpdate policyUpdate)
|
||||
{
|
||||
var org = await _applicationCacheService.GetOrganizationAbilityAsync(policyUpdate.OrganizationId);
|
||||
if (org == null)
|
||||
{
|
||||
throw new BadRequestException("Organization not found");
|
||||
}
|
||||
|
||||
if (!org.UsePolicies)
|
||||
{
|
||||
throw new BadRequestException("This organization cannot use policies.");
|
||||
}
|
||||
|
||||
if (_policyValidators.TryGetValue(policyUpdate.Type, out var validator))
|
||||
{
|
||||
await RunValidatorAsync(validator, policyUpdate);
|
||||
}
|
||||
|
||||
var policy = await _policyRepository.GetByOrganizationIdTypeAsync(policyUpdate.OrganizationId, policyUpdate.Type)
|
||||
?? new Policy
|
||||
{
|
||||
OrganizationId = policyUpdate.OrganizationId,
|
||||
Type = policyUpdate.Type,
|
||||
CreationDate = _timeProvider.GetUtcNow().UtcDateTime
|
||||
};
|
||||
|
||||
policy.Enabled = policyUpdate.Enabled;
|
||||
policy.Data = policyUpdate.Data;
|
||||
policy.RevisionDate = _timeProvider.GetUtcNow().UtcDateTime;
|
||||
|
||||
await _policyRepository.UpsertAsync(policy);
|
||||
await _eventService.LogPolicyEventAsync(policy, EventType.Policy_Updated);
|
||||
}
|
||||
|
||||
private async Task RunValidatorAsync(IPolicyValidator validator, PolicyUpdate policyUpdate)
|
||||
{
|
||||
var savedPolicies = await _policyRepository.GetManyByOrganizationIdAsync(policyUpdate.OrganizationId);
|
||||
// Note: policies may be missing from this dict if they have never been enabled
|
||||
var savedPoliciesDict = savedPolicies.ToDictionary(p => p.Type);
|
||||
var currentPolicy = savedPoliciesDict.GetValueOrDefault(policyUpdate.Type);
|
||||
|
||||
// If enabling this policy - check that all policy requirements are satisfied
|
||||
if (currentPolicy is not { Enabled: true } && policyUpdate.Enabled)
|
||||
{
|
||||
var missingRequiredPolicyTypes = validator.RequiredPolicies
|
||||
.Where(requiredPolicyType =>
|
||||
savedPoliciesDict.GetValueOrDefault(requiredPolicyType) is not { Enabled: true })
|
||||
.ToList();
|
||||
|
||||
if (missingRequiredPolicyTypes.Count != 0)
|
||||
{
|
||||
throw new BadRequestException($"Turn on the {missingRequiredPolicyTypes.First().GetName()} policy because it is required for the {validator.Type.GetName()} policy.");
|
||||
}
|
||||
}
|
||||
|
||||
// If disabling this policy - ensure it's not required by any other policy
|
||||
if (currentPolicy is { Enabled: true } && !policyUpdate.Enabled)
|
||||
{
|
||||
var dependentPolicyTypes = _policyValidators.Values
|
||||
.Where(otherValidator => otherValidator.RequiredPolicies.Contains(policyUpdate.Type))
|
||||
.Select(otherValidator => otherValidator.Type)
|
||||
.Where(otherPolicyType => savedPoliciesDict.ContainsKey(otherPolicyType) &&
|
||||
savedPoliciesDict[otherPolicyType].Enabled)
|
||||
.ToList();
|
||||
|
||||
switch (dependentPolicyTypes)
|
||||
{
|
||||
case { Count: 1 }:
|
||||
throw new BadRequestException($"Turn off the {dependentPolicyTypes.First().GetName()} policy because it requires the {validator.Type.GetName()} policy.");
|
||||
case { Count: > 1 }:
|
||||
throw new BadRequestException($"Turn off all of the policies that require the {validator.Type.GetName()} policy.");
|
||||
}
|
||||
}
|
||||
|
||||
// Run other validation
|
||||
var validationError = await validator.ValidateAsync(policyUpdate, currentPolicy);
|
||||
if (!string.IsNullOrEmpty(validationError))
|
||||
{
|
||||
throw new BadRequestException(validationError);
|
||||
}
|
||||
|
||||
// Run side effects
|
||||
await validator.OnSaveSideEffectsAsync(policyUpdate, currentPolicy);
|
||||
}
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
#nullable enable
|
||||
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;
|
||||
|
||||
/// <summary>
|
||||
/// A request for SavePolicyCommand to update a policy
|
||||
/// </summary>
|
||||
public record PolicyUpdate
|
||||
{
|
||||
public Guid OrganizationId { get; set; }
|
||||
public PolicyType Type { get; set; }
|
||||
public string? Data { get; set; }
|
||||
public bool Enabled { get; set; }
|
||||
|
||||
public T GetDataModel<T>() where T : IPolicyDataModel, new()
|
||||
{
|
||||
return CoreHelpers.LoadClassFromJsonData<T>(Data);
|
||||
}
|
||||
|
||||
public void SetDataModel<T>(T dataModel) where T : IPolicyDataModel, new()
|
||||
{
|
||||
Data = CoreHelpers.ClassToJsonData(dataModel);
|
||||
}
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Implementations;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyValidators;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.AdminConsole.Services.Implementations;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||
|
||||
public static class PolicyServiceCollectionExtensions
|
||||
{
|
||||
public static void AddPolicyServices(this IServiceCollection services)
|
||||
{
|
||||
services.AddScoped<IPolicyService, PolicyService>();
|
||||
services.AddScoped<ISavePolicyCommand, SavePolicyCommand>();
|
||||
|
||||
services.AddScoped<IPolicyValidator, TwoFactorAuthenticationPolicyValidator>();
|
||||
services.AddScoped<IPolicyValidator, SingleOrgPolicyValidator>();
|
||||
services.AddScoped<IPolicyValidator, RequireSsoPolicyValidator>();
|
||||
services.AddScoped<IPolicyValidator, ResetPasswordPolicyValidator>();
|
||||
services.AddScoped<IPolicyValidator, MaximumVaultTimeoutPolicyValidator>();
|
||||
}
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
#nullable enable
|
||||
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;
|
||||
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyValidators;
|
||||
|
||||
public class MaximumVaultTimeoutPolicyValidator : IPolicyValidator
|
||||
{
|
||||
public PolicyType Type => PolicyType.MaximumVaultTimeout;
|
||||
public IEnumerable<PolicyType> RequiredPolicies => [PolicyType.SingleOrg];
|
||||
public Task<string> ValidateAsync(PolicyUpdate policyUpdate, Policy? currentPolicy) => Task.FromResult("");
|
||||
public Task OnSaveSideEffectsAsync(PolicyUpdate policyUpdate, Policy? currentPolicy) => Task.FromResult(0);
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
#nullable enable
|
||||
|
||||
using Bit.Core.Auth.Entities;
|
||||
using Bit.Core.Auth.Enums;
|
||||
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyValidators;
|
||||
|
||||
public static class PolicyValidatorHelpers
|
||||
{
|
||||
/// <summary>
|
||||
/// Validate that given Member Decryption Options are not enabled.
|
||||
/// Used for validation when disabling a policy that is required by certain Member Decryption Options.
|
||||
/// </summary>
|
||||
/// <param name="decryptionOptions">The Member Decryption Options that require the policy to be enabled.</param>
|
||||
/// <returns>A validation error if validation was unsuccessful, otherwise an empty string</returns>
|
||||
public static string ValidateDecryptionOptionsNotEnabled(this SsoConfig? ssoConfig,
|
||||
MemberDecryptionType[] decryptionOptions)
|
||||
{
|
||||
if (ssoConfig is not { Enabled: true })
|
||||
{
|
||||
return "";
|
||||
}
|
||||
|
||||
return ssoConfig.GetData().MemberDecryptionType switch
|
||||
{
|
||||
MemberDecryptionType.KeyConnector when decryptionOptions.Contains(MemberDecryptionType.KeyConnector)
|
||||
=> "Key Connector is enabled and requires this policy.",
|
||||
MemberDecryptionType.TrustedDeviceEncryption when decryptionOptions.Contains(MemberDecryptionType
|
||||
.TrustedDeviceEncryption) => "Trusted device encryption is on and requires this policy.",
|
||||
_ => ""
|
||||
};
|
||||
}
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
#nullable enable
|
||||
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;
|
||||
using Bit.Core.Auth.Enums;
|
||||
using Bit.Core.Auth.Repositories;
|
||||
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyValidators;
|
||||
|
||||
public class RequireSsoPolicyValidator : IPolicyValidator
|
||||
{
|
||||
private readonly ISsoConfigRepository _ssoConfigRepository;
|
||||
|
||||
public RequireSsoPolicyValidator(ISsoConfigRepository ssoConfigRepository)
|
||||
{
|
||||
_ssoConfigRepository = ssoConfigRepository;
|
||||
}
|
||||
|
||||
public PolicyType Type => PolicyType.RequireSso;
|
||||
public IEnumerable<PolicyType> RequiredPolicies => [PolicyType.SingleOrg];
|
||||
|
||||
public async Task<string> ValidateAsync(PolicyUpdate policyUpdate, Policy? currentPolicy)
|
||||
{
|
||||
if (policyUpdate is not { Enabled: true })
|
||||
{
|
||||
var ssoConfig = await _ssoConfigRepository.GetByOrganizationIdAsync(policyUpdate.OrganizationId);
|
||||
return ssoConfig.ValidateDecryptionOptionsNotEnabled([
|
||||
MemberDecryptionType.KeyConnector,
|
||||
MemberDecryptionType.TrustedDeviceEncryption
|
||||
]);
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
public Task OnSaveSideEffectsAsync(PolicyUpdate policyUpdate, Policy? currentPolicy) => Task.FromResult(0);
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
#nullable enable
|
||||
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;
|
||||
using Bit.Core.Auth.Enums;
|
||||
using Bit.Core.Auth.Repositories;
|
||||
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyValidators;
|
||||
|
||||
public class ResetPasswordPolicyValidator : IPolicyValidator
|
||||
{
|
||||
private readonly ISsoConfigRepository _ssoConfigRepository;
|
||||
public PolicyType Type => PolicyType.ResetPassword;
|
||||
public IEnumerable<PolicyType> RequiredPolicies => [PolicyType.SingleOrg];
|
||||
|
||||
public ResetPasswordPolicyValidator(ISsoConfigRepository ssoConfigRepository)
|
||||
{
|
||||
_ssoConfigRepository = ssoConfigRepository;
|
||||
}
|
||||
|
||||
public async Task<string> ValidateAsync(PolicyUpdate policyUpdate, Policy? currentPolicy)
|
||||
{
|
||||
if (policyUpdate is not { Enabled: true } ||
|
||||
policyUpdate.GetDataModel<ResetPasswordDataModel>().AutoEnrollEnabled == false)
|
||||
{
|
||||
var ssoConfig = await _ssoConfigRepository.GetByOrganizationIdAsync(policyUpdate.OrganizationId);
|
||||
return ssoConfig.ValidateDecryptionOptionsNotEnabled([MemberDecryptionType.TrustedDeviceEncryption]);
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
public Task OnSaveSideEffectsAsync(PolicyUpdate policyUpdate, Policy? currentPolicy) => Task.FromResult(0);
|
||||
}
|
@ -0,0 +1,101 @@
|
||||
#nullable enable
|
||||
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;
|
||||
using Bit.Core.Auth.Enums;
|
||||
using Bit.Core.Auth.Repositories;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyValidators;
|
||||
|
||||
public class SingleOrgPolicyValidator : IPolicyValidator
|
||||
{
|
||||
public PolicyType Type => PolicyType.SingleOrg;
|
||||
|
||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||
private readonly IMailService _mailService;
|
||||
private readonly IOrganizationRepository _organizationRepository;
|
||||
private readonly ISsoConfigRepository _ssoConfigRepository;
|
||||
private readonly ICurrentContext _currentContext;
|
||||
private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand;
|
||||
|
||||
public SingleOrgPolicyValidator(
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
IMailService mailService,
|
||||
IOrganizationRepository organizationRepository,
|
||||
ISsoConfigRepository ssoConfigRepository,
|
||||
ICurrentContext currentContext,
|
||||
IRemoveOrganizationUserCommand removeOrganizationUserCommand)
|
||||
{
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
_mailService = mailService;
|
||||
_organizationRepository = organizationRepository;
|
||||
_ssoConfigRepository = ssoConfigRepository;
|
||||
_currentContext = currentContext;
|
||||
_removeOrganizationUserCommand = removeOrganizationUserCommand;
|
||||
}
|
||||
|
||||
public IEnumerable<PolicyType> RequiredPolicies => [];
|
||||
|
||||
public async Task OnSaveSideEffectsAsync(PolicyUpdate policyUpdate, Policy? currentPolicy)
|
||||
{
|
||||
if (currentPolicy is not { Enabled: true } && policyUpdate is { Enabled: true })
|
||||
{
|
||||
await RemoveNonCompliantUsersAsync(policyUpdate.OrganizationId);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RemoveNonCompliantUsersAsync(Guid organizationId)
|
||||
{
|
||||
// Remove non-compliant users
|
||||
var savingUserId = _currentContext.UserId;
|
||||
// Note: must get OrganizationUserUserDetails so that Email is always populated from the User object
|
||||
var orgUsers = await _organizationUserRepository.GetManyDetailsByOrganizationAsync(organizationId);
|
||||
var org = await _organizationRepository.GetByIdAsync(organizationId);
|
||||
if (org == null)
|
||||
{
|
||||
throw new NotFoundException("Organization not found.");
|
||||
}
|
||||
|
||||
var removableOrgUsers = orgUsers.Where(ou =>
|
||||
ou.Status != OrganizationUserStatusType.Invited &&
|
||||
ou.Status != OrganizationUserStatusType.Revoked &&
|
||||
ou.Type != OrganizationUserType.Owner &&
|
||||
ou.Type != OrganizationUserType.Admin &&
|
||||
ou.UserId != savingUserId
|
||||
).ToList();
|
||||
|
||||
var userOrgs = await _organizationUserRepository.GetManyByManyUsersAsync(
|
||||
removableOrgUsers.Select(ou => ou.UserId!.Value));
|
||||
foreach (var orgUser in removableOrgUsers)
|
||||
{
|
||||
if (userOrgs.Any(ou => ou.UserId == orgUser.UserId
|
||||
&& ou.OrganizationId != org.Id
|
||||
&& ou.Status != OrganizationUserStatusType.Invited))
|
||||
{
|
||||
await _removeOrganizationUserCommand.RemoveUserAsync(organizationId, orgUser.Id,
|
||||
savingUserId);
|
||||
|
||||
await _mailService.SendOrganizationUserRemovedForPolicySingleOrgEmailAsync(
|
||||
org.DisplayName(), orgUser.Email);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<string> ValidateAsync(PolicyUpdate policyUpdate, Policy? currentPolicy)
|
||||
{
|
||||
if (policyUpdate is not { Enabled: true })
|
||||
{
|
||||
var ssoConfig = await _ssoConfigRepository.GetByOrganizationIdAsync(policyUpdate.OrganizationId);
|
||||
return ssoConfig.ValidateDecryptionOptionsNotEnabled([MemberDecryptionType.KeyConnector]);
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
}
|
@ -0,0 +1,87 @@
|
||||
#nullable enable
|
||||
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;
|
||||
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyValidators;
|
||||
|
||||
public class TwoFactorAuthenticationPolicyValidator : IPolicyValidator
|
||||
{
|
||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||
private readonly IMailService _mailService;
|
||||
private readonly IOrganizationRepository _organizationRepository;
|
||||
private readonly ICurrentContext _currentContext;
|
||||
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
|
||||
private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand;
|
||||
|
||||
public PolicyType Type => PolicyType.TwoFactorAuthentication;
|
||||
public IEnumerable<PolicyType> RequiredPolicies => [];
|
||||
|
||||
public TwoFactorAuthenticationPolicyValidator(
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
IMailService mailService,
|
||||
IOrganizationRepository organizationRepository,
|
||||
ICurrentContext currentContext,
|
||||
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
|
||||
IRemoveOrganizationUserCommand removeOrganizationUserCommand)
|
||||
{
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
_mailService = mailService;
|
||||
_organizationRepository = organizationRepository;
|
||||
_currentContext = currentContext;
|
||||
_twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
|
||||
_removeOrganizationUserCommand = removeOrganizationUserCommand;
|
||||
}
|
||||
|
||||
public async Task OnSaveSideEffectsAsync(PolicyUpdate policyUpdate, Policy? currentPolicy)
|
||||
{
|
||||
if (currentPolicy is not { Enabled: true } && policyUpdate is { Enabled: true })
|
||||
{
|
||||
await RemoveNonCompliantUsersAsync(policyUpdate.OrganizationId);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RemoveNonCompliantUsersAsync(Guid organizationId)
|
||||
{
|
||||
var org = await _organizationRepository.GetByIdAsync(organizationId);
|
||||
var savingUserId = _currentContext.UserId;
|
||||
|
||||
var orgUsers = await _organizationUserRepository.GetManyDetailsByOrganizationAsync(organizationId);
|
||||
var organizationUsersTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(orgUsers);
|
||||
var removableOrgUsers = orgUsers.Where(ou =>
|
||||
ou.Status != OrganizationUserStatusType.Invited && ou.Status != OrganizationUserStatusType.Revoked &&
|
||||
ou.Type != OrganizationUserType.Owner && ou.Type != OrganizationUserType.Admin &&
|
||||
ou.UserId != savingUserId);
|
||||
|
||||
// Reorder by HasMasterPassword to prioritize checking users without a master if they have 2FA enabled
|
||||
foreach (var orgUser in removableOrgUsers.OrderBy(ou => ou.HasMasterPassword))
|
||||
{
|
||||
var userTwoFactorEnabled = organizationUsersTwoFactorEnabled.FirstOrDefault(u => u.user.Id == orgUser.Id)
|
||||
.twoFactorIsEnabled;
|
||||
if (!userTwoFactorEnabled)
|
||||
{
|
||||
if (!orgUser.HasMasterPassword)
|
||||
{
|
||||
throw new BadRequestException(
|
||||
"Policy could not be enabled. Non-compliant members will lose access to their accounts. Identify members without two-step login from the policies column in the members page.");
|
||||
}
|
||||
|
||||
await _removeOrganizationUserCommand.RemoveUserAsync(organizationId, orgUser.Id,
|
||||
savingUserId);
|
||||
|
||||
await _mailService.SendOrganizationUserRemovedForPolicyTwoStepEmailAsync(
|
||||
org!.DisplayName(), orgUser.Email);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Task<string> ValidateAsync(PolicyUpdate policyUpdate, Policy? currentPolicy) => Task.FromResult("");
|
||||
}
|
Reference in New Issue
Block a user