1
0
mirror of https://github.com/bitwarden/server.git synced 2024-12-02 13:53:23 +01:00

Reduce scope to just saving, implement RequiredPolicies

This commit is contained in:
Thomas Rittson 2024-10-04 12:25:18 +10:00
parent 5943f0115d
commit f6c7be50cf
No known key found for this signature in database
GPG Key ID: CDDDA03861C35E27
6 changed files with 74 additions and 93 deletions

View File

@ -6,7 +6,7 @@ using Bit.Core.Entities;
namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies;
public interface IPolicyDefinition<TRequirement>
public interface IPolicyDefinition
{
/// <summary>
/// The PolicyType that the strategy is responsible for handling.
@ -14,23 +14,14 @@ public interface IPolicyDefinition<TRequirement>
public PolicyType Type { get; }
/// <summary>
/// A predicate function that returns true if a policy should be enforced against a user
/// and false otherwise. This does not need to check Organization.UsePolicies or Policy.Enabled.
/// PolicyTypes that must be enabled before this policy can be enabled, if any.
/// </summary>
public Predicate<(OrganizationUser orgUser, Policy policy)> Filter { get; }
public IEnumerable<PolicyType> RequiredPolicies { get; }
/// <summary>
/// A reducer function that reduces Policies into policy requirements (as defined by TRequirement).
/// This is used to reconcile policies of the same type from different organizations and combine them into
/// a single object that represents the requirements of the domain.
/// </summary>
public (Func<TRequirement, Policy> reducer, TRequirement initialValue) Reducer { get; }
// TODO: Currently interdependencies between policies must be checked in both definitions.
// TODO: Consider a separate definition for policy prerequisites that is automatically cross-checked on all handlers,
// TODO: so they can be declared once only.
/// <summary>
/// Validates a policy before saving it.
/// Basic interdependencies between policies are already handled by the <see cref="RequiredPolicies"/> definition.
/// Use this for additional or more complex validation, if any.
/// </summary>
/// <param name="currentPolicy">The current policy, if any</param>
/// <param name="modifiedPolicy">The modified policy to be saved</param>
@ -45,12 +36,3 @@ public interface IPolicyDefinition<TRequirement>
/// <param name="modifiedPolicy">The modified policy to be saved</param>
public Task OnSaveSideEffectsAsync(Policy? currentPolicy, Policy modifiedPolicy);
}
public interface IPolicyDefinition<TRequirement, TData> : IPolicyDefinition<TRequirement>
{
/// <summary>
/// A factory that transforms the untyped Policy.Data JSON object to a domain specific object,
/// usually used for additional policy configuration.
/// </summary>
public Func<object, TData>? DataFactory { get; }
}

View File

