mirror of
https://github.com/bitwarden/server.git
synced 2024-11-21 12:05:42 +01:00
[PM-13322] [BEEEP] Add PolicyValidators and refactor policy save logic (#4877)
This commit is contained in:
parent
75cc907785
commit
dfa411131d
@ -16,3 +16,30 @@ public enum PolicyType : byte
|
||||
ActivateAutofill = 11,
|
||||
AutomaticAppLogIn = 12,
|
||||
}
|
||||
|
||||
public static class PolicyTypeExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns the name of the policy for display to the user.
|
||||
/// Do not include the word "policy" in the return value.
|
||||
/// </summary>
|
||||
public static string GetName(this PolicyType type)
|
||||
{
|
||||
return type switch
|
||||
{
|
||||
PolicyType.TwoFactorAuthentication => "Require two-step login",
|
||||
PolicyType.MasterPassword => "Master password requirements",
|
||||
PolicyType.PasswordGenerator => "Password generator",
|
||||
PolicyType.SingleOrg => "Single organization",
|
||||
PolicyType.RequireSso => "Require single sign-on authentication",
|
||||
PolicyType.PersonalOwnership => "Remove individual vault",
|
||||
PolicyType.DisableSend => "Remove Send",
|
||||
PolicyType.SendOptions => "Send options",
|
||||
PolicyType.ResetPassword => "Account recovery administration",
|
||||
PolicyType.MaximumVaultTimeout => "Vault timeout",
|
||||
PolicyType.DisablePersonalVaultExport => "Remove individual vault export",
|
||||
PolicyType.ActivateAutofill => "Active auto-fill",
|
||||
PolicyType.AutomaticAppLogIn => "Automatically log in users for allowed applications",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -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("");
|
||||
}
|
@ -2,6 +2,8 @@
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Auth.Enums;
|
||||
using Bit.Core.Auth.Repositories;
|
||||
@ -27,6 +29,8 @@ public class PolicyService : IPolicyService
|
||||
private readonly IMailService _mailService;
|
||||
private readonly GlobalSettings _globalSettings;
|
||||
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
|
||||
private readonly IFeatureService _featureService;
|
||||
private readonly ISavePolicyCommand _savePolicyCommand;
|
||||
private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand;
|
||||
|
||||
public PolicyService(
|
||||
@ -39,6 +43,8 @@ public class PolicyService : IPolicyService
|
||||
IMailService mailService,
|
||||
GlobalSettings globalSettings,
|
||||
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
|
||||
IFeatureService featureService,
|
||||
ISavePolicyCommand savePolicyCommand,
|
||||
IRemoveOrganizationUserCommand removeOrganizationUserCommand)
|
||||
{
|
||||
_applicationCacheService = applicationCacheService;
|
||||
@ -50,11 +56,28 @@ public class PolicyService : IPolicyService
|
||||
_mailService = mailService;
|
||||
_globalSettings = globalSettings;
|
||||
_twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
|
||||
_featureService = featureService;
|
||||
_savePolicyCommand = savePolicyCommand;
|
||||
_removeOrganizationUserCommand = removeOrganizationUserCommand;
|
||||
}
|
||||
|
||||
public async Task SaveAsync(Policy policy, IOrganizationService organizationService, Guid? savingUserId)
|
||||
{
|
||||
if (_featureService.IsEnabled(FeatureFlagKeys.Pm13322AddPolicyDefinitions))
|
||||
{
|
||||
// Transitional mapping - this will be moved to callers once the feature flag is removed
|
||||
var policyUpdate = new PolicyUpdate
|
||||
{
|
||||
OrganizationId = policy.OrganizationId,
|
||||
Type = policy.Type,
|
||||
Enabled = policy.Enabled,
|
||||
Data = policy.Data
|
||||
};
|
||||
|
||||
await _savePolicyCommand.SaveAsync(policyUpdate);
|
||||
return;
|
||||
}
|
||||
|
||||
var org = await _organizationRepository.GetByIdAsync(policy.OrganizationId);
|
||||
if (org == null)
|
||||
{
|
||||
|
@ -146,6 +146,7 @@ public static class FeatureFlagKeys
|
||||
public const string RemoveServerVersionHeader = "remove-server-version-header";
|
||||
public const string AccessIntelligence = "pm-13227-access-intelligence";
|
||||
public const string VerifiedSsoDomainEndpoint = "pm-12337-refactor-sso-details-endpoint";
|
||||
public const string Pm13322AddPolicyDefinitions = "pm-13322-add-policy-definitions";
|
||||
public const string LimitCollectionCreationDeletionSplit = "pm-10863-limit-collection-creation-deletion-split";
|
||||
|
||||
public static List<string> GetAllKeys()
|
||||
|
@ -4,6 +4,7 @@ using System.Security.Claims;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using AspNetCoreRateLimit;
|
||||
using Bit.Core.AdminConsole.Models.Business.Tokenables;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.AdminConsole.Services.Implementations;
|
||||
using Bit.Core.AdminConsole.Services.NoopImplementations;
|
||||
@ -102,9 +103,9 @@ public static class ServiceCollectionExtensions
|
||||
services.AddUserServices(globalSettings);
|
||||
services.AddTrialInitiationServices();
|
||||
services.AddOrganizationServices(globalSettings);
|
||||
services.AddPolicyServices();
|
||||
services.AddScoped<ICollectionService, CollectionService>();
|
||||
services.AddScoped<IGroupService, GroupService>();
|
||||
services.AddScoped<IPolicyService, PolicyService>();
|
||||
services.AddScoped<IEventService, EventService>();
|
||||
services.AddScoped<IEmergencyAccessService, EmergencyAccessService>();
|
||||
services.AddSingleton<IDeviceService, DeviceService>();
|
||||
|
@ -127,7 +127,6 @@ public class SutProvider<TSut> : ISutProvider
|
||||
return _sutProvider.GetDependency(parameterInfo.ParameterType, "");
|
||||
}
|
||||
|
||||
|
||||
// This is the equivalent of _fixture.Create<parameterInfo.ParameterType>, but no overload for
|
||||
// Create(Type type) exists.
|
||||
var dependency = new SpecimenContext(_fixture).Resolve(new SeededRequest(parameterInfo.ParameterType,
|
||||
|
@ -1,6 +1,7 @@
|
||||
using AutoFixture;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
using Microsoft.Extensions.Time.Testing;
|
||||
using NSubstitute;
|
||||
using RichardSzalay.MockHttp;
|
||||
|
||||
@ -47,4 +48,19 @@ public static class SutProviderExtensions
|
||||
.SetDependency(mockHttpClientFactory)
|
||||
.Create();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configures SutProvider to use FakeTimeProvider.
|
||||
/// It is registered under both the TimeProvider type and the FakeTimeProvider type
|
||||
/// so that it can be retrieved in a type-safe manner with GetDependency.
|
||||
/// This can be chained with other builder methods; make sure to call
|
||||
/// <see cref="ISutProvider.Create"/> before use.
|
||||
/// </summary>
|
||||
public static SutProvider<T> WithFakeTimeProvider<T>(this SutProvider<T> sutProvider)
|
||||
{
|
||||
var fakeTimeProvider = new FakeTimeProvider();
|
||||
return sutProvider
|
||||
.SetDependency((TimeProvider)fakeTimeProvider)
|
||||
.SetDependency(fakeTimeProvider);
|
||||
}
|
||||
}
|
||||
|
@ -5,6 +5,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.TimeProvider.Testing" Version="8.10.0" />
|
||||
<PackageReference Include="NSubstitute" Version="$(NSubstituteVersion)" />
|
||||
<PackageReference Include="xunit" Version="$(XUnitVersion)" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="$(XUnitRunnerVisualStudioVersion)">
|
||||
|
@ -9,10 +9,12 @@ namespace Bit.Core.Test.AdminConsole.AutoFixture;
|
||||
internal class PolicyCustomization : ICustomization
|
||||
{
|
||||
public PolicyType Type { get; set; }
|
||||
public bool Enabled { get; set; }
|
||||
|
||||
public PolicyCustomization(PolicyType type)
|
||||
public PolicyCustomization(PolicyType type, bool enabled)
|
||||
{
|
||||
Type = type;
|
||||
Enabled = enabled;
|
||||
}
|
||||
|
||||
public void Customize(IFixture fixture)
|
||||
@ -20,21 +22,23 @@ internal class PolicyCustomization : ICustomization
|
||||
fixture.Customize<Policy>(composer => composer
|
||||
.With(o => o.OrganizationId, Guid.NewGuid())
|
||||
.With(o => o.Type, Type)
|
||||
.With(o => o.Enabled, true));
|
||||
.With(o => o.Enabled, Enabled));
|
||||
}
|
||||
}
|
||||
|
||||
public class PolicyAttribute : CustomizeAttribute
|
||||
{
|
||||
private readonly PolicyType _type;
|
||||
private readonly bool _enabled;
|
||||
|
||||
public PolicyAttribute(PolicyType type)
|
||||
public PolicyAttribute(PolicyType type, bool enabled = true)
|
||||
{
|
||||
_type = type;
|
||||
_enabled = enabled;
|
||||
}
|
||||
|
||||
public override ICustomization GetCustomization(ParameterInfo parameter)
|
||||
{
|
||||
return new PolicyCustomization(_type);
|
||||
return new PolicyCustomization(_type, _enabled);
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,25 @@
|
||||
using System.Reflection;
|
||||
using AutoFixture;
|
||||
using AutoFixture.Xunit2;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;
|
||||
|
||||
namespace Bit.Core.Test.AdminConsole.AutoFixture;
|
||||
|
||||
internal class PolicyUpdateCustomization(PolicyType type, bool enabled) : ICustomization
|
||||
{
|
||||
public void Customize(IFixture fixture)
|
||||
{
|
||||
fixture.Customize<PolicyUpdate>(composer => composer
|
||||
.With(o => o.Type, type)
|
||||
.With(o => o.Enabled, enabled));
|
||||
}
|
||||
}
|
||||
|
||||
public class PolicyUpdateAttribute(PolicyType type, bool enabled = true) : CustomizeAttribute
|
||||
{
|
||||
public override ICustomization GetCustomization(ParameterInfo parameter)
|
||||
{
|
||||
return new PolicyUpdateCustomization(type, enabled);
|
||||
}
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
#nullable enable
|
||||
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;
|
||||
using NSubstitute;
|
||||
|
||||
namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies;
|
||||
|
||||
public class FakeSingleOrgPolicyValidator : IPolicyValidator
|
||||
{
|
||||
public PolicyType Type => PolicyType.SingleOrg;
|
||||
public IEnumerable<PolicyType> RequiredPolicies => Array.Empty<PolicyType>();
|
||||
|
||||
public readonly Func<PolicyUpdate, Policy?, Task<string>> ValidateAsyncMock = Substitute.For<Func<PolicyUpdate, Policy?, Task<string>>>();
|
||||
public readonly Action<PolicyUpdate, Policy?> OnSaveSideEffectsAsyncMock = Substitute.For<Action<PolicyUpdate, Policy?>>();
|
||||
|
||||
public Task<string> ValidateAsync(PolicyUpdate policyUpdate, Policy? currentPolicy)
|
||||
{
|
||||
return ValidateAsyncMock(policyUpdate, currentPolicy);
|
||||
}
|
||||
|
||||
public Task OnSaveSideEffectsAsync(PolicyUpdate policyUpdate, Policy? currentPolicy)
|
||||
{
|
||||
OnSaveSideEffectsAsyncMock(policyUpdate, currentPolicy);
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
}
|
||||
public class FakeRequireSsoPolicyValidator : IPolicyValidator
|
||||
{
|
||||
public PolicyType Type => PolicyType.RequireSso;
|
||||
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);
|
||||
}
|
||||
public class FakeVaultTimeoutPolicyValidator : 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,64 @@
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyValidators;
|
||||
using Bit.Core.Auth.Entities;
|
||||
using Bit.Core.Auth.Enums;
|
||||
using Bit.Core.Auth.Models.Data;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies;
|
||||
|
||||
public class PolicyValidatorHelpersTests
|
||||
{
|
||||
[Fact]
|
||||
public void ValidateDecryptionOptionsNotEnabled_RequiredByKeyConnector_ValidationError()
|
||||
{
|
||||
var ssoConfig = new SsoConfig();
|
||||
ssoConfig.SetData(new SsoConfigurationData { MemberDecryptionType = MemberDecryptionType.KeyConnector });
|
||||
|
||||
var result = ssoConfig.ValidateDecryptionOptionsNotEnabled([MemberDecryptionType.KeyConnector]);
|
||||
|
||||
Assert.Contains("Key Connector is enabled", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ValidateDecryptionOptionsNotEnabled_RequiredByTDE_ValidationError()
|
||||
{
|
||||
var ssoConfig = new SsoConfig();
|
||||
ssoConfig.SetData(new SsoConfigurationData { MemberDecryptionType = MemberDecryptionType.TrustedDeviceEncryption });
|
||||
|
||||
var result = ssoConfig.ValidateDecryptionOptionsNotEnabled([MemberDecryptionType.TrustedDeviceEncryption]);
|
||||
|
||||
Assert.Contains("Trusted device encryption is on", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ValidateDecryptionOptionsNotEnabled_NullSsoConfig_NoValidationError()
|
||||
{
|
||||
var ssoConfig = new SsoConfig();
|
||||
var result = ssoConfig.ValidateDecryptionOptionsNotEnabled([MemberDecryptionType.KeyConnector]);
|
||||
|
||||
Assert.True(string.IsNullOrEmpty(result));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ValidateDecryptionOptionsNotEnabled_RequiredOptionNotEnabled_NoValidationError()
|
||||
{
|
||||
var ssoConfig = new SsoConfig();
|
||||
ssoConfig.SetData(new SsoConfigurationData { MemberDecryptionType = MemberDecryptionType.KeyConnector });
|
||||
|
||||
var result = ssoConfig.ValidateDecryptionOptionsNotEnabled([MemberDecryptionType.TrustedDeviceEncryption]);
|
||||
|
||||
Assert.True(string.IsNullOrEmpty(result));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ValidateDecryptionOptionsNotEnabled_SsoConfigDisabled_NoValidationError()
|
||||
{
|
||||
var ssoConfig = new SsoConfig();
|
||||
ssoConfig.Enabled = false;
|
||||
ssoConfig.SetData(new SsoConfigurationData { MemberDecryptionType = MemberDecryptionType.KeyConnector });
|
||||
|
||||
var result = ssoConfig.ValidateDecryptionOptionsNotEnabled([MemberDecryptionType.KeyConnector]);
|
||||
|
||||
Assert.True(string.IsNullOrEmpty(result));
|
||||
}
|
||||
}
|
@ -0,0 +1,75 @@
|
||||
namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies.PolicyValidators;
|
||||
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyValidators;
|
||||
using Bit.Core.Auth.Entities;
|
||||
using Bit.Core.Auth.Enums;
|
||||
using Bit.Core.Auth.Models.Data;
|
||||
using Bit.Core.Auth.Repositories;
|
||||
using Bit.Core.Test.AdminConsole.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class RequireSsoPolicyValidatorTests
|
||||
{
|
||||
[Theory, BitAutoData]
|
||||
public async Task ValidateAsync_DisablingPolicy_KeyConnectorEnabled_ValidationError(
|
||||
[PolicyUpdate(PolicyType.SingleOrg, false)] PolicyUpdate policyUpdate,
|
||||
[Policy(PolicyType.SingleOrg)] Policy policy,
|
||||
SutProvider<RequireSsoPolicyValidator> sutProvider)
|
||||
{
|
||||
policy.OrganizationId = policyUpdate.OrganizationId;
|
||||
|
||||
var ssoConfig = new SsoConfig { Enabled = true };
|
||||
ssoConfig.SetData(new SsoConfigurationData { MemberDecryptionType = MemberDecryptionType.KeyConnector });
|
||||
|
||||
sutProvider.GetDependency<ISsoConfigRepository>()
|
||||
.GetByOrganizationIdAsync(policyUpdate.OrganizationId)
|
||||
.Returns(ssoConfig);
|
||||
|
||||
var result = await sutProvider.Sut.ValidateAsync(policyUpdate, policy);
|
||||
Assert.Contains("Key Connector is enabled", result, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task ValidateAsync_DisablingPolicy_TdeEnabled_ValidationError(
|
||||
[PolicyUpdate(PolicyType.SingleOrg, false)] PolicyUpdate policyUpdate,
|
||||
[Policy(PolicyType.SingleOrg)] Policy policy,
|
||||
SutProvider<RequireSsoPolicyValidator> sutProvider)
|
||||
{
|
||||
policy.OrganizationId = policyUpdate.OrganizationId;
|
||||
|
||||
var ssoConfig = new SsoConfig { Enabled = true };
|
||||
ssoConfig.SetData(new SsoConfigurationData { MemberDecryptionType = MemberDecryptionType.TrustedDeviceEncryption });
|
||||
|
||||
sutProvider.GetDependency<ISsoConfigRepository>()
|
||||
.GetByOrganizationIdAsync(policyUpdate.OrganizationId)
|
||||
.Returns(ssoConfig);
|
||||
|
||||
var result = await sutProvider.Sut.ValidateAsync(policyUpdate, policy);
|
||||
Assert.Contains("Trusted device encryption is on", result, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task ValidateAsync_DisablingPolicy_DecryptionOptionsNotEnabled_Success(
|
||||
[PolicyUpdate(PolicyType.ResetPassword, false)] PolicyUpdate policyUpdate,
|
||||
[Policy(PolicyType.ResetPassword)] Policy policy,
|
||||
SutProvider<RequireSsoPolicyValidator> sutProvider)
|
||||
{
|
||||
policy.OrganizationId = policyUpdate.OrganizationId;
|
||||
|
||||
var ssoConfig = new SsoConfig { Enabled = false };
|
||||
|
||||
sutProvider.GetDependency<ISsoConfigRepository>()
|
||||
.GetByOrganizationIdAsync(policyUpdate.OrganizationId)
|
||||
.Returns(ssoConfig);
|
||||
|
||||
var result = await sutProvider.Sut.ValidateAsync(policyUpdate, policy);
|
||||
Assert.True(string.IsNullOrEmpty(result));
|
||||
}
|
||||
}
|
@ -0,0 +1,71 @@
|
||||
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.AdminConsole.OrganizationFeatures.Policies.PolicyValidators;
|
||||
using Bit.Core.Auth.Entities;
|
||||
using Bit.Core.Auth.Enums;
|
||||
using Bit.Core.Auth.Models.Data;
|
||||
using Bit.Core.Auth.Repositories;
|
||||
using Bit.Core.Test.AdminConsole.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies.PolicyValidators;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class ResetPasswordPolicyValidatorTests
|
||||
{
|
||||
[Theory]
|
||||
[BitAutoData(true, false)]
|
||||
[BitAutoData(false, true)]
|
||||
[BitAutoData(false, false)]
|
||||
public async Task ValidateAsync_DisablingPolicy_TdeEnabled_ValidationError(
|
||||
bool policyEnabled,
|
||||
bool autoEnrollEnabled,
|
||||
[PolicyUpdate(PolicyType.ResetPassword)] PolicyUpdate policyUpdate,
|
||||
[Policy(PolicyType.ResetPassword)] Policy policy,
|
||||
SutProvider<ResetPasswordPolicyValidator> sutProvider)
|
||||
{
|
||||
policyUpdate.Enabled = policyEnabled;
|
||||
policyUpdate.SetDataModel(new ResetPasswordDataModel
|
||||
{
|
||||
AutoEnrollEnabled = autoEnrollEnabled
|
||||
});
|
||||
policy.OrganizationId = policyUpdate.OrganizationId;
|
||||
|
||||
var ssoConfig = new SsoConfig { Enabled = true };
|
||||
ssoConfig.SetData(new SsoConfigurationData { MemberDecryptionType = MemberDecryptionType.TrustedDeviceEncryption });
|
||||
|
||||
sutProvider.GetDependency<ISsoConfigRepository>()
|
||||
.GetByOrganizationIdAsync(policyUpdate.OrganizationId)
|
||||
.Returns(ssoConfig);
|
||||
|
||||
var result = await sutProvider.Sut.ValidateAsync(policyUpdate, policy);
|
||||
Assert.Contains("Trusted device encryption is on and requires this policy.", result, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task ValidateAsync_DisablingPolicy_TdeNotEnabled_Success(
|
||||
[PolicyUpdate(PolicyType.ResetPassword, false)] PolicyUpdate policyUpdate,
|
||||
[Policy(PolicyType.ResetPassword)] Policy policy,
|
||||
SutProvider<ResetPasswordPolicyValidator> sutProvider)
|
||||
{
|
||||
policyUpdate.SetDataModel(new ResetPasswordDataModel
|
||||
{
|
||||
AutoEnrollEnabled = false
|
||||
});
|
||||
policy.OrganizationId = policyUpdate.OrganizationId;
|
||||
|
||||
var ssoConfig = new SsoConfig { Enabled = false };
|
||||
|
||||
sutProvider.GetDependency<ISsoConfigRepository>()
|
||||
.GetByOrganizationIdAsync(policyUpdate.OrganizationId)
|
||||
.Returns(ssoConfig);
|
||||
|
||||
var result = await sutProvider.Sut.ValidateAsync(policyUpdate, policy);
|
||||
Assert.True(string.IsNullOrEmpty(result));
|
||||
}
|
||||
}
|
@ -0,0 +1,129 @@
|
||||
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.AdminConsole.OrganizationFeatures.Policies.PolicyValidators;
|
||||
using Bit.Core.Auth.Entities;
|
||||
using Bit.Core.Auth.Enums;
|
||||
using Bit.Core.Auth.Models.Data;
|
||||
using Bit.Core.Auth.Repositories;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Test.AdminConsole.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies.PolicyValidators;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class SingleOrgPolicyValidatorTests
|
||||
{
|
||||
[Theory, BitAutoData]
|
||||
public async Task ValidateAsync_DisablingPolicy_KeyConnectorEnabled_ValidationError(
|
||||
[PolicyUpdate(PolicyType.SingleOrg, false)] PolicyUpdate policyUpdate,
|
||||
[Policy(PolicyType.SingleOrg)] Policy policy,
|
||||
SutProvider<SingleOrgPolicyValidator> sutProvider)
|
||||
{
|
||||
policy.OrganizationId = policyUpdate.OrganizationId;
|
||||
|
||||
var ssoConfig = new SsoConfig { Enabled = true };
|
||||
ssoConfig.SetData(new SsoConfigurationData { MemberDecryptionType = MemberDecryptionType.KeyConnector });
|
||||
|
||||
sutProvider.GetDependency<ISsoConfigRepository>()
|
||||
.GetByOrganizationIdAsync(policyUpdate.OrganizationId)
|
||||
.Returns(ssoConfig);
|
||||
|
||||
var result = await sutProvider.Sut.ValidateAsync(policyUpdate, policy);
|
||||
Assert.Contains("Key Connector is enabled", result, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task ValidateAsync_DisablingPolicy_KeyConnectorNotEnabled_Success(
|
||||
[PolicyUpdate(PolicyType.ResetPassword, false)] PolicyUpdate policyUpdate,
|
||||
[Policy(PolicyType.ResetPassword)] Policy policy,
|
||||
SutProvider<SingleOrgPolicyValidator> sutProvider)
|
||||
{
|
||||
policy.OrganizationId = policyUpdate.OrganizationId;
|
||||
|
||||
var ssoConfig = new SsoConfig { Enabled = false };
|
||||
|
||||
sutProvider.GetDependency<ISsoConfigRepository>()
|
||||
.GetByOrganizationIdAsync(policyUpdate.OrganizationId)
|
||||
.Returns(ssoConfig);
|
||||
|
||||
var result = await sutProvider.Sut.ValidateAsync(policyUpdate, policy);
|
||||
Assert.True(string.IsNullOrEmpty(result));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task OnSaveSideEffectsAsync_RemovesNonCompliantUsers(
|
||||
[PolicyUpdate(PolicyType.SingleOrg)] PolicyUpdate policyUpdate,
|
||||
[Policy(PolicyType.SingleOrg, false)] Policy policy,
|
||||
Guid savingUserId,
|
||||
Guid nonCompliantUserId,
|
||||
Organization organization, SutProvider<SingleOrgPolicyValidator> sutProvider)
|
||||
{
|
||||
policy.OrganizationId = organization.Id = policyUpdate.OrganizationId;
|
||||
|
||||
var compliantUser1 = new OrganizationUserUserDetails
|
||||
{
|
||||
OrganizationId = organization.Id,
|
||||
Type = OrganizationUserType.User,
|
||||
Status = OrganizationUserStatusType.Confirmed,
|
||||
UserId = new Guid(),
|
||||
Email = "user1@example.com"
|
||||
};
|
||||
|
||||
var compliantUser2 = new OrganizationUserUserDetails
|
||||
{
|
||||
OrganizationId = organization.Id,
|
||||
Type = OrganizationUserType.User,
|
||||
Status = OrganizationUserStatusType.Confirmed,
|
||||
UserId = new Guid(),
|
||||
Email = "user2@example.com"
|
||||
};
|
||||
|
||||
var nonCompliantUser = new OrganizationUserUserDetails
|
||||
{
|
||||
OrganizationId = organization.Id,
|
||||
Type = OrganizationUserType.User,
|
||||
Status = OrganizationUserStatusType.Confirmed,
|
||||
UserId = nonCompliantUserId,
|
||||
Email = "user3@example.com"
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetManyDetailsByOrganizationAsync(policyUpdate.OrganizationId)
|
||||
.Returns([compliantUser1, compliantUser2, nonCompliantUser]);
|
||||
|
||||
var otherOrganizationUser = new OrganizationUser
|
||||
{
|
||||
OrganizationId = new Guid(),
|
||||
UserId = nonCompliantUserId,
|
||||
Status = OrganizationUserStatusType.Confirmed
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetManyByManyUsersAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(nonCompliantUserId)))
|
||||
.Returns([otherOrganizationUser]);
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(savingUserId);
|
||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(policyUpdate.OrganizationId).Returns(organization);
|
||||
|
||||
await sutProvider.Sut.OnSaveSideEffectsAsync(policyUpdate, policy);
|
||||
|
||||
await sutProvider.GetDependency<IRemoveOrganizationUserCommand>()
|
||||
.Received(1)
|
||||
.RemoveUserAsync(policyUpdate.OrganizationId, nonCompliantUser.Id, savingUserId);
|
||||
await sutProvider.GetDependency<IMailService>()
|
||||
.Received(1)
|
||||
.SendOrganizationUserRemovedForPolicySingleOrgEmailAsync(organization.DisplayName(),
|
||||
"user3@example.com");
|
||||
}
|
||||
}
|
@ -0,0 +1,209 @@
|
||||
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.AdminConsole.OrganizationFeatures.Policies.PolicyValidators;
|
||||
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Test.AdminConsole.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies.PolicyValidators;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class TwoFactorAuthenticationPolicyValidatorTests
|
||||
{
|
||||
[Theory, BitAutoData]
|
||||
public async Task OnSaveSideEffectsAsync_RemovesNonCompliantUsers(
|
||||
Organization organization,
|
||||
[PolicyUpdate(PolicyType.TwoFactorAuthentication)] PolicyUpdate policyUpdate,
|
||||
[Policy(PolicyType.TwoFactorAuthentication, false)] Policy policy,
|
||||
SutProvider<TwoFactorAuthenticationPolicyValidator> sutProvider)
|
||||
{
|
||||
policy.OrganizationId = organization.Id = policyUpdate.OrganizationId;
|
||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
|
||||
|
||||
var orgUserDetailUserInvited = new OrganizationUserUserDetails
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Status = OrganizationUserStatusType.Invited,
|
||||
Type = OrganizationUserType.User,
|
||||
// Needs to be different from what is passed in as the savingUserId to Sut.SaveAsync
|
||||
Email = "user1@test.com",
|
||||
Name = "TEST",
|
||||
UserId = Guid.NewGuid(),
|
||||
HasMasterPassword = false
|
||||
};
|
||||
var orgUserDetailUserAcceptedWith2FA = new OrganizationUserUserDetails
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Status = OrganizationUserStatusType.Accepted,
|
||||
Type = OrganizationUserType.User,
|
||||
// Needs to be different from what is passed in as the savingUserId to Sut.SaveAsync
|
||||
Email = "user2@test.com",
|
||||
Name = "TEST",
|
||||
UserId = Guid.NewGuid(),
|
||||
HasMasterPassword = true
|
||||
};
|
||||
var orgUserDetailUserAcceptedWithout2FA = new OrganizationUserUserDetails
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Status = OrganizationUserStatusType.Accepted,
|
||||
Type = OrganizationUserType.User,
|
||||
// Needs to be different from what is passed in as the savingUserId to Sut.SaveAsync
|
||||
Email = "user3@test.com",
|
||||
Name = "TEST",
|
||||
UserId = Guid.NewGuid(),
|
||||
HasMasterPassword = true
|
||||
};
|
||||
var orgUserDetailAdmin = new OrganizationUserUserDetails
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Status = OrganizationUserStatusType.Confirmed,
|
||||
Type = OrganizationUserType.Admin,
|
||||
// Needs to be different from what is passed in as the savingUserId to Sut.SaveAsync
|
||||
Email = "admin@test.com",
|
||||
Name = "ADMIN",
|
||||
UserId = Guid.NewGuid(),
|
||||
HasMasterPassword = false
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetManyDetailsByOrganizationAsync(policyUpdate.OrganizationId)
|
||||
.Returns(new List<OrganizationUserUserDetails>
|
||||
{
|
||||
orgUserDetailUserInvited,
|
||||
orgUserDetailUserAcceptedWith2FA,
|
||||
orgUserDetailUserAcceptedWithout2FA,
|
||||
orgUserDetailAdmin
|
||||
});
|
||||
|
||||
sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()
|
||||
.TwoFactorIsEnabledAsync(Arg.Any<IEnumerable<OrganizationUserUserDetails>>())
|
||||
.Returns(new List<(OrganizationUserUserDetails user, bool hasTwoFactor)>()
|
||||
{
|
||||
(orgUserDetailUserInvited, false),
|
||||
(orgUserDetailUserAcceptedWith2FA, true),
|
||||
(orgUserDetailUserAcceptedWithout2FA, false),
|
||||
(orgUserDetailAdmin, false),
|
||||
});
|
||||
|
||||
var savingUserId = Guid.NewGuid();
|
||||
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(savingUserId);
|
||||
|
||||
await sutProvider.Sut.OnSaveSideEffectsAsync(policyUpdate, policy);
|
||||
|
||||
var removeOrganizationUserCommand = sutProvider.GetDependency<IRemoveOrganizationUserCommand>();
|
||||
|
||||
await removeOrganizationUserCommand.Received()
|
||||
.RemoveUserAsync(policy.OrganizationId, orgUserDetailUserAcceptedWithout2FA.Id, savingUserId);
|
||||
await sutProvider.GetDependency<IMailService>().Received()
|
||||
.SendOrganizationUserRemovedForPolicyTwoStepEmailAsync(organization.DisplayName(), orgUserDetailUserAcceptedWithout2FA.Email);
|
||||
|
||||
await removeOrganizationUserCommand.DidNotReceive()
|
||||
.RemoveUserAsync(policy.OrganizationId, orgUserDetailUserInvited.Id, savingUserId);
|
||||
await sutProvider.GetDependency<IMailService>().DidNotReceive()
|
||||
.SendOrganizationUserRemovedForPolicyTwoStepEmailAsync(organization.DisplayName(), orgUserDetailUserInvited.Email);
|
||||
await removeOrganizationUserCommand.DidNotReceive()
|
||||
.RemoveUserAsync(policy.OrganizationId, orgUserDetailUserAcceptedWith2FA.Id, savingUserId);
|
||||
await sutProvider.GetDependency<IMailService>().DidNotReceive()
|
||||
.SendOrganizationUserRemovedForPolicyTwoStepEmailAsync(organization.DisplayName(), orgUserDetailUserAcceptedWith2FA.Email);
|
||||
await removeOrganizationUserCommand.DidNotReceive()
|
||||
.RemoveUserAsync(policy.OrganizationId, orgUserDetailAdmin.Id, savingUserId);
|
||||
await sutProvider.GetDependency<IMailService>().DidNotReceive()
|
||||
.SendOrganizationUserRemovedForPolicyTwoStepEmailAsync(organization.DisplayName(), orgUserDetailAdmin.Email);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task OnSaveSideEffectsAsync_UsersToBeRemovedDontHaveMasterPasswords_Throws(
|
||||
Organization organization,
|
||||
[PolicyUpdate(PolicyType.TwoFactorAuthentication)] PolicyUpdate policyUpdate,
|
||||
[Policy(PolicyType.TwoFactorAuthentication, false)] Policy policy,
|
||||
SutProvider<TwoFactorAuthenticationPolicyValidator> sutProvider)
|
||||
{
|
||||
policy.OrganizationId = organization.Id = policyUpdate.OrganizationId;
|
||||
|
||||
var orgUserDetailUserWith2FAAndMP = new OrganizationUserUserDetails
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Status = OrganizationUserStatusType.Confirmed,
|
||||
Type = OrganizationUserType.User,
|
||||
// Needs to be different from what is passed in as the savingUserId to Sut.SaveAsync
|
||||
Email = "user1@test.com",
|
||||
Name = "TEST",
|
||||
UserId = Guid.NewGuid(),
|
||||
HasMasterPassword = true
|
||||
};
|
||||
var orgUserDetailUserWith2FANoMP = new OrganizationUserUserDetails
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Status = OrganizationUserStatusType.Confirmed,
|
||||
Type = OrganizationUserType.User,
|
||||
// Needs to be different from what is passed in as the savingUserId to Sut.SaveAsync
|
||||
Email = "user2@test.com",
|
||||
Name = "TEST",
|
||||
UserId = Guid.NewGuid(),
|
||||
HasMasterPassword = false
|
||||
};
|
||||
var orgUserDetailUserWithout2FA = new OrganizationUserUserDetails
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Status = OrganizationUserStatusType.Confirmed,
|
||||
Type = OrganizationUserType.User,
|
||||
// Needs to be different from what is passed in as the savingUserId to Sut.SaveAsync
|
||||
Email = "user3@test.com",
|
||||
Name = "TEST",
|
||||
UserId = Guid.NewGuid(),
|
||||
HasMasterPassword = false
|
||||
};
|
||||
var orgUserDetailAdmin = new OrganizationUserUserDetails
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Status = OrganizationUserStatusType.Confirmed,
|
||||
Type = OrganizationUserType.Admin,
|
||||
// Needs to be different from what is passed in as the savingUserId to Sut.SaveAsync
|
||||
Email = "admin@test.com",
|
||||
Name = "ADMIN",
|
||||
UserId = Guid.NewGuid(),
|
||||
HasMasterPassword = false
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetManyDetailsByOrganizationAsync(policy.OrganizationId)
|
||||
.Returns(new List<OrganizationUserUserDetails>
|
||||
{
|
||||
orgUserDetailUserWith2FAAndMP,
|
||||
orgUserDetailUserWith2FANoMP,
|
||||
orgUserDetailUserWithout2FA,
|
||||
orgUserDetailAdmin
|
||||
});
|
||||
|
||||
sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()
|
||||
.TwoFactorIsEnabledAsync(Arg.Is<IEnumerable<Guid>>(ids =>
|
||||
ids.Contains(orgUserDetailUserWith2FANoMP.UserId.Value)
|
||||
&& ids.Contains(orgUserDetailUserWithout2FA.UserId.Value)
|
||||
&& ids.Contains(orgUserDetailAdmin.UserId.Value)))
|
||||
.Returns(new List<(Guid userId, bool hasTwoFactor)>()
|
||||
{
|
||||
(orgUserDetailUserWith2FANoMP.UserId.Value, true),
|
||||
(orgUserDetailUserWithout2FA.UserId.Value, false),
|
||||
(orgUserDetailAdmin.UserId.Value, false),
|
||||
});
|
||||
|
||||
var badRequestException = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.OnSaveSideEffectsAsync(policyUpdate, policy));
|
||||
|
||||
Assert.Contains("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.", badRequestException.Message, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
await sutProvider.GetDependency<IRemoveOrganizationUserCommand>().DidNotReceiveWithAnyArgs()
|
||||
.RemoveUserAsync(organizationId: default, organizationUserId: default, deletingUserId: default);
|
||||
}
|
||||
}
|
@ -0,0 +1,330 @@
|
||||
#nullable enable
|
||||
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Implementations;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Data.Organizations;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Test.AdminConsole.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Microsoft.Extensions.Time.Testing;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
using EventType = Bit.Core.Enums.EventType;
|
||||
|
||||
namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies;
|
||||
|
||||
public class SavePolicyCommandTests
|
||||
{
|
||||
[Theory, BitAutoData]
|
||||
public async Task SaveAsync_NewPolicy_Success([PolicyUpdate(PolicyType.SingleOrg)] PolicyUpdate policyUpdate)
|
||||
{
|
||||
var fakePolicyValidator = new FakeSingleOrgPolicyValidator();
|
||||
fakePolicyValidator.ValidateAsyncMock(policyUpdate, null).Returns("");
|
||||
var sutProvider = SutProviderFactory([fakePolicyValidator]);
|
||||
|
||||
ArrangeOrganization(sutProvider, policyUpdate);
|
||||
sutProvider.GetDependency<IPolicyRepository>().GetManyByOrganizationIdAsync(policyUpdate.OrganizationId).Returns([]);
|
||||
|
||||
var creationDate = sutProvider.GetDependency<FakeTimeProvider>().Start;
|
||||
|
||||
await sutProvider.Sut.SaveAsync(policyUpdate);
|
||||
|
||||
await fakePolicyValidator.ValidateAsyncMock.Received(1).Invoke(policyUpdate, null);
|
||||
fakePolicyValidator.OnSaveSideEffectsAsyncMock.Received(1).Invoke(policyUpdate, null);
|
||||
|
||||
await AssertPolicySavedAsync(sutProvider, policyUpdate);
|
||||
await sutProvider.GetDependency<IPolicyRepository>().Received(1).UpsertAsync(Arg.Is<Policy>(p =>
|
||||
p.CreationDate == creationDate &&
|
||||
p.RevisionDate == creationDate));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task SaveAsync_ExistingPolicy_Success(
|
||||
[PolicyUpdate(PolicyType.SingleOrg)] PolicyUpdate policyUpdate,
|
||||
[Policy(PolicyType.SingleOrg, false)] Policy currentPolicy)
|
||||
{
|
||||
var fakePolicyValidator = new FakeSingleOrgPolicyValidator();
|
||||
fakePolicyValidator.ValidateAsyncMock(policyUpdate, null).Returns("");
|
||||
var sutProvider = SutProviderFactory([fakePolicyValidator]);
|
||||
|
||||
currentPolicy.OrganizationId = policyUpdate.OrganizationId;
|
||||
sutProvider.GetDependency<IPolicyRepository>()
|
||||
.GetByOrganizationIdTypeAsync(policyUpdate.OrganizationId, policyUpdate.Type)
|
||||
.Returns(currentPolicy);
|
||||
|
||||
ArrangeOrganization(sutProvider, policyUpdate);
|
||||
sutProvider.GetDependency<IPolicyRepository>()
|
||||
.GetManyByOrganizationIdAsync(policyUpdate.OrganizationId)
|
||||
.Returns([currentPolicy]);
|
||||
|
||||
// Store mutable properties separately to assert later
|
||||
var id = currentPolicy.Id;
|
||||
var organizationId = currentPolicy.OrganizationId;
|
||||
var type = currentPolicy.Type;
|
||||
var creationDate = currentPolicy.CreationDate;
|
||||
var revisionDate = sutProvider.GetDependency<FakeTimeProvider>().Start;
|
||||
|
||||
await sutProvider.Sut.SaveAsync(policyUpdate);
|
||||
|
||||
await fakePolicyValidator.ValidateAsyncMock.Received(1).Invoke(policyUpdate, currentPolicy);
|
||||
fakePolicyValidator.OnSaveSideEffectsAsyncMock.Received(1).Invoke(policyUpdate, currentPolicy);
|
||||
|
||||
await AssertPolicySavedAsync(sutProvider, policyUpdate);
|
||||
// Additional assertions to ensure certain properties have or have not been updated
|
||||
await sutProvider.GetDependency<IPolicyRepository>().Received(1).UpsertAsync(Arg.Is<Policy>(p =>
|
||||
p.Id == id &&
|
||||
p.OrganizationId == organizationId &&
|
||||
p.Type == type &&
|
||||
p.CreationDate == creationDate &&
|
||||
p.RevisionDate == revisionDate));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_DuplicatePolicyValidators_Throws()
|
||||
{
|
||||
var exception = Assert.Throws<Exception>(() =>
|
||||
new SavePolicyCommand(
|
||||
Substitute.For<IApplicationCacheService>(),
|
||||
Substitute.For<IEventService>(),
|
||||
Substitute.For<IPolicyRepository>(),
|
||||
[new FakeSingleOrgPolicyValidator(), new FakeSingleOrgPolicyValidator()],
|
||||
Substitute.For<TimeProvider>()
|
||||
));
|
||||
Assert.Contains("Duplicate PolicyValidator for SingleOrg policy", exception.Message);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task SaveAsync_OrganizationDoesNotExist_ThrowsBadRequest(PolicyUpdate policyUpdate)
|
||||
{
|
||||
var sutProvider = SutProviderFactory();
|
||||
sutProvider.GetDependency<IApplicationCacheService>()
|
||||
.GetOrganizationAbilityAsync(policyUpdate.OrganizationId)
|
||||
.Returns(Task.FromResult<OrganizationAbility?>(null));
|
||||
|
||||
var badRequestException = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.SaveAsync(policyUpdate));
|
||||
|
||||
Assert.Contains("Organization not found", badRequestException.Message, StringComparison.OrdinalIgnoreCase);
|
||||
await AssertPolicyNotSavedAsync(sutProvider);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task SaveAsync_OrganizationCannotUsePolicies_ThrowsBadRequest(PolicyUpdate policyUpdate)
|
||||
{
|
||||
var sutProvider = SutProviderFactory();
|
||||
sutProvider.GetDependency<IApplicationCacheService>()
|
||||
.GetOrganizationAbilityAsync(policyUpdate.OrganizationId)
|
||||
.Returns(new OrganizationAbility
|
||||
{
|
||||
Id = policyUpdate.OrganizationId,
|
||||
UsePolicies = false
|
||||
});
|
||||
|
||||
var badRequestException = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.SaveAsync(policyUpdate));
|
||||
|
||||
Assert.Contains("cannot use policies", badRequestException.Message, StringComparison.OrdinalIgnoreCase);
|
||||
await AssertPolicyNotSavedAsync(sutProvider);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task SaveAsync_RequiredPolicyIsNull_Throws(
|
||||
[PolicyUpdate(PolicyType.RequireSso)] PolicyUpdate policyUpdate)
|
||||
{
|
||||
var sutProvider = SutProviderFactory([
|
||||
new FakeRequireSsoPolicyValidator(),
|
||||
new FakeSingleOrgPolicyValidator()
|
||||
]);
|
||||
|
||||
ArrangeOrganization(sutProvider, policyUpdate);
|
||||
sutProvider.GetDependency<IPolicyRepository>()
|
||||
.GetManyByOrganizationIdAsync(policyUpdate.OrganizationId)
|
||||
.Returns([]);
|
||||
|
||||
var badRequestException = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.SaveAsync(policyUpdate));
|
||||
|
||||
Assert.Contains("Turn on the Single organization policy because it is required for the Require single sign-on authentication policy", badRequestException.Message, StringComparison.OrdinalIgnoreCase);
|
||||
await AssertPolicyNotSavedAsync(sutProvider);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task SaveAsync_RequiredPolicyNotEnabled_Throws(
|
||||
[PolicyUpdate(PolicyType.RequireSso)] PolicyUpdate policyUpdate,
|
||||
[Policy(PolicyType.SingleOrg, false)] Policy singleOrgPolicy)
|
||||
{
|
||||
var sutProvider = SutProviderFactory([
|
||||
new FakeRequireSsoPolicyValidator(),
|
||||
new FakeSingleOrgPolicyValidator()
|
||||
]);
|
||||
|
||||
ArrangeOrganization(sutProvider, policyUpdate);
|
||||
sutProvider.GetDependency<IPolicyRepository>()
|
||||
.GetManyByOrganizationIdAsync(policyUpdate.OrganizationId)
|
||||
.Returns([singleOrgPolicy]);
|
||||
|
||||
var badRequestException = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.SaveAsync(policyUpdate));
|
||||
|
||||
Assert.Contains("Turn on the Single organization policy because it is required for the Require single sign-on authentication policy", badRequestException.Message, StringComparison.OrdinalIgnoreCase);
|
||||
await AssertPolicyNotSavedAsync(sutProvider);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task SaveAsync_RequiredPolicyEnabled_Success(
|
||||
[PolicyUpdate(PolicyType.RequireSso)] PolicyUpdate policyUpdate,
|
||||
[Policy(PolicyType.SingleOrg)] Policy singleOrgPolicy)
|
||||
{
|
||||
var sutProvider = SutProviderFactory([
|
||||
new FakeRequireSsoPolicyValidator(),
|
||||
new FakeSingleOrgPolicyValidator()
|
||||
]);
|
||||
|
||||
ArrangeOrganization(sutProvider, policyUpdate);
|
||||
sutProvider.GetDependency<IPolicyRepository>()
|
||||
.GetManyByOrganizationIdAsync(policyUpdate.OrganizationId)
|
||||
.Returns([singleOrgPolicy]);
|
||||
|
||||
await sutProvider.Sut.SaveAsync(policyUpdate);
|
||||
await AssertPolicySavedAsync(sutProvider, policyUpdate);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task SaveAsync_DependentPolicyIsEnabled_Throws(
|
||||
[PolicyUpdate(PolicyType.SingleOrg, false)] PolicyUpdate policyUpdate,
|
||||
[Policy(PolicyType.SingleOrg)] Policy currentPolicy,
|
||||
[Policy(PolicyType.RequireSso)] Policy requireSsoPolicy) // depends on Single Org
|
||||
{
|
||||
var sutProvider = SutProviderFactory([
|
||||
new FakeRequireSsoPolicyValidator(),
|
||||
new FakeSingleOrgPolicyValidator()
|
||||
]);
|
||||
|
||||
ArrangeOrganization(sutProvider, policyUpdate);
|
||||
sutProvider.GetDependency<IPolicyRepository>()
|
||||
.GetManyByOrganizationIdAsync(policyUpdate.OrganizationId)
|
||||
.Returns([currentPolicy, requireSsoPolicy]);
|
||||
|
||||
var badRequestException = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.SaveAsync(policyUpdate));
|
||||
|
||||
Assert.Contains("Turn off the Require single sign-on authentication policy because it requires the Single organization policy", badRequestException.Message, StringComparison.OrdinalIgnoreCase);
|
||||
await AssertPolicyNotSavedAsync(sutProvider);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task SaveAsync_MultipleDependentPoliciesAreEnabled_Throws(
|
||||
[PolicyUpdate(PolicyType.SingleOrg, false)] PolicyUpdate policyUpdate,
|
||||
[Policy(PolicyType.SingleOrg)] Policy currentPolicy,
|
||||
[Policy(PolicyType.RequireSso)] Policy requireSsoPolicy, // depends on Single Org
|
||||
[Policy(PolicyType.MaximumVaultTimeout)] Policy vaultTimeoutPolicy) // depends on Single Org
|
||||
{
|
||||
var sutProvider = SutProviderFactory([
|
||||
new FakeRequireSsoPolicyValidator(),
|
||||
new FakeSingleOrgPolicyValidator(),
|
||||
new FakeVaultTimeoutPolicyValidator()
|
||||
]);
|
||||
|
||||
ArrangeOrganization(sutProvider, policyUpdate);
|
||||
sutProvider.GetDependency<IPolicyRepository>()
|
||||
.GetManyByOrganizationIdAsync(policyUpdate.OrganizationId)
|
||||
.Returns([currentPolicy, requireSsoPolicy, vaultTimeoutPolicy]);
|
||||
|
||||
var badRequestException = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.SaveAsync(policyUpdate));
|
||||
|
||||
Assert.Contains("Turn off all of the policies that require the Single organization policy", badRequestException.Message, StringComparison.OrdinalIgnoreCase);
|
||||
await AssertPolicyNotSavedAsync(sutProvider);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task SaveAsync_DependentPolicyNotEnabled_Success(
|
||||
[PolicyUpdate(PolicyType.SingleOrg, false)] PolicyUpdate policyUpdate,
|
||||
[Policy(PolicyType.SingleOrg)] Policy currentPolicy,
|
||||
[Policy(PolicyType.RequireSso, false)] Policy requireSsoPolicy) // depends on Single Org but is not enabled
|
||||
{
|
||||
var sutProvider = SutProviderFactory([
|
||||
new FakeRequireSsoPolicyValidator(),
|
||||
new FakeSingleOrgPolicyValidator()
|
||||
]);
|
||||
|
||||
ArrangeOrganization(sutProvider, policyUpdate);
|
||||
sutProvider.GetDependency<IPolicyRepository>()
|
||||
.GetManyByOrganizationIdAsync(policyUpdate.OrganizationId)
|
||||
.Returns([currentPolicy, requireSsoPolicy]);
|
||||
|
||||
await sutProvider.Sut.SaveAsync(policyUpdate);
|
||||
|
||||
await AssertPolicySavedAsync(sutProvider, policyUpdate);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task SaveAsync_ThrowsOnValidationError([PolicyUpdate(PolicyType.SingleOrg)] PolicyUpdate policyUpdate)
|
||||
{
|
||||
var fakePolicyValidator = new FakeSingleOrgPolicyValidator();
|
||||
fakePolicyValidator.ValidateAsyncMock(policyUpdate, null).Returns("Validation error!");
|
||||
var sutProvider = SutProviderFactory([fakePolicyValidator]);
|
||||
|
||||
ArrangeOrganization(sutProvider, policyUpdate);
|
||||
sutProvider.GetDependency<IPolicyRepository>().GetManyByOrganizationIdAsync(policyUpdate.OrganizationId).Returns([]);
|
||||
|
||||
var badRequestException = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.SaveAsync(policyUpdate));
|
||||
|
||||
Assert.Contains("Validation error!", badRequestException.Message, StringComparison.OrdinalIgnoreCase);
|
||||
await AssertPolicyNotSavedAsync(sutProvider);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a new SutProvider with the PolicyValidators registered in the Sut.
|
||||
/// </summary>
|
||||
private static SutProvider<SavePolicyCommand> SutProviderFactory(IEnumerable<IPolicyValidator>? policyValidators = null)
|
||||
{
|
||||
return new SutProvider<SavePolicyCommand>()
|
||||
.WithFakeTimeProvider()
|
||||
.SetDependency(typeof(IEnumerable<IPolicyValidator>), policyValidators ?? [])
|
||||
.Create();
|
||||
}
|
||||
|
||||
private static void ArrangeOrganization(SutProvider<SavePolicyCommand> sutProvider, PolicyUpdate policyUpdate)
|
||||
{
|
||||
sutProvider.GetDependency<IApplicationCacheService>()
|
||||
.GetOrganizationAbilityAsync(policyUpdate.OrganizationId)
|
||||
.Returns(new OrganizationAbility
|
||||
{
|
||||
Id = policyUpdate.OrganizationId,
|
||||
UsePolicies = true
|
||||
});
|
||||
}
|
||||
|
||||
private static async Task AssertPolicyNotSavedAsync(SutProvider<SavePolicyCommand> sutProvider)
|
||||
{
|
||||
await sutProvider.GetDependency<IPolicyRepository>()
|
||||
.DidNotReceiveWithAnyArgs()
|
||||
.UpsertAsync(default!);
|
||||
|
||||
await sutProvider.GetDependency<IEventService>()
|
||||
.DidNotReceiveWithAnyArgs()
|
||||
.LogPolicyEventAsync(default, default);
|
||||
}
|
||||
|
||||
private static async Task AssertPolicySavedAsync(SutProvider<SavePolicyCommand> sutProvider, PolicyUpdate policyUpdate)
|
||||
{
|
||||
var expectedPolicy = () => Arg.Is<Policy>(p =>
|
||||
p.Type == policyUpdate.Type &&
|
||||
p.OrganizationId == policyUpdate.OrganizationId &&
|
||||
p.Enabled == policyUpdate.Enabled &&
|
||||
p.Data == policyUpdate.Data);
|
||||
|
||||
await sutProvider.GetDependency<IPolicyRepository>().Received(1).UpsertAsync(expectedPolicy());
|
||||
|
||||
await sutProvider.GetDependency<IEventService>().Received(1)
|
||||
.LogPolicyEventAsync(expectedPolicy(), EventType.Policy_Updated);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user