diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SendPolicyRequirement.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SendPolicyRequirement.cs new file mode 100644 index 0000000000..c54cc98373 --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SendPolicyRequirement.cs @@ -0,0 +1,54 @@ +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 f7b35f2f06..7bc8a7b5a3 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyServiceCollectionExtensions.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyServiceCollectionExtensions.cs @@ -32,6 +32,7 @@ public static class PolicyServiceCollectionExtensions private static void AddPolicyRequirements(this IServiceCollection services) { // Register policy requirement factories here + services.AddPolicyRequirement(SendPolicyRequirement.Create); } /// diff --git a/src/Core/Tools/Services/Implementations/SendService.cs b/src/Core/Tools/Services/Implementations/SendService.cs index 918379d7a5..bddaa93bfc 100644 --- a/src/Core/Tools/Services/Implementations/SendService.cs +++ b/src/Core/Tools/Services/Implementations/SendService.cs @@ -1,7 +1,8 @@ using System.Text.Json; using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; -using Bit.Core.AdminConsole.Repositories; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; using Bit.Core.AdminConsole.Services; using Bit.Core.Context; using Bit.Core.Entities; @@ -26,7 +27,6 @@ public class SendService : ISendService public const string MAX_FILE_SIZE_READABLE = "500 MB"; private readonly ISendRepository _sendRepository; private readonly IUserRepository _userRepository; - private readonly IPolicyRepository _policyRepository; private readonly IPolicyService _policyService; private readonly IUserService _userService; private readonly IOrganizationRepository _organizationRepository; @@ -36,6 +36,9 @@ public class SendService : ISendService private readonly IReferenceEventService _referenceEventService; private readonly GlobalSettings _globalSettings; private readonly ICurrentContext _currentContext; + private readonly IPolicyRequirementQuery _policyRequirementQuery; + private readonly IFeatureService _featureService; + private const long _fileSizeLeeway = 1024L * 1024L; // 1MB public SendService( @@ -48,14 +51,14 @@ public class SendService : ISendService IPushNotificationService pushService, IReferenceEventService referenceEventService, GlobalSettings globalSettings, - IPolicyRepository policyRepository, IPolicyService policyService, - ICurrentContext currentContext) + ICurrentContext currentContext, + IPolicyRequirementQuery policyRequirementQuery, + IFeatureService featureService) { _sendRepository = sendRepository; _userRepository = userRepository; _userService = userService; - _policyRepository = policyRepository; _policyService = policyService; _organizationRepository = organizationRepository; _sendFileStorageService = sendFileStorageService; @@ -64,6 +67,8 @@ public class SendService : ISendService _referenceEventService = referenceEventService; _globalSettings = globalSettings; _currentContext = currentContext; + _policyRequirementQuery = policyRequirementQuery; + _featureService = featureService; } public async Task SaveSendAsync(Send send) @@ -286,6 +291,12 @@ public class SendService : ISendService private async Task ValidateUserCanSaveAsync(Guid? userId, Send send) { + if (_featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements)) + { + await ValidateUserCanSaveAsync_vNext(userId, send); + return; + } + if (!userId.HasValue || (!_currentContext.Organizations?.Any() ?? true)) { return; @@ -308,6 +319,26 @@ public class SendService : ISendService } } + private async Task ValidateUserCanSaveAsync_vNext(Guid? userId, Send send) + { + if (!userId.HasValue) + { + return; + } + + var sendPolicyRequirement = await _policyRequirementQuery.GetAsync(userId.Value); + + if (sendPolicyRequirement.DisableSend) + { + throw new BadRequestException("Due to an Enterprise Policy, you are only able to delete an existing Send."); + } + + if (sendPolicyRequirement.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."); + } + } + private async Task StorageRemainingForSendAsync(Send send) { var storageBytesRemaining = 0L; diff --git a/test/Core.Test/AdminConsole/AutoFixture/PolicyDetailsFixtures.cs b/test/Core.Test/AdminConsole/AutoFixture/PolicyDetailsFixtures.cs new file mode 100644 index 0000000000..87ea390cb6 --- /dev/null +++ b/test/Core.Test/AdminConsole/AutoFixture/PolicyDetailsFixtures.cs @@ -0,0 +1,35 @@ +using System.Reflection; +using AutoFixture; +using AutoFixture.Xunit2; +using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; +using Bit.Core.Enums; + +namespace Bit.Core.Test.AdminConsole.AutoFixture; + +internal class PolicyDetailsCustomization( + PolicyType policyType, + OrganizationUserType userType, + bool isProvider, + OrganizationUserStatusType userStatus) : ICustomization +{ + public void Customize(IFixture fixture) + { + fixture.Customize(composer => composer + .With(o => o.PolicyType, policyType) + .With(o => o.OrganizationUserType, userType) + .With(o => o.IsProvider, isProvider) + .With(o => o.OrganizationUserStatus, userStatus) + .Without(o => o.PolicyData)); // avoid autogenerating invalid json data + } +} + +public class PolicyDetailsAttribute( + PolicyType policyType, + OrganizationUserType userType = OrganizationUserType.User, + bool isProvider = false, + OrganizationUserStatusType userStatus = OrganizationUserStatusType.Confirmed) : CustomizeAttribute +{ + public override ICustomization GetCustomization(ParameterInfo parameter) + => new PolicyDetailsCustomization(policyType, userType, isProvider, userStatus); +} diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/PolicyDetailsTestExtensions.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/PolicyDetailsTestExtensions.cs new file mode 100644 index 0000000000..3323c9c754 --- /dev/null +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/PolicyDetailsTestExtensions.cs @@ -0,0 +1,10 @@ +using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; +using Bit.Core.Utilities; + +namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; + +public static class PolicyDetailsTestExtensions +{ + public static void SetDataModel(this PolicyDetails policyDetails, T data) where T : IPolicyDataModel + => policyDetails.PolicyData = CoreHelpers.ClassToJsonData(data); +} diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SendPolicyRequirementTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SendPolicyRequirementTests.cs new file mode 100644 index 0000000000..4d7bf5db4e --- /dev/null +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SendPolicyRequirementTests.cs @@ -0,0 +1,138 @@ +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/AutoFixture/SendFixtures.cs b/test/Core.Test/Tools/AutoFixture/SendFixtures.cs index c8005f4faf..0d58ca1671 100644 --- a/test/Core.Test/Tools/AutoFixture/SendFixtures.cs +++ b/test/Core.Test/Tools/AutoFixture/SendFixtures.cs @@ -1,4 +1,6 @@ -using AutoFixture; +using System.Reflection; +using AutoFixture; +using AutoFixture.Xunit2; using Bit.Core.Tools.Entities; using Bit.Test.Common.AutoFixture.Attributes; @@ -19,3 +21,20 @@ internal class UserSendCustomizeAttribute : BitCustomizeAttribute { public override ICustomization GetCustomization() => new UserSend(); } + +internal class NewUserSend : ICustomization +{ + public void Customize(IFixture fixture) + { + fixture.Customize(composer => composer + .With(s => s.Id, Guid.Empty) + .Without(s => s.OrganizationId)); + } +} + +internal class NewUserSendCustomizeAttribute : CustomizeAttribute +{ + public override ICustomization GetCustomization(ParameterInfo parameterInfo) + => new NewUserSend(); +} + diff --git a/test/Core.Test/Tools/Services/SendServiceTests.cs b/test/Core.Test/Tools/Services/SendServiceTests.cs index cabb438b61..ae65ee1388 100644 --- a/test/Core.Test/Tools/Services/SendServiceTests.cs +++ b/test/Core.Test/Tools/Services/SendServiceTests.cs @@ -3,6 +3,8 @@ using System.Text.Json; using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; using Bit.Core.AdminConsole.Services; using Bit.Core.Entities; using Bit.Core.Exceptions; @@ -22,6 +24,7 @@ using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using Microsoft.AspNetCore.Identity; using NSubstitute; +using NSubstitute.ExceptionExtensions; using Xunit; using GlobalSettings = Bit.Core.Settings.GlobalSettings; @@ -118,6 +121,93 @@ public class SendServiceTests await sutProvider.GetDependency().Received(1).CreateAsync(send); } + // Disable Send policy check - vNext + private void SaveSendAsync_Setup_vNext(SutProvider sutProvider, Send send, + SendPolicyRequirement sendPolicyRequirement) + { + sutProvider.GetDependency().GetAsync(send.UserId!.Value) + .Returns(sendPolicyRequirement); + sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.PolicyRequirements).Returns(true); + + // Should not be called in these tests + sutProvider.GetDependency().AnyPoliciesApplicableToUserAsync( + Arg.Any(), Arg.Any()).ThrowsAsync(); + } + + [Theory] + [BitAutoData(SendType.File)] + [BitAutoData(SendType.Text)] + public async Task SaveSendAsync_DisableSend_Applies_Throws_vNext(SendType sendType, + SutProvider sutProvider, [NewUserSendCustomize] Send send) + { + send.Type = sendType; + SaveSendAsync_Setup_vNext(sutProvider, send, new SendPolicyRequirement { DisableSend = true }); + + 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.", + exception.Message); + } + + [Theory] + [BitAutoData(SendType.File)] + [BitAutoData(SendType.Text)] + public async Task SaveSendAsync_DisableSend_DoesntApply_Success_vNext(SendType sendType, + SutProvider sutProvider, [NewUserSendCustomize] Send send) + { + send.Type = sendType; + SaveSendAsync_Setup_vNext(sutProvider, send, new SendPolicyRequirement()); + + await sutProvider.Sut.SaveSendAsync(send); + + await sutProvider.GetDependency().Received(1).CreateAsync(send); + } + + // Send Options Policy - Disable Hide Email check + + [Theory] + [BitAutoData(SendType.File)] + [BitAutoData(SendType.Text)] + public async Task SaveSendAsync_DisableHideEmail_Applies_Throws_vNext(SendType sendType, + SutProvider sutProvider, [NewUserSendCustomize] Send send) + { + send.Type = sendType; + SaveSendAsync_Setup_vNext(sutProvider, send, new SendPolicyRequirement { DisableHideEmail = true }); + send.HideEmail = true; + + var exception = await Assert.ThrowsAsync(() => sutProvider.Sut.SaveSendAsync(send)); + Assert.Contains("Due to an Enterprise Policy, you are not allowed to hide your email address from recipients when creating or editing a Send.", exception.Message); + } + + [Theory] + [BitAutoData(SendType.File)] + [BitAutoData(SendType.Text)] + public async Task SaveSendAsync_DisableHideEmail_Applies_ButEmailNotHidden_Success_vNext(SendType sendType, + SutProvider sutProvider, [NewUserSendCustomize] Send send) + { + send.Type = sendType; + SaveSendAsync_Setup_vNext(sutProvider, send, new SendPolicyRequirement { DisableHideEmail = true }); + send.HideEmail = false; + + await sutProvider.Sut.SaveSendAsync(send); + + await sutProvider.GetDependency().Received(1).CreateAsync(send); + } + + [Theory] + [BitAutoData(SendType.File)] + [BitAutoData(SendType.Text)] + public async Task SaveSendAsync_DisableHideEmail_DoesntApply_Success_vNext(SendType sendType, + SutProvider sutProvider, [NewUserSendCustomize] Send send) + { + send.Type = sendType; + SaveSendAsync_Setup_vNext(sutProvider, send, new SendPolicyRequirement()); + send.HideEmail = true; + + await sutProvider.Sut.SaveSendAsync(send); + + await sutProvider.GetDependency().Received(1).CreateAsync(send); + } + [Theory] [BitAutoData] public async Task SaveSendAsync_ExistingSend_Updates(SutProvider sutProvider,