1
0
mirror of https://github.com/bitwarden/server.git synced 2025-04-05 05:00:19 -05:00

[PM-18234] Add SendPolicyRequirement (#5409)

This commit is contained in:
Thomas Rittson 2025-02-24 09:19:52 +10:00 committed by GitHub
parent 5241e09c1a
commit b0c6fc9146
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 384 additions and 6 deletions

View File

@ -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;
/// <summary>
/// Policy requirements for the Disable Send and Send Options policies.
/// </summary>
public class SendPolicyRequirement : IPolicyRequirement
{
/// <summary>
/// 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.
/// </summary>
public bool DisableSend { get; init; }
/// <summary>
/// Indicates whether the user is prohibited from hiding their email from the recipient of a Send.
/// </summary>
public bool DisableHideEmail { get; init; }
/// <summary>
/// Create a new SendPolicyRequirement.
/// </summary>
/// <param name="policyDetails">All PolicyDetails relating to the user.</param>
/// <remarks>
/// This is a <see cref="RequirementFactory{T}"/> for the SendPolicyRequirement.
/// </remarks>
public static SendPolicyRequirement Create(IEnumerable<PolicyDetails> 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<SendOptionsPolicyData>())
.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;
}
}

View File

@ -32,6 +32,7 @@ public static class PolicyServiceCollectionExtensions
private static void AddPolicyRequirements(this IServiceCollection services) private static void AddPolicyRequirements(this IServiceCollection services)
{ {
// Register policy requirement factories here // Register policy requirement factories here
services.AddPolicyRequirement(SendPolicyRequirement.Create);
} }
/// <summary> /// <summary>

View File

@ -1,7 +1,8 @@
using System.Text.Json; using System.Text.Json;
using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; 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.AdminConsole.Services;
using Bit.Core.Context; using Bit.Core.Context;
using Bit.Core.Entities; using Bit.Core.Entities;
@ -26,7 +27,6 @@ public class SendService : ISendService
public const string MAX_FILE_SIZE_READABLE = "500 MB"; public const string MAX_FILE_SIZE_READABLE = "500 MB";
private readonly ISendRepository _sendRepository; private readonly ISendRepository _sendRepository;
private readonly IUserRepository _userRepository; private readonly IUserRepository _userRepository;
private readonly IPolicyRepository _policyRepository;
private readonly IPolicyService _policyService; private readonly IPolicyService _policyService;
private readonly IUserService _userService; private readonly IUserService _userService;
private readonly IOrganizationRepository _organizationRepository; private readonly IOrganizationRepository _organizationRepository;
@ -36,6 +36,9 @@ public class SendService : ISendService
private readonly IReferenceEventService _referenceEventService; private readonly IReferenceEventService _referenceEventService;
private readonly GlobalSettings _globalSettings; private readonly GlobalSettings _globalSettings;
private readonly ICurrentContext _currentContext; private readonly ICurrentContext _currentContext;
private readonly IPolicyRequirementQuery _policyRequirementQuery;
private readonly IFeatureService _featureService;
private const long _fileSizeLeeway = 1024L * 1024L; // 1MB private const long _fileSizeLeeway = 1024L * 1024L; // 1MB
public SendService( public SendService(
@ -48,14 +51,14 @@ public class SendService : ISendService
IPushNotificationService pushService, IPushNotificationService pushService,
IReferenceEventService referenceEventService, IReferenceEventService referenceEventService,
GlobalSettings globalSettings, GlobalSettings globalSettings,
IPolicyRepository policyRepository,
IPolicyService policyService, IPolicyService policyService,
ICurrentContext currentContext) ICurrentContext currentContext,
IPolicyRequirementQuery policyRequirementQuery,
IFeatureService featureService)
{ {
_sendRepository = sendRepository; _sendRepository = sendRepository;
_userRepository = userRepository; _userRepository = userRepository;
_userService = userService; _userService = userService;
_policyRepository = policyRepository;
_policyService = policyService; _policyService = policyService;
_organizationRepository = organizationRepository; _organizationRepository = organizationRepository;
_sendFileStorageService = sendFileStorageService; _sendFileStorageService = sendFileStorageService;
@ -64,6 +67,8 @@ public class SendService : ISendService
_referenceEventService = referenceEventService; _referenceEventService = referenceEventService;
_globalSettings = globalSettings; _globalSettings = globalSettings;
_currentContext = currentContext; _currentContext = currentContext;
_policyRequirementQuery = policyRequirementQuery;
_featureService = featureService;
} }
public async Task SaveSendAsync(Send send) public async Task SaveSendAsync(Send send)
@ -286,6 +291,12 @@ public class SendService : ISendService
private async Task ValidateUserCanSaveAsync(Guid? userId, Send send) 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)) if (!userId.HasValue || (!_currentContext.Organizations?.Any() ?? true))
{ {
return; 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<SendPolicyRequirement>(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<long> StorageRemainingForSendAsync(Send send) private async Task<long> StorageRemainingForSendAsync(Send send)
{ {
var storageBytesRemaining = 0L; var storageBytesRemaining = 0L;

View File

@ -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<PolicyDetails>(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);
}

View File

@ -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<T>(this PolicyDetails policyDetails, T data) where T : IPolicyDataModel
=> policyDetails.PolicyData = CoreHelpers.ClassToJsonData(data);
}

View File

@ -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 });
}

