diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/Implementations/PolicyRequirementQuery.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/Implementations/PolicyRequirementQuery.cs index 585d2348ef..de4796d4b5 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Policies/Implementations/PolicyRequirementQuery.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/Implementations/PolicyRequirementQuery.cs @@ -8,21 +8,25 @@ namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.Implementations; public class PolicyRequirementQuery( IPolicyRepository policyRepository, - IEnumerable> factories) + IEnumerable> factories) : IPolicyRequirementQuery { public async Task GetAsync(Guid userId) where T : IPolicyRequirement { - var factory = factories.OfType>().SingleOrDefault(); + var factory = factories.OfType>().SingleOrDefault(); if (factory is null) { - throw new NotImplementedException("No Policy Requirement found for " + typeof(T)); + throw new NotImplementedException("No Requirement Factory found for " + typeof(T)); } - return factory(await GetPolicyDetails(userId)); + var policyDetails = await GetPolicyDetails(userId); + var filteredPolicies = policyDetails + .Where(p => p.PolicyType == factory.PolicyType) + .Where(factory.Enforce); + var requirement = factory.Create(filteredPolicies); + return requirement; } - private Task> GetPolicyDetails(Guid userId) => - policyRepository.GetPolicyDetailsByUserId(userId); + private Task> GetPolicyDetails(Guid userId) + => policyRepository.GetPolicyDetailsByUserId(userId); } - diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/BasePolicyRequirementFactory.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/BasePolicyRequirementFactory.cs new file mode 100644 index 0000000000..cebbe91904 --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/BasePolicyRequirementFactory.cs @@ -0,0 +1,44 @@ +using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; +using Bit.Core.Enums; + +namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; + +/// +/// A simple base implementation of which will be suitable for most policies. +/// It provides sensible defaults to help teams to implement their own Policy Requirements. +/// +/// +public abstract class BasePolicyRequirementFactory : IPolicyRequirementFactory where T : IPolicyRequirement +{ + /// + /// User roles that are exempt from policy enforcement. + /// Owners and Admins are exempt by default but this may be overridden. + /// + protected virtual IEnumerable ExemptRoles { get; } = + [OrganizationUserType.Owner, OrganizationUserType.Admin]; + + /// + /// User statuses that are exempt from policy enforcement. + /// Invited and Revoked users are exempt by default, which is appropriate in the majority of cases. + /// + protected virtual IEnumerable ExemptStatuses { get; } = + [OrganizationUserStatusType.Invited, OrganizationUserStatusType.Revoked]; + + /// + /// Whether a Provider User for the organization is exempt from policy enforcement. + /// Provider Users are exempt by default, which is appropriate in the majority of cases. + /// + protected virtual bool ExemptProviders { get; } = true; + + /// + public abstract PolicyType PolicyType { get; } + + public bool Enforce(PolicyDetails policyDetails) + => !policyDetails.HasRole(ExemptRoles) && + !policyDetails.HasStatus(ExemptStatuses) && + (!policyDetails.IsProvider || !ExemptProviders); + + /// + public abstract T Create(IEnumerable policyDetails); +} diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/DisableSendPolicyRequirement.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/DisableSendPolicyRequirement.cs new file mode 100644 index 0000000000..1cb7f4f619 --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/DisableSendPolicyRequirement.cs @@ -0,0 +1,27 @@ +using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; + +namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; + +/// +/// Policy requirements for the Disable Send policy. +/// +public class DisableSendPolicyRequirement : IPolicyRequirement +{ + /// + /// Indicates whether Send is disabled for the user. If true, the user should not be able to create or edit Sends. + /// They may still delete existing Sends. + /// + public bool DisableSend { get; init; } +} + +public class DisableSendPolicyRequirementFactory : BasePolicyRequirementFactory +{ + public override PolicyType PolicyType => PolicyType.DisableSend; + + public override DisableSendPolicyRequirement Create(IEnumerable policyDetails) + { + var result = new DisableSendPolicyRequirement { DisableSend = policyDetails.Any() }; + return result; + } +} diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/IPolicyRequirement.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/IPolicyRequirement.cs index 3f331b1130..dcb82b1ac0 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/IPolicyRequirement.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/IPolicyRequirement.cs @@ -1,24 +1,11 @@ -#nullable enable - -using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; +using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Enums; namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; /// -/// Represents the business requirements of how one or more enterprise policies will be enforced against a user. -/// The implementation of this interface will depend on how the policies are enforced in the relevant domain. +/// An object that represents how a will be enforced against a user. +/// This acts as a bridge between the entity saved to the database and the domain that the policy +/// affects. You may represent the impact of the policy in any way that makes sense for the domain. /// public interface IPolicyRequirement; - -/// -/// A factory function that takes a sequence of and transforms them into a single -/// for consumption by the relevant domain. This will receive *all* policy types -/// that may be enforced against a user; when implementing this delegate, you must filter out irrelevant policy types -/// as well as policies that should not be enforced against a user (e.g. due to the user's role or status). -/// -/// -/// See for extension methods to handle common requirements when implementing -/// this delegate. -/// -public delegate T RequirementFactory(IEnumerable policyDetails) - where T : IPolicyRequirement; diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/IPolicyRequirementFactory.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/IPolicyRequirementFactory.cs new file mode 100644 index 0000000000..e0b51a46a2 --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/IPolicyRequirementFactory.cs @@ -0,0 +1,39 @@ +#nullable enable + +using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; + +namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; + +/// +/// An interface that defines how to create a single from a sequence of +/// . +/// +/// The that the factory produces. +/// +/// See for a simple base implementation suitable for most policies. +/// +public interface IPolicyRequirementFactory where T : IPolicyRequirement +{ + /// + /// The that the requirement relates to. + /// + PolicyType PolicyType { get; } + + /// + /// A predicate that determines whether a policy should be enforced against the user. + /// + /// Use this to exempt users based on their role, status or other attributes. + /// Policy details for the defined PolicyType. + /// True if the policy should be enforced against the user, false otherwise. + bool Enforce(PolicyDetails policyDetails); + + /// + /// A reducer method that creates a single from a set of PolicyDetails. + /// + /// + /// PolicyDetails for the specified PolicyType, after they have been filtered by the Enforce predicate. That is, + /// this is the final interface to be called. + /// + T Create(IEnumerable policyDetails); +} diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/PolicyRequirementHelpers.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/PolicyRequirementHelpers.cs index fc4cd91a3d..3497c18031 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/PolicyRequirementHelpers.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/PolicyRequirementHelpers.cs @@ -1,5 +1,4 @@ -using Bit.Core.AdminConsole.Enums; -using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; +using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; using Bit.Core.Enums; namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; @@ -7,35 +6,16 @@ namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements public static class PolicyRequirementHelpers { /// - /// Filters the PolicyDetails by PolicyType. This is generally required to only get the PolicyDetails that your - /// IPolicyRequirement relates to. + /// Returns true if the is for one of the specified roles, false otherwise. /// - public static IEnumerable GetPolicyType( - this IEnumerable policyDetails, - PolicyType type) - => policyDetails.Where(x => x.PolicyType == type); - - /// - /// Filters the PolicyDetails to remove the specified user roles. This can be used to exempt - /// owners and admins from policy enforcement. - /// - public static IEnumerable ExemptRoles( - this IEnumerable policyDetails, + public static bool HasRole( + this PolicyDetails policyDetails, IEnumerable roles) - => policyDetails.Where(x => !roles.Contains(x.OrganizationUserType)); + => roles.Contains(policyDetails.OrganizationUserType); /// - /// Filters the PolicyDetails to remove organization users who are also provider users for the organization. - /// This can be used to exempt provider users from policy enforcement. + /// Returns true if the relates to one of the specified statuses, false otherwise. /// - public static IEnumerable ExemptProviders(this IEnumerable policyDetails) - => policyDetails.Where(x => !x.IsProvider); - - /// - /// Filters the PolicyDetails to remove the specified organization user statuses. For example, this can be used - /// to exempt users in the invited and revoked statuses from policy enforcement. - /// - public static IEnumerable ExemptStatus( - this IEnumerable policyDetails, IEnumerable status) - => policyDetails.Where(x => !status.Contains(x.OrganizationUserStatus)); + public static bool HasStatus(this PolicyDetails policyDetails, IEnumerable status) + => status.Contains(policyDetails.OrganizationUserStatus); } diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SendOptionsPolicyRequirement.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SendOptionsPolicyRequirement.cs new file mode 100644 index 0000000000..9ba11c11df --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SendOptionsPolicyRequirement.cs @@ -0,0 +1,34 @@ +using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; + +namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; + +/// +/// Policy requirements for the Send Options policy. +/// +public class SendOptionsPolicyRequirement : IPolicyRequirement +{ + /// + /// Indicates whether the user is prohibited from hiding their email from the recipient of a Send. + /// + public bool DisableHideEmail { get; init; } +} + +public class SendOptionsPolicyRequirementFactory : BasePolicyRequirementFactory +{ + public override PolicyType PolicyType => PolicyType.SendOptions; + + public override SendOptionsPolicyRequirement Create(IEnumerable policyDetails) + { + var result = policyDetails + .Select(p => p.GetDataModel()) + .Aggregate( + new SendOptionsPolicyRequirement(), + (result, data) => new SendOptionsPolicyRequirement + { + DisableHideEmail = result.DisableHideEmail || data.DisableHideEmail + }); + + return result; + } +} diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SendPolicyRequirement.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SendPolicyRequirement.cs deleted file mode 100644 index c54cc98373..0000000000 --- a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SendPolicyRequirement.cs +++ /dev/null @@ -1,54 +0,0 @@ -using Bit.Core.AdminConsole.Enums; -using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; -using Bit.Core.Enums; - -namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; - -/// -/// Policy requirements for the Disable Send and Send Options policies. -/// -public class SendPolicyRequirement : IPolicyRequirement -{ - /// - /// Indicates whether Send is disabled for the user. If true, the user should not be able to create or edit Sends. - /// They may still delete existing Sends. - /// - public bool DisableSend { get; init; } - /// - /// Indicates whether the user is prohibited from hiding their email from the recipient of a Send. - /// - public bool DisableHideEmail { get; init; } - - /// - /// Create a new SendPolicyRequirement. - /// - /// All PolicyDetails relating to the user. - /// - /// This is a for the SendPolicyRequirement. - /// - public static SendPolicyRequirement Create(IEnumerable policyDetails) - { - var filteredPolicies = policyDetails - .ExemptRoles([OrganizationUserType.Owner, OrganizationUserType.Admin]) - .ExemptStatus([OrganizationUserStatusType.Invited, OrganizationUserStatusType.Revoked]) - .ExemptProviders() - .ToList(); - - var result = filteredPolicies - .GetPolicyType(PolicyType.SendOptions) - .Select(p => p.GetDataModel()) - .Aggregate( - new SendPolicyRequirement - { - // Set Disable Send requirement in the initial seed - DisableSend = filteredPolicies.GetPolicyType(PolicyType.DisableSend).Any() - }, - (result, data) => new SendPolicyRequirement - { - DisableSend = result.DisableSend, - DisableHideEmail = result.DisableHideEmail || data.DisableHideEmail - }); - - return result; - } -} diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyServiceCollectionExtensions.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyServiceCollectionExtensions.cs index 7bc8a7b5a3..6c698f9ffc 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyServiceCollectionExtensions.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyServiceCollectionExtensions.cs @@ -31,32 +31,7 @@ public static class PolicyServiceCollectionExtensions private static void AddPolicyRequirements(this IServiceCollection services) { - // Register policy requirement factories here - services.AddPolicyRequirement(SendPolicyRequirement.Create); + services.AddScoped, DisableSendPolicyRequirementFactory>(); + services.AddScoped, SendOptionsPolicyRequirementFactory>(); } - - /// - /// Used to register simple policy requirements where its factory method implements CreateRequirement. - /// This MUST be used rather than calling AddScoped directly, because it will ensure the factory method has - /// the correct type to be injected and then identified by at runtime. - /// - /// The specific PolicyRequirement being registered. - private static void AddPolicyRequirement(this IServiceCollection serviceCollection, RequirementFactory factory) - where T : class, IPolicyRequirement - => serviceCollection.AddPolicyRequirement(_ => factory); - - /// - /// Used to register policy requirements where you need to access additional dependencies (usually to return a - /// curried factory method). - /// This MUST be used rather than calling AddScoped directly, because it will ensure the factory method has - /// the correct type to be injected and then identified by at runtime. - /// - /// - /// A callback that takes IServiceProvider and returns a RequirementFactory for - /// your policy requirement. - /// - private static void AddPolicyRequirement(this IServiceCollection serviceCollection, - Func> factory) - where T : class, IPolicyRequirement - => serviceCollection.AddScoped>(factory); } diff --git a/src/Core/Tools/Services/Implementations/SendService.cs b/src/Core/Tools/Services/Implementations/SendService.cs index bddaa93bfc..e09787d7eb 100644 --- a/src/Core/Tools/Services/Implementations/SendService.cs +++ b/src/Core/Tools/Services/Implementations/SendService.cs @@ -326,14 +326,14 @@ public class SendService : ISendService return; } - var sendPolicyRequirement = await _policyRequirementQuery.GetAsync(userId.Value); - - if (sendPolicyRequirement.DisableSend) + var disableSendRequirement = await _policyRequirementQuery.GetAsync(userId.Value); + if (disableSendRequirement.DisableSend) { throw new BadRequestException("Due to an Enterprise Policy, you are only able to delete an existing Send."); } - if (sendPolicyRequirement.DisableHideEmail && send.HideEmail.GetValueOrDefault()) + var sendOptionsRequirement = await _policyRequirementQuery.GetAsync(userId.Value); + if (sendOptionsRequirement.DisableHideEmail && send.HideEmail.GetValueOrDefault()) { throw new BadRequestException("Due to an Enterprise Policy, you are not allowed to hide your email address from recipients when creating or editing a Send."); } diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirementFixtures.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirementFixtures.cs new file mode 100644 index 0000000000..4838d1e3c4 --- /dev/null +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirementFixtures.cs @@ -0,0 +1,23 @@ +using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; + +namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies; + +/// +/// Intentionally simplified PolicyRequirement that just holds the input PolicyDetails for us to assert against. +/// +public class TestPolicyRequirement : IPolicyRequirement +{ + public IEnumerable Policies { get; init; } = []; +} + +public class TestPolicyRequirementFactory(Func enforce) : IPolicyRequirementFactory +{ + public PolicyType PolicyType => PolicyType.SingleOrg; + + public bool Enforce(PolicyDetails policyDetails) => enforce(policyDetails); + + public TestPolicyRequirement Create(IEnumerable policyDetails) + => new() { Policies = policyDetails }; +} diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirementQueryTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirementQueryTests.cs index 4c98353774..56b6740678 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirementQueryTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirementQueryTests.cs @@ -1,6 +1,6 @@ -using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; +using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Implementations; -using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; using Bit.Core.AdminConsole.Repositories; using Bit.Test.Common.AutoFixture.Attributes; using NSubstitute; @@ -11,50 +11,72 @@ namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies; [SutProviderCustomize] public class PolicyRequirementQueryTests { - /// - /// Tests that the query correctly registers, retrieves and instantiates arbitrary IPolicyRequirements - /// according to their provided CreateRequirement delegate. - /// [Theory, BitAutoData] - public async Task GetAsync_Works(Guid userId, Guid organizationId) + public async Task GetAsync_IgnoresOtherPolicyTypes(Guid userId) { + var thisPolicy = new PolicyDetails { PolicyType = PolicyType.SingleOrg }; + var otherPolicy = new PolicyDetails { PolicyType = PolicyType.RequireSso }; var policyRepository = Substitute.For(); - var factories = new List> - { - // In prod this cast is handled when the CreateRequirement delegate is registered in DI - (RequirementFactory)TestPolicyRequirement.Create - }; + policyRepository.GetPolicyDetailsByUserId(userId).Returns([otherPolicy, thisPolicy]); - var sut = new PolicyRequirementQuery(policyRepository, factories); - policyRepository.GetPolicyDetailsByUserId(userId).Returns([ - new PolicyDetails - { - OrganizationId = organizationId - } - ]); + var factory = new TestPolicyRequirementFactory(_ => true); + var sut = new PolicyRequirementQuery(policyRepository, [factory]); var requirement = await sut.GetAsync(userId); - Assert.Equal(organizationId, requirement.OrganizationId); + + Assert.Contains(thisPolicy, requirement.Policies); + Assert.DoesNotContain(otherPolicy, requirement.Policies); } [Theory, BitAutoData] - public async Task GetAsync_ThrowsIfNoRequirementRegistered(Guid userId) + public async Task GetAsync_CallsEnforceCallback(Guid userId) + { + // Arrange policies + var policyRepository = Substitute.For(); + var thisPolicy = new PolicyDetails { PolicyType = PolicyType.SingleOrg }; + var otherPolicy = new PolicyDetails { PolicyType = PolicyType.SingleOrg }; + policyRepository.GetPolicyDetailsByUserId(userId).Returns([thisPolicy, otherPolicy]); + + // Arrange a substitute Enforce function so that we can inspect the received calls + var callback = Substitute.For>(); + callback(Arg.Any()).Returns(x => x.Arg() == thisPolicy); + + // Arrange the sut + var factory = new TestPolicyRequirementFactory(callback); + var sut = new PolicyRequirementQuery(policyRepository, [factory]); + + // Act + var requirement = await sut.GetAsync(userId); + + // Assert + Assert.Contains(thisPolicy, requirement.Policies); + Assert.DoesNotContain(otherPolicy, requirement.Policies); + callback.Received()(Arg.Is(thisPolicy)); + callback.Received()(Arg.Is(otherPolicy)); + } + + [Theory, BitAutoData] + public async Task GetAsync_ThrowsIfNoFactoryRegistered(Guid userId) { var policyRepository = Substitute.For(); var sut = new PolicyRequirementQuery(policyRepository, []); var exception = await Assert.ThrowsAsync(() => sut.GetAsync(userId)); - Assert.Contains("No Policy Requirement found", exception.Message); + Assert.Contains("No Requirement Factory found", exception.Message); } - /// - /// Intentionally simplified PolicyRequirement that just holds the Policy.OrganizationId for us to assert against. - /// - private class TestPolicyRequirement : IPolicyRequirement + [Theory, BitAutoData] + public async Task GetAsync_HandlesNoPolicies(Guid userId) { - public Guid OrganizationId { get; init; } - public static TestPolicyRequirement Create(IEnumerable policyDetails) - => new() { OrganizationId = policyDetails.Single().OrganizationId }; + var policyRepository = Substitute.For(); + policyRepository.GetPolicyDetailsByUserId(userId).Returns([]); + + var factory = new TestPolicyRequirementFactory(x => x.IsProvider); + var sut = new PolicyRequirementQuery(policyRepository, [factory]); + + var requirement = await sut.GetAsync(userId); + + Assert.Empty(requirement.Policies); } } diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/BasePolicyRequirementFactoryTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/BasePolicyRequirementFactoryTests.cs new file mode 100644 index 0000000000..e81459808d --- /dev/null +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/BasePolicyRequirementFactoryTests.cs @@ -0,0 +1,90 @@ +using AutoFixture.Xunit2; +using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; +using Bit.Core.Enums; +using Bit.Core.Test.AdminConsole.AutoFixture; +using Xunit; + +namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; + +public class BasePolicyRequirementFactoryTests +{ + [Theory, AutoData] + public void ExemptRoles_DoesNotEnforceAgainstThoseRoles( + [PolicyDetails(PolicyType.SingleOrg, OrganizationUserType.Owner)] PolicyDetails ownerPolicy, + [PolicyDetails(PolicyType.SingleOrg, OrganizationUserType.Admin)] PolicyDetails adminPolicy, + [PolicyDetails(PolicyType.SingleOrg, OrganizationUserType.Custom)] PolicyDetails customPolicy, + [PolicyDetails(PolicyType.SingleOrg)] PolicyDetails userPolicy) + { + var sut = new TestPolicyRequirementFactory( + // These exempt roles are intentionally unusual to make sure we're properly testing the sut + [OrganizationUserType.User, OrganizationUserType.Custom], + [], + false); + + Assert.True(sut.Enforce(ownerPolicy)); + Assert.True(sut.Enforce(adminPolicy)); + Assert.False(sut.Enforce(customPolicy)); + Assert.False(sut.Enforce(userPolicy)); + } + + [Theory, AutoData] + public void ExemptStatuses_DoesNotEnforceAgainstThoseStatuses( + [PolicyDetails(PolicyType.SingleOrg, userStatus: OrganizationUserStatusType.Invited)] PolicyDetails invitedPolicy, + [PolicyDetails(PolicyType.SingleOrg, userStatus: OrganizationUserStatusType.Accepted)] PolicyDetails acceptedPolicy, + [PolicyDetails(PolicyType.SingleOrg, userStatus: OrganizationUserStatusType.Confirmed)] PolicyDetails confirmedPolicy, + [PolicyDetails(PolicyType.SingleOrg, userStatus: OrganizationUserStatusType.Revoked)] PolicyDetails revokedPolicy) + { + var sut = new TestPolicyRequirementFactory( + [], + // These exempt statuses are intentionally unusual to make sure we're properly testing the sut + [OrganizationUserStatusType.Confirmed, OrganizationUserStatusType.Accepted], + false); + + Assert.True(sut.Enforce(invitedPolicy)); + Assert.True(sut.Enforce(revokedPolicy)); + Assert.False(sut.Enforce(confirmedPolicy)); + Assert.False(sut.Enforce(acceptedPolicy)); + } + + [Theory, AutoData] + public void ExemptProviders_DoesNotEnforceAgainstProviders( + [PolicyDetails(PolicyType.SingleOrg, isProvider: true)] PolicyDetails policy) + { + var sut = new TestPolicyRequirementFactory( + [], + [], + true); + + Assert.False(sut.Enforce(policy)); + } + + [Theory, AutoData] + public void NoExemptions_EnforcesAgainstAdminsAndProviders( + [PolicyDetails(PolicyType.SingleOrg, OrganizationUserType.Owner, isProvider: true)] PolicyDetails policy) + { + var sut = new TestPolicyRequirementFactory( + [], + [], + false); + + Assert.True(sut.Enforce(policy)); + } + + private class TestPolicyRequirementFactory( + IEnumerable exemptRoles, + IEnumerable exemptStatuses, + bool exemptProviders + ) : BasePolicyRequirementFactory + { + public override PolicyType PolicyType => PolicyType.SingleOrg; + protected override IEnumerable ExemptRoles => exemptRoles; + protected override IEnumerable ExemptStatuses => exemptStatuses; + + protected override bool ExemptProviders => exemptProviders; + + public override TestPolicyRequirement Create(IEnumerable policyDetails) + => new() { Policies = policyDetails }; + } +} diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/DisableSendPolicyRequirementFactoryTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/DisableSendPolicyRequirementFactoryTests.cs new file mode 100644 index 0000000000..2304c0e9ae --- /dev/null +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/DisableSendPolicyRequirementFactoryTests.cs @@ -0,0 +1,32 @@ +using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; +using Bit.Core.Test.AdminConsole.AutoFixture; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using Xunit; + +namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; + +[SutProviderCustomize] +public class DisableSendPolicyRequirementFactoryTests +{ + [Theory, BitAutoData] + public void DisableSend_IsFalse_IfNoPolicies(SutProvider sutProvider) + { + var actual = sutProvider.Sut.Create([]); + + Assert.False(actual.DisableSend); + } + + [Theory, BitAutoData] + public void DisableSend_IsTrue_IfAnyDisableSendPolicies( + [PolicyDetails(PolicyType.DisableSend)] PolicyDetails[] policies, + SutProvider sutProvider + ) + { + var actual = sutProvider.Sut.Create(policies); + + Assert.True(actual.DisableSend); + } +} diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SendOptionsPolicyRequirementFactoryTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SendOptionsPolicyRequirementFactoryTests.cs new file mode 100644 index 0000000000..af66d858ef --- /dev/null +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SendOptionsPolicyRequirementFactoryTests.cs @@ -0,0 +1,49 @@ +using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; +using Bit.Core.Test.AdminConsole.AutoFixture; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using Xunit; + +namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; + +[SutProviderCustomize] +public class SendOptionsPolicyRequirementFactoryTests +{ + [Theory, BitAutoData] + public void DisableHideEmail_IsFalse_IfNoPolicies(SutProvider sutProvider) + { + var actual = sutProvider.Sut.Create([]); + + Assert.False(actual.DisableHideEmail); + } + + [Theory, BitAutoData] + public void DisableHideEmail_IsFalse_IfNotConfigured( + [PolicyDetails(PolicyType.SendOptions)] PolicyDetails[] policies, + SutProvider sutProvider + ) + { + policies[0].SetDataModel(new SendOptionsPolicyData { DisableHideEmail = false }); + policies[1].SetDataModel(new SendOptionsPolicyData { DisableHideEmail = false }); + + var actual = sutProvider.Sut.Create(policies); + + Assert.False(actual.DisableHideEmail); + } + + [Theory, BitAutoData] + public void DisableHideEmail_IsTrue_IfAnyConfigured( + [PolicyDetails(PolicyType.SendOptions)] PolicyDetails[] policies, + SutProvider sutProvider + ) + { + policies[0].SetDataModel(new SendOptionsPolicyData { DisableHideEmail = true }); + policies[1].SetDataModel(new SendOptionsPolicyData { DisableHideEmail = false }); + + var actual = sutProvider.Sut.Create(policies); + + Assert.True(actual.DisableHideEmail); + } +} diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SendPolicyRequirementTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SendPolicyRequirementTests.cs deleted file mode 100644 index 4d7bf5db4e..0000000000 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SendPolicyRequirementTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using AutoFixture.Xunit2; -using Bit.Core.AdminConsole.Enums; -using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; -using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; -using Bit.Core.Enums; -using Bit.Core.Test.AdminConsole.AutoFixture; -using Xunit; - -namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; - -public class SendPolicyRequirementTests -{ - [Theory, AutoData] - public void DisableSend_IsFalse_IfNoDisableSendPolicies( - [PolicyDetails(PolicyType.RequireSso)] PolicyDetails otherPolicy1, - [PolicyDetails(PolicyType.SendOptions)] PolicyDetails otherPolicy2) - { - EnableDisableHideEmail(otherPolicy2); - - var actual = SendPolicyRequirement.Create([otherPolicy1, otherPolicy2]); - - Assert.False(actual.DisableSend); - } - - [Theory] - [InlineAutoData(OrganizationUserType.Owner, false)] - [InlineAutoData(OrganizationUserType.Admin, false)] - [InlineAutoData(OrganizationUserType.User, true)] - [InlineAutoData(OrganizationUserType.Custom, true)] - public void DisableSend_TestRoles( - OrganizationUserType userType, - bool shouldBeEnforced, - [PolicyDetails(PolicyType.DisableSend)] PolicyDetails policyDetails) - { - policyDetails.OrganizationUserType = userType; - - var actual = SendPolicyRequirement.Create([policyDetails]); - - Assert.Equal(shouldBeEnforced, actual.DisableSend); - } - - [Theory, AutoData] - public void DisableSend_Not_EnforcedAgainstProviders( - [PolicyDetails(PolicyType.DisableSend, isProvider: true)] PolicyDetails policyDetails) - { - var actual = SendPolicyRequirement.Create([policyDetails]); - - Assert.False(actual.DisableSend); - } - - [Theory] - [InlineAutoData(OrganizationUserStatusType.Confirmed, true)] - [InlineAutoData(OrganizationUserStatusType.Accepted, true)] - [InlineAutoData(OrganizationUserStatusType.Invited, false)] - [InlineAutoData(OrganizationUserStatusType.Revoked, false)] - public void DisableSend_TestStatuses( - OrganizationUserStatusType userStatus, - bool shouldBeEnforced, - [PolicyDetails(PolicyType.DisableSend)] PolicyDetails policyDetails) - { - policyDetails.OrganizationUserStatus = userStatus; - - var actual = SendPolicyRequirement.Create([policyDetails]); - - Assert.Equal(shouldBeEnforced, actual.DisableSend); - } - - [Theory, AutoData] - public void DisableHideEmail_IsFalse_IfNoSendOptionsPolicies( - [PolicyDetails(PolicyType.RequireSso)] PolicyDetails otherPolicy1, - [PolicyDetails(PolicyType.DisableSend)] PolicyDetails otherPolicy2) - { - var actual = SendPolicyRequirement.Create([otherPolicy1, otherPolicy2]); - - Assert.False(actual.DisableHideEmail); - } - - [Theory] - [InlineAutoData(OrganizationUserType.Owner, false)] - [InlineAutoData(OrganizationUserType.Admin, false)] - [InlineAutoData(OrganizationUserType.User, true)] - [InlineAutoData(OrganizationUserType.Custom, true)] - public void DisableHideEmail_TestRoles( - OrganizationUserType userType, - bool shouldBeEnforced, - [PolicyDetails(PolicyType.SendOptions)] PolicyDetails policyDetails) - { - EnableDisableHideEmail(policyDetails); - policyDetails.OrganizationUserType = userType; - - var actual = SendPolicyRequirement.Create([policyDetails]); - - Assert.Equal(shouldBeEnforced, actual.DisableHideEmail); - } - - [Theory, AutoData] - public void DisableHideEmail_Not_EnforcedAgainstProviders( - [PolicyDetails(PolicyType.SendOptions, isProvider: true)] PolicyDetails policyDetails) - { - EnableDisableHideEmail(policyDetails); - - var actual = SendPolicyRequirement.Create([policyDetails]); - - Assert.False(actual.DisableHideEmail); - } - - [Theory] - [InlineAutoData(OrganizationUserStatusType.Confirmed, true)] - [InlineAutoData(OrganizationUserStatusType.Accepted, true)] - [InlineAutoData(OrganizationUserStatusType.Invited, false)] - [InlineAutoData(OrganizationUserStatusType.Revoked, false)] - public void DisableHideEmail_TestStatuses( - OrganizationUserStatusType userStatus, - bool shouldBeEnforced, - [PolicyDetails(PolicyType.SendOptions)] PolicyDetails policyDetails) - { - EnableDisableHideEmail(policyDetails); - policyDetails.OrganizationUserStatus = userStatus; - - var actual = SendPolicyRequirement.Create([policyDetails]); - - Assert.Equal(shouldBeEnforced, actual.DisableHideEmail); - } - - [Theory, AutoData] - public void DisableHideEmail_HandlesNullData( - [PolicyDetails(PolicyType.SendOptions)] PolicyDetails policyDetails) - { - policyDetails.PolicyData = null; - - var actual = SendPolicyRequirement.Create([policyDetails]); - - Assert.False(actual.DisableHideEmail); - } - - private static void EnableDisableHideEmail(PolicyDetails policyDetails) - => policyDetails.SetDataModel(new SendOptionsPolicyData { DisableHideEmail = true }); -} diff --git a/test/Core.Test/Tools/Services/SendServiceTests.cs b/test/Core.Test/Tools/Services/SendServiceTests.cs index ae65ee1388..86d476340d 100644 --- a/test/Core.Test/Tools/Services/SendServiceTests.cs +++ b/test/Core.Test/Tools/Services/SendServiceTests.cs @@ -123,10 +123,12 @@ public class SendServiceTests // Disable Send policy check - vNext private void SaveSendAsync_Setup_vNext(SutProvider sutProvider, Send send, - SendPolicyRequirement sendPolicyRequirement) + DisableSendPolicyRequirement disableSendPolicyRequirement, SendOptionsPolicyRequirement sendOptionsPolicyRequirement) { - sutProvider.GetDependency().GetAsync(send.UserId!.Value) - .Returns(sendPolicyRequirement); + sutProvider.GetDependency().GetAsync(send.UserId!.Value) + .Returns(disableSendPolicyRequirement); + sutProvider.GetDependency().GetAsync(send.UserId!.Value) + .Returns(sendOptionsPolicyRequirement); sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.PolicyRequirements).Returns(true); // Should not be called in these tests @@ -141,7 +143,7 @@ public class SendServiceTests SutProvider sutProvider, [NewUserSendCustomize] Send send) { send.Type = sendType; - SaveSendAsync_Setup_vNext(sutProvider, send, new SendPolicyRequirement { DisableSend = true }); + SaveSendAsync_Setup_vNext(sutProvider, send, new DisableSendPolicyRequirement { DisableSend = true }, new SendOptionsPolicyRequirement()); var exception = await Assert.ThrowsAsync(() => sutProvider.Sut.SaveSendAsync(send)); Assert.Contains("Due to an Enterprise Policy, you are only able to delete an existing Send.", @@ -155,7 +157,7 @@ public class SendServiceTests SutProvider sutProvider, [NewUserSendCustomize] Send send) { send.Type = sendType; - SaveSendAsync_Setup_vNext(sutProvider, send, new SendPolicyRequirement()); + SaveSendAsync_Setup_vNext(sutProvider, send, new DisableSendPolicyRequirement(), new SendOptionsPolicyRequirement()); await sutProvider.Sut.SaveSendAsync(send); @@ -171,7 +173,7 @@ public class SendServiceTests SutProvider sutProvider, [NewUserSendCustomize] Send send) { send.Type = sendType; - SaveSendAsync_Setup_vNext(sutProvider, send, new SendPolicyRequirement { DisableHideEmail = true }); + SaveSendAsync_Setup_vNext(sutProvider, send, new DisableSendPolicyRequirement(), new SendOptionsPolicyRequirement { DisableHideEmail = true }); send.HideEmail = true; var exception = await Assert.ThrowsAsync(() => sutProvider.Sut.SaveSendAsync(send)); @@ -185,7 +187,7 @@ public class SendServiceTests SutProvider sutProvider, [NewUserSendCustomize] Send send) { send.Type = sendType; - SaveSendAsync_Setup_vNext(sutProvider, send, new SendPolicyRequirement { DisableHideEmail = true }); + SaveSendAsync_Setup_vNext(sutProvider, send, new DisableSendPolicyRequirement(), new SendOptionsPolicyRequirement { DisableHideEmail = true }); send.HideEmail = false; await sutProvider.Sut.SaveSendAsync(send); @@ -200,7 +202,7 @@ public class SendServiceTests SutProvider sutProvider, [NewUserSendCustomize] Send send) { send.Type = sendType; - SaveSendAsync_Setup_vNext(sutProvider, send, new SendPolicyRequirement()); + SaveSendAsync_Setup_vNext(sutProvider, send, new DisableSendPolicyRequirement(), new SendOptionsPolicyRequirement()); send.HideEmail = true; await sutProvider.Sut.SaveSendAsync(send);