@ -15,11 +15,10 @@ using Bit.Core.Services;
namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.Implementations;
public record SingleOrgRequirement(bool SingleOrgRequired);
public class SingleOrgPolicyDefinition : IPolicyDefinition<SingleOrgRequirement>
public class SingleOrgPolicyDefinition : IPolicyDefinition
{
public PolicyType Type => PolicyType.SingleOrg;
public IEnumerable<PolicyType> RequiredPolicies => Array.Empty<PolicyType>();
private readonly IOrganizationUserRepository _organizationUserRepository;
private readonly IMailService _mailService;
@ -44,13 +43,6 @@ public class SingleOrgPolicyDefinition : IPolicyDefinition<SingleOrgRequirement>
_currentContext = currentContext;
}
public Predicate<(OrganizationUser orgUser, Policy policy)> Filter => tuple =>
tuple.orgUser is not { Type: OrganizationUserType.Owner or OrganizationUserType.Admin };
public (Func<SingleOrgRequirement, Policy, SingleOrgRequirement> reducer, SingleOrgRequirement initialValue) Reducer() =>
((SingleOrgRequirement init, Policy next, SingleOrgRequirement ) => new SingleOrgRequirement(true), new SingleOrgRequirement(false));
public async Task OnSaveSideEffectsAsync(Policy? currentPolicy, Policy modifiedPolicy)
{
if (currentPolicy is null or { Enabled: false } && modifiedPolicy is { Enabled: true })
@ -100,23 +92,6 @@ public class SingleOrgPolicyDefinition : IPolicyDefinition<SingleOrgRequirement>
{
var organizationId = modifiedPolicy.OrganizationId;
// Do not allow this policy to be disabled if a dependent policy is still enabled
var policies = await _policyRepository.GetManyByOrganizationIdAsync(organizationId);
if (policies.Any(p => p.Type == PolicyType.RequireSso && p.Enabled))
{
return "Single Sign-On Authentication policy is enabled.";
}
if (policies.Any(p => p.Type == PolicyType.MaximumVaultTimeout && p.Enabled))
{
return "Maximum Vault Timeout policy is enabled.";
}
if (policies.Any(p => p.Type == PolicyType.ResetPassword && p.Enabled))
{
return "Account Recovery policy is enabled.";
}
// Do not allow this policy to be disabled if Key Connector is being used
var ssoConfig = await _ssoConfigRepository.GetByOrganizationIdAsync(organizationId);
if (ssoConfig?.GetData()?.MemberDecryptionType == MemberDecryptionType.KeyConnector)

View File

@ -1,8 +0,0 @@
using Bit.Core.AdminConsole.Entities;
namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies;
public static class PolicyDefinitionExtensions
{
public static void PolicyStateChanged(Policy? currentPolicy, Policy modifiedPolicy)
}

View File

@ -10,6 +10,6 @@ public static class PolicyServiceCollectionExtensions
public static void AddPolicyServices(this IServiceCollection services)
{
services.AddScoped<IPolicyService, PolicyService>();
services.AddScoped<IPolicyDefinition<SingleOrgRequirement>, SingleOrgPolicyDefinition>();
services.AddScoped<IPolicyDefinition, SingleOrgPolicyDefinition>();
}
}

View File

@ -19,35 +19,36 @@ public class PolicyService : IPolicyService
{
private readonly IApplicationCacheService _applicationCacheService;
private readonly IEventService _eventService;
private readonly IOrganizationRepository _organizationRepository;
private readonly IOrganizationUserRepository _organizationUserRepository;
private readonly IPolicyRepository _policyRepository;
private readonly GlobalSettings _globalSettings;
private readonly IEnumerable<IPolicyDefinition<,>> _policyStrategies;
private readonly Dictionary<PolicyType, IPolicyDefinition> _policyDefinitions = new();
public PolicyService(
IApplicationCacheService applicationCacheService,
IEventService eventService,
IOrganizationRepository organizationRepository,
IOrganizationUserRepository organizationUserRepository,
IPolicyRepository policyRepository,
GlobalSettings globalSettings,
IEnumerable<IPolicyDefinition<,>> policyStrategies)
IEnumerable<IPolicyDefinition> policyDefinitions)
{
_applicationCacheService = applicationCacheService;
_eventService = eventService;
_organizationRepository = organizationRepository;
_organizationUserRepository = organizationUserRepository;
_policyRepository = policyRepository;
_globalSettings = globalSettings;
_policyStrategies = policyStrategies;
foreach (var policyDefinition in policyDefinitions)
{
_policyDefinitions.Add(policyDefinition.Type, policyDefinition);
// TODO: throw if any policyDefinition is missing
}
}
public async Task SaveAsync(Policy policy, IUserService userService, IOrganizationService organizationService,
Guid? savingUserId)
{
// TODO: this could use the cache
var org = await _organizationRepository.GetByIdAsync(policy.OrganizationId);
var org = await _applicationCacheService.GetOrganizationAbilityAsync(policy.OrganizationId);
if (org == null)
{
throw new BadRequestException("Organization not found");
@ -58,10 +59,40 @@ public class PolicyService : IPolicyService
throw new BadRequestException("This organization cannot use policies.");
}
var policyDefinition = _policyStrategies.Single(strategy => strategy.Type == policy.Type);
var currentPolicy = await _policyRepository.GetByIdAsync(policy.Id);
var policyDefinition = _policyDefinitions[policy.Type];
var allSavedPolicies = await _policyRepository.GetManyByOrganizationIdAsync(org.Id);
var currentPolicy = allSavedPolicies.SingleOrDefault(p => p.Id == policy.Id);
// Validate
// If enabling this policy - check that all policy requirements are satisfied
if (currentPolicy is not { Enabled: true } && policy.Enabled)
{
foreach (var requiredPolicyType in policyDefinition.RequiredPolicies)
{
if (allSavedPolicies.SingleOrDefault(p => p.Type == requiredPolicyType) is not { Enabled: true })
{
// TODO: would be better to reference the name instead of the enum
throw new BadRequestException("Policy requires PolicyType " + requiredPolicyType + " to be enabled first.");
}
}
}
// If disabling this policy - ensure it's not required by any other policy
if (currentPolicy is { Enabled: true } && !policy.Enabled)
{
var dependentPolicies = _policyDefinitions.Values
.Where(policyDef => policyDef.RequiredPolicies.Contains(policy.Type))
.Select(policyDef => policyDef.Type)
.Select(otherPolicyType => allSavedPolicies.SingleOrDefault(p => p.Type == otherPolicyType))
.Where(otherPolicy => otherPolicy is { Enabled: true })
.ToList();
if (dependentPolicies is { Count: > 0})
{
throw new BadRequestException("This policy is required by " + dependentPolicies.First() + ". Try disabling that policy first." );
}
}
// Run other validation
var validationError = await policyDefinition.ValidateAsync(currentPolicy, policy);
if (validationError != null)
{

View File

@ -9,6 +9,7 @@ using Bit.Core.Auth.Models.Data;
using Bit.Core.Auth.Repositories;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Models.Data.Organizations;
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
using Bit.Core.Repositories;
using Bit.Core.Services;
@ -51,9 +52,7 @@ public class PolicyServiceTests
public async Task SaveAsync_OrganizationCannotUsePolicies_ThrowsBadRequest(
[AdminConsoleFixtures.Policy(PolicyType.DisableSend)] Policy policy, SutProvider<PolicyService> sutProvider)
{
var orgId = Guid.NewGuid();
SetupOrg(sutProvider, policy.OrganizationId, new Organization
SetupOrg(sutProvider, policy.OrganizationId, new OrganizationAbility
{
UsePolicies = false,
});
@ -81,7 +80,7 @@ public class PolicyServiceTests
{
policy.Enabled = false;
SetupOrg(sutProvider, policy.OrganizationId, new Organization
SetupOrg(sutProvider, policy.OrganizationId, new OrganizationAbility
{
Id = policy.OrganizationId,
UsePolicies = true,
@ -113,7 +112,7 @@ public class PolicyServiceTests
{
policy.Enabled = false;
SetupOrg(sutProvider, policy.OrganizationId, new Organization
SetupOrg(sutProvider, policy.OrganizationId, new OrganizationAbility
{
Id = policy.OrganizationId,
UsePolicies = true,
@ -147,7 +146,7 @@ public class PolicyServiceTests
policy.Enabled = false;
policy.Type = policyType;
SetupOrg(sutProvider, policy.OrganizationId, new Organization
SetupOrg(sutProvider, policy.OrganizationId, new OrganizationAbility
{
Id = policy.OrganizationId,
UsePolicies = true,
@ -180,7 +179,7 @@ public class PolicyServiceTests
{
policy.Enabled = true;
SetupOrg(sutProvider, policy.OrganizationId, new Organization
SetupOrg(sutProvider, policy.OrganizationId, new OrganizationAbility
{
Id = policy.OrganizationId,
UsePolicies = true,
@ -214,7 +213,7 @@ public class PolicyServiceTests
policy.Id = default;
policy.Data = null;
SetupOrg(sutProvider, policy.OrganizationId, new Organization
SetupOrg(sutProvider, policy.OrganizationId, new OrganizationAbility
{
Id = policy.OrganizationId,
UsePolicies = true,
@ -244,7 +243,7 @@ public class PolicyServiceTests
{
policy.Enabled = true;
SetupOrg(sutProvider, policy.OrganizationId, new Organization
SetupOrg(sutProvider, policy.OrganizationId, new OrganizationAbility
{
Id = policy.OrganizationId,
UsePolicies = true,
@ -273,16 +272,19 @@ public class PolicyServiceTests
[Theory, BitAutoData]
public async Task SaveAsync_ExistingPolicy_UpdateTwoFactor(
OrganizationAbility organizationAbility,
Organization organization,
[AdminConsoleFixtures.Policy(PolicyType.TwoFactorAuthentication)] Policy policy,
SutProvider<PolicyService> sutProvider)
{
// If the policy that this is updating isn't enabled then do some work now that the current one is enabled
organization.UsePolicies = true;
policy.OrganizationId = organization.Id;
organizationAbility.UsePolicies = true;
policy.OrganizationId = organizationAbility.Id = organization.Id;
SetupOrg(sutProvider, organization.Id, organization);
SetupOrg(sutProvider, organization.Id, organizationAbility);
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
sutProvider.GetDependency<IPolicyRepository>()
.GetByIdAsync(policy.Id)
@ -392,7 +394,7 @@ public class PolicyServiceTests
[Theory, BitAutoData]
public async Task SaveAsync_EnableTwoFactor_WithoutMasterPasswordOr2FA_ThrowsBadRequest(
Organization organization,
OrganizationAbility organization,
[AdminConsoleFixtures.Policy(PolicyType.TwoFactorAuthentication)] Policy policy,
SutProvider<PolicyService> sutProvider)
{
@ -489,11 +491,10 @@ public class PolicyServiceTests
{
// If the policy that this is updating isn't enabled then do some work now that the current one is enabled
var org = new Organization
var org = new OrganizationAbility()
{
Id = policy.OrganizationId,
UsePolicies = true,
Name = "TEST",
};
SetupOrg(sutProvider, policy.OrganizationId, org);
@ -564,7 +565,7 @@ public class PolicyServiceTests
AutoEnrollEnabled = autoEnrollEnabled
});
SetupOrg(sutProvider, policy.OrganizationId, new Organization
SetupOrg(sutProvider, policy.OrganizationId, new OrganizationAbility
{
Id = policy.OrganizationId,
UsePolicies = true,
@ -601,7 +602,7 @@ public class PolicyServiceTests
{
policy.Enabled = false;
SetupOrg(sutProvider, policy.OrganizationId, new Organization
SetupOrg(sutProvider, policy.OrganizationId, new OrganizationAbility
{
Id = policy.OrganizationId,
UsePolicies = true,
@ -638,7 +639,7 @@ public class PolicyServiceTests
policy.Enabled = true;
policy.SetDataModel(new ResetPasswordDataModel());
SetupOrg(sutProvider, policy.OrganizationId, new Organization
SetupOrg(sutProvider, policy.OrganizationId, new OrganizationAbility
{
Id = policy.OrganizationId,
UsePolicies = true,
@ -672,7 +673,7 @@ public class PolicyServiceTests
{
policy.Enabled = false;
SetupOrg(sutProvider, policy.OrganizationId, new Organization
SetupOrg(sutProvider, policy.OrganizationId, new OrganizationAbility
{
Id = policy.OrganizationId,
UsePolicies = true,
@ -797,11 +798,11 @@ public class PolicyServiceTests
Assert.True(result);
}
private static void SetupOrg(SutProvider<PolicyService> sutProvider, Guid organizationId, Organization organization)
private static void SetupOrg(SutProvider<PolicyService> sutProvider, Guid organizationId, OrganizationAbility organizationAbility)
{
sutProvider.GetDependency<IOrganizationRepository>()
.GetByIdAsync(organizationId)
.Returns(Task.FromResult(organization));
sutProvider.GetDependency<IApplicationCacheService>()
.GetOrganizationAbilityAsync(organizationId)
.Returns(organizationAbility);
}
private static void SetupUserPolicies(Guid userId, SutProvider<PolicyService> sutProvider)