View File

@ -1,4 +1,6 @@
using AutoFixture; using System.Reflection;
using AutoFixture;
using AutoFixture.Xunit2;
using Bit.Core.Tools.Entities; using Bit.Core.Tools.Entities;
using Bit.Test.Common.AutoFixture.Attributes; using Bit.Test.Common.AutoFixture.Attributes;
@ -19,3 +21,20 @@ internal class UserSendCustomizeAttribute : BitCustomizeAttribute
{ {
public override ICustomization GetCustomization() => new UserSend(); public override ICustomization GetCustomization() => new UserSend();
} }
internal class NewUserSend : ICustomization
{
public void Customize(IFixture fixture)
{
fixture.Customize<Send>(composer => composer
.With(s => s.Id, Guid.Empty)
.Without(s => s.OrganizationId));
}
}
internal class NewUserSendCustomizeAttribute : CustomizeAttribute
{
public override ICustomization GetCustomization(ParameterInfo parameterInfo)
=> new NewUserSend();
}

View File

@ -3,6 +3,8 @@ using System.Text.Json;
using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; 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.AdminConsole.Services;
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
@ -22,6 +24,7 @@ using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes; using Bit.Test.Common.AutoFixture.Attributes;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
using NSubstitute; using NSubstitute;
using NSubstitute.ExceptionExtensions;
using Xunit; using Xunit;
using GlobalSettings = Bit.Core.Settings.GlobalSettings; using GlobalSettings = Bit.Core.Settings.GlobalSettings;
@ -118,6 +121,93 @@ public class SendServiceTests
await sutProvider.GetDependency<ISendRepository>().Received(1).CreateAsync(send); await sutProvider.GetDependency<ISendRepository>().Received(1).CreateAsync(send);
} }
// Disable Send policy check - vNext
private void SaveSendAsync_Setup_vNext(SutProvider<SendService> sutProvider, Send send,
SendPolicyRequirement sendPolicyRequirement)
{
sutProvider.GetDependency<IPolicyRequirementQuery>().GetAsync<SendPolicyRequirement>(send.UserId!.Value)
.Returns(sendPolicyRequirement);
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.PolicyRequirements).Returns(true);
// Should not be called in these tests
sutProvider.GetDependency<IPolicyService>().AnyPoliciesApplicableToUserAsync(
Arg.Any<Guid>(), Arg.Any<PolicyType>()).ThrowsAsync<Exception>();
}
[Theory]
[BitAutoData(SendType.File)]
[BitAutoData(SendType.Text)]
public async Task SaveSendAsync_DisableSend_Applies_Throws_vNext(SendType sendType,
SutProvider<SendService> sutProvider, [NewUserSendCustomize] Send send)
{
send.Type = sendType;
SaveSendAsync_Setup_vNext(sutProvider, send, new SendPolicyRequirement { DisableSend = true });
var exception = await Assert.ThrowsAsync<BadRequestException>(() => 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<SendService> sutProvider, [NewUserSendCustomize] Send send)
{
send.Type = sendType;
SaveSendAsync_Setup_vNext(sutProvider, send, new SendPolicyRequirement());
await sutProvider.Sut.SaveSendAsync(send);
await sutProvider.GetDependency<ISendRepository>().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<SendService> sutProvider, [NewUserSendCustomize] Send send)
{
send.Type = sendType;
SaveSendAsync_Setup_vNext(sutProvider, send, new SendPolicyRequirement { DisableHideEmail = true });
send.HideEmail = true;
var exception = await Assert.ThrowsAsync<BadRequestException>(() => 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<SendService> 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<ISendRepository>().Received(1).CreateAsync(send);
}
[Theory]
[BitAutoData(SendType.File)]
[BitAutoData(SendType.Text)]
public async Task SaveSendAsync_DisableHideEmail_DoesntApply_Success_vNext(SendType sendType,
SutProvider<SendService> 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<ISendRepository>().Received(1).CreateAsync(send);
}
[Theory] [Theory]
[BitAutoData] [BitAutoData]
public async Task SaveSendAsync_ExistingSend_Updates(SutProvider<SendService> sutProvider, public async Task SaveSendAsync_ExistingSend_Updates(SutProvider<SendService> sutProvider,