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,