From 7cd289ecab34a9f9a33e8908cfb31932cbc0078d Mon Sep 17 00:00:00 2001 From: John Harrington <84741727+harr1424@users.noreply.github.com> Date: Tue, 24 Feb 2026 12:58:38 -0700 Subject: [PATCH 1/2] initial work to consolidate Send Policies --- src/Core/AdminConsole/Enums/PolicyType.cs | 3 +- ...OptionsPolicyData.cs => SendPolicyData.cs} | 5 +- .../DisableSendPolicyRequirement.cs | 27 ------ .../SendOptionsPolicyRequirement.cs | 34 -------- .../SendPolicyRequirement.cs | 39 +++++++++ .../PolicyServiceCollectionExtensions.cs | 3 +- .../Utilities/PolicyDataValidator.cs | 2 +- .../Services/SendValidationService.cs | 10 +-- ...isableSendPolicyRequirementFactoryTests.cs | 32 ------- ...endOptionsPolicyRequirementFactoryTests.cs | 49 ----------- .../SendPolicyRequirementFactoryTests.cs | 85 +++++++++++++++++++ ..._ConsolidateDisableSendIntoSendOptions.sql | 42 +++++++++ .../2026-02-FinalizationMigration.sql | 11 +++ 13 files changed, 190 insertions(+), 152 deletions(-) rename src/Core/AdminConsole/Models/Data/Organizations/Policies/{SendOptionsPolicyData.cs => SendPolicyData.cs} (62%) delete mode 100644 src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/DisableSendPolicyRequirement.cs delete mode 100644 src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SendOptionsPolicyRequirement.cs create mode 100644 src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SendPolicyRequirement.cs delete mode 100644 test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/DisableSendPolicyRequirementFactoryTests.cs delete mode 100644 test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SendOptionsPolicyRequirementFactoryTests.cs create mode 100644 test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SendPolicyRequirementFactoryTests.cs create mode 100644 util/Migrator/DbScripts/2026-02-24_00_ConsolidateDisableSendIntoSendOptions.sql create mode 100644 util/Migrator/DbScripts_finalization/2026-02-FinalizationMigration.sql diff --git a/src/Core/AdminConsole/Enums/PolicyType.cs b/src/Core/AdminConsole/Enums/PolicyType.cs index bd6daf7cdffa..d60af0b01979 100644 --- a/src/Core/AdminConsole/Enums/PolicyType.cs +++ b/src/Core/AdminConsole/Enums/PolicyType.cs @@ -8,6 +8,7 @@ public enum PolicyType : byte SingleOrg = 3, RequireSso = 4, OrganizationDataOwnership = 5, + [Obsolete("Consolidated into SendOptions (type 7). Use SendOptions policy with SendPolicyData.DisableSend.")] DisableSend = 6, SendOptions = 7, ResetPassword = 8, @@ -41,7 +42,7 @@ public static string GetName(this PolicyType type) PolicyType.RequireSso => "Require single sign-on authentication", PolicyType.OrganizationDataOwnership => "Enforce organization data ownership", PolicyType.DisableSend => "Remove Send", - PolicyType.SendOptions => "Send options", + PolicyType.SendOptions => "Send", PolicyType.ResetPassword => "Account recovery administration", PolicyType.MaximumVaultTimeout => "Vault timeout", PolicyType.DisablePersonalVaultExport => "Remove individual vault export", diff --git a/src/Core/AdminConsole/Models/Data/Organizations/Policies/SendOptionsPolicyData.cs b/src/Core/AdminConsole/Models/Data/Organizations/Policies/SendPolicyData.cs similarity index 62% rename from src/Core/AdminConsole/Models/Data/Organizations/Policies/SendOptionsPolicyData.cs rename to src/Core/AdminConsole/Models/Data/Organizations/Policies/SendPolicyData.cs index 57a8544b40b0..7e4046b9e82f 100644 --- a/src/Core/AdminConsole/Models/Data/Organizations/Policies/SendOptionsPolicyData.cs +++ b/src/Core/AdminConsole/Models/Data/Organizations/Policies/SendPolicyData.cs @@ -2,8 +2,11 @@ namespace Bit.Core.AdminConsole.Models.Data.Organizations.Policies; -public class SendOptionsPolicyData : IPolicyDataModel +public class SendPolicyData : IPolicyDataModel { + [Display(Name = "DisableSend")] + public bool DisableSend { get; set; } + [Display(Name = "DisableHideEmail")] public bool DisableHideEmail { get; set; } } diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/DisableSendPolicyRequirement.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/DisableSendPolicyRequirement.cs deleted file mode 100644 index 1cb7f4f6196b..000000000000 --- a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/DisableSendPolicyRequirement.cs +++ /dev/null @@ -1,27 +0,0 @@ -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/SendOptionsPolicyRequirement.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SendOptionsPolicyRequirement.cs deleted file mode 100644 index 9ba11c11df16..000000000000 --- a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SendOptionsPolicyRequirement.cs +++ /dev/null @@ -1,34 +0,0 @@ -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 new file mode 100644 index 000000000000..0aa973f8584e --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SendPolicyRequirement.cs @@ -0,0 +1,39 @@ +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 policy. +/// +public class SendPolicyRequirement : IPolicyRequirement +{ + /// + /// Indicates whether Send is disabled for the org. If true, the org users should not be able to create or edit Sends. + /// They may still delete existing Sends. + /// + public bool DisableSend { get; init; } + + /// + /// Indicates whether the org users are prohibited from hiding their email from the recipient of a Send. + /// + public bool DisableHideEmail { get; init; } +} + +public class SendPolicyRequirementFactory : BasePolicyRequirementFactory +{ + public override PolicyType PolicyType => PolicyType.SendOptions; + + public override SendPolicyRequirement Create(IEnumerable policyDetails) + { + return policyDetails + .Select(p => p.GetDataModel()) + .Aggregate( + new SendPolicyRequirement(), + (result, data) => new SendPolicyRequirement + { + DisableSend = result.DisableSend || data.DisableSend, + DisableHideEmail = result.DisableHideEmail || data.DisableHideEmail + }); + } +} diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyServiceCollectionExtensions.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyServiceCollectionExtensions.cs index a7657dc71477..7b48bd23d4be 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyServiceCollectionExtensions.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyServiceCollectionExtensions.cs @@ -67,8 +67,7 @@ private static void AddPolicyUpdateEvents(this IServiceCollection services) private static void AddPolicyRequirements(this IServiceCollection services) { - services.AddScoped, DisableSendPolicyRequirementFactory>(); - services.AddScoped, SendOptionsPolicyRequirementFactory>(); + services.AddScoped, SendPolicyRequirementFactory>(); services.AddScoped, ResetPasswordPolicyRequirementFactory>(); services.AddScoped, OrganizationDataOwnershipPolicyRequirementFactory>(); services.AddScoped, RequireSsoPolicyRequirementFactory>(); diff --git a/src/Core/AdminConsole/Utilities/PolicyDataValidator.cs b/src/Core/AdminConsole/Utilities/PolicyDataValidator.cs index d533ca88cf8b..66b4db5866db 100644 --- a/src/Core/AdminConsole/Utilities/PolicyDataValidator.cs +++ b/src/Core/AdminConsole/Utilities/PolicyDataValidator.cs @@ -35,7 +35,7 @@ public static class PolicyDataValidator ValidateModel(masterPasswordData, policyType); break; case PolicyType.SendOptions: - CoreHelpers.LoadClassFromJsonData(json); + CoreHelpers.LoadClassFromJsonData(json); break; case PolicyType.ResetPassword: CoreHelpers.LoadClassFromJsonData(json); diff --git a/src/Core/Tools/SendFeatures/Services/SendValidationService.cs b/src/Core/Tools/SendFeatures/Services/SendValidationService.cs index bd987bb396bc..a951d6233367 100644 --- a/src/Core/Tools/SendFeatures/Services/SendValidationService.cs +++ b/src/Core/Tools/SendFeatures/Services/SendValidationService.cs @@ -77,7 +77,7 @@ public async Task ValidateUserCanSaveAsync(Guid? userId, Send send) if (send.HideEmail.GetValueOrDefault()) { var sendOptionsPolicies = await _policyService.GetPoliciesApplicableToUserAsync(userId.Value, PolicyType.SendOptions); - if (sendOptionsPolicies.Any(p => CoreHelpers.LoadClassFromJsonData(p.PolicyData)?.DisableHideEmail ?? false)) + if (sendOptionsPolicies.Any(p => CoreHelpers.LoadClassFromJsonData(p.PolicyData)?.DisableHideEmail ?? false)) { 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."); } @@ -91,14 +91,14 @@ public async Task ValidateUserCanSaveAsync_vNext(Guid? userId, Send send) return; } - var disableSendRequirement = await _policyRequirementQuery.GetAsync(userId.Value); - if (disableSendRequirement.DisableSend) + var sendRequirement = await _policyRequirementQuery.GetAsync(userId.Value); + + if (sendRequirement.DisableSend) { throw new BadRequestException("Due to an Enterprise Policy, you are only able to delete an existing Send."); } - var sendOptionsRequirement = await _policyRequirementQuery.GetAsync(userId.Value); - if (sendOptionsRequirement.DisableHideEmail && send.HideEmail.GetValueOrDefault()) + if (sendRequirement.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/PolicyRequirements/DisableSendPolicyRequirementFactoryTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/DisableSendPolicyRequirementFactoryTests.cs deleted file mode 100644 index 2304c0e9ae2e..000000000000 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/DisableSendPolicyRequirementFactoryTests.cs +++ /dev/null @@ -1,32 +0,0 @@ -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 deleted file mode 100644 index af66d858ef46..000000000000 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SendOptionsPolicyRequirementFactoryTests.cs +++ /dev/null @@ -1,49 +0,0 @@ -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/SendPolicyRequirementFactoryTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SendPolicyRequirementFactoryTests.cs new file mode 100644 index 000000000000..8afbdc94e7d6 --- /dev/null +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SendPolicyRequirementFactoryTests.cs @@ -0,0 +1,85 @@ +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 SendPolicyRequirementFactoryTests +{ + [Theory, BitAutoData] + public void DisableSend_IsFalse_IfNoPolicies(SutProvider sutProvider) + { + var actual = sutProvider.Sut.Create([]); + + Assert.False(actual.DisableSend); + } + + [Theory, BitAutoData] + public void DisableSend_IsFalse_IfNotConfigured( + [PolicyDetails(PolicyType.SendOptions)] PolicyDetails[] policies, + SutProvider sutProvider + ) + { + policies[0].SetDataModel(new SendPolicyData { DisableSend = false }); + policies[1].SetDataModel(new SendPolicyData { DisableSend = false }); + + var actual = sutProvider.Sut.Create(policies); + + Assert.False(actual.DisableSend); + } + + [Theory, BitAutoData] + public void DisableSend_IsTrue_IfAnyConfigured( + [PolicyDetails(PolicyType.SendOptions)] PolicyDetails[] policies, + SutProvider sutProvider + ) + { + policies[0].SetDataModel(new SendPolicyData { DisableSend = true }); + policies[1].SetDataModel(new SendPolicyData { DisableSend = false }); + + var actual = sutProvider.Sut.Create(policies); + + Assert.True(actual.DisableSend); + } + + [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 SendPolicyData { DisableHideEmail = false }); + policies[1].SetDataModel(new SendPolicyData { 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 SendPolicyData { DisableHideEmail = true }); + policies[1].SetDataModel(new SendPolicyData { DisableHideEmail = false }); + + var actual = sutProvider.Sut.Create(policies); + + Assert.True(actual.DisableHideEmail); + } +} diff --git a/util/Migrator/DbScripts/2026-02-24_00_ConsolidateDisableSendIntoSendOptions.sql b/util/Migrator/DbScripts/2026-02-24_00_ConsolidateDisableSendIntoSendOptions.sql new file mode 100644 index 000000000000..1fb9cc0823ff --- /dev/null +++ b/util/Migrator/DbScripts/2026-02-24_00_ConsolidateDisableSendIntoSendOptions.sql @@ -0,0 +1,42 @@ +-- Consolidate DisableSend (type 6) policies into SendOptions (type 7) policies. +-- This is the Phase 1 (initial) migration: it populates SendOptions rows with +-- disableSend=true before the new code is deployed, without removing type 6 rows. +-- Type 6 rows are intentionally left in place so that a rollback to the previous +-- release continues to enforce the policy correctly. +-- The DELETE of type 6 rows is a breaking change deferred to DbScripts_finalization. + +-- Step 1: For orgs that have DisableSend (type 6) enabled AND already have a +-- SendOptions (type 7) row, merge disableSend=true into the existing JSON data. +UPDATE [dbo].[Policy] +SET [Data] = JSON_MODIFY(ISNULL([Data], '{}'), '$.disableSend', CAST(1 AS BIT)), + [RevisionDate] = GETUTCDATE() +WHERE [Type] = 7 + AND [OrganizationId] IN ( + SELECT [OrganizationId] + FROM [dbo].[Policy] + WHERE [Type] = 6 + AND [Enabled] = 1 + ); +GO + +-- Step 2: For orgs that have DisableSend (type 6) enabled but NO SendOptions (type 7) +-- row yet, insert a new enabled SendOptions row with disableSend=true. +INSERT INTO [dbo].[Policy] ([Id], [OrganizationId], [Type], [Data], [Enabled], [CreationDate], [RevisionDate]) +SELECT + NEWID(), + ds.[OrganizationId], + 7, + '{"disableSend":true}', + 1, + GETUTCDATE(), + GETUTCDATE() +FROM [dbo].[Policy] ds +WHERE ds.[Type] = 6 + AND ds.[Enabled] = 1 + AND NOT EXISTS ( + SELECT 1 + FROM [dbo].[Policy] so + WHERE so.[OrganizationId] = ds.[OrganizationId] + AND so.[Type] = 7 + ); +GO diff --git a/util/Migrator/DbScripts_finalization/2026-02-FinalizationMigration.sql b/util/Migrator/DbScripts_finalization/2026-02-FinalizationMigration.sql new file mode 100644 index 000000000000..84d52cdc3386 --- /dev/null +++ b/util/Migrator/DbScripts_finalization/2026-02-FinalizationMigration.sql @@ -0,0 +1,11 @@ +-- Remove all DisableSend (type 6) policy rows. +-- These were consolidated into SendOptions (type 7) rows with disableSend=true +-- in the Phase 1 migration (2026-02-24_00_ConsolidateDisableSendIntoSendOptions.sql). +-- This finalization runs during the next release deployment once no rollback to the +-- previous release is possible, making the removal of type 6 rows safe. + +-- Move this file to DbScripts/ as part of the next release. + +DELETE FROM [dbo].[Policy] +WHERE [Type] = 6; +GO From aea6b31f5e0f236cecbed293c93bc6f5a628f82e Mon Sep 17 00:00:00 2001 From: John Harrington <84741727+harr1424@users.noreply.github.com> Date: Wed, 25 Feb 2026 11:10:38 -0700 Subject: [PATCH 2/2] add Send policies related to access restrictions --- .../Organizations/Policies/SendPolicyData.cs | 9 + .../SendPolicyRequirement.cs | 31 ++- .../Services/SendValidationService.cs | 54 +++- .../SendPolicyRequirementFactoryTests.cs | 136 +++++++++ .../Services/SendValidationServiceTests.cs | 263 ++++++++++++++++++ 5 files changed, 484 insertions(+), 9 deletions(-) diff --git a/src/Core/AdminConsole/Models/Data/Organizations/Policies/SendPolicyData.cs b/src/Core/AdminConsole/Models/Data/Organizations/Policies/SendPolicyData.cs index 7e4046b9e82f..859cd9939e8a 100644 --- a/src/Core/AdminConsole/Models/Data/Organizations/Policies/SendPolicyData.cs +++ b/src/Core/AdminConsole/Models/Data/Organizations/Policies/SendPolicyData.cs @@ -9,4 +9,13 @@ public class SendPolicyData : IPolicyDataModel [Display(Name = "DisableHideEmail")] public bool DisableHideEmail { get; set; } + + [Display(Name = "DisableNoAuthSends")] + public bool DisableNoAuthSends { get; set; } + + [Display(Name = "DisablePasswordSends")] + public bool DisablePasswordSends { get; set; } + + [Display(Name = "DisableEmailVerifiedSends")] + public bool DisableEmailVerifiedSends { get; set; } } diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SendPolicyRequirement.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SendPolicyRequirement.cs index 0aa973f8584e..99f6fde4efec 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SendPolicyRequirement.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SendPolicyRequirement.cs @@ -18,6 +18,21 @@ public class SendPolicyRequirement : IPolicyRequirement /// Indicates whether the org users are prohibited from hiding their email from the recipient of a Send. /// public bool DisableHideEmail { get; init; } + + /// + /// Indicates whether the org users are prohibited from creating or editing Sends that use no authorization. + /// + public bool DisableNoAuthSends { get; init; } + + /// + /// Indicates whether the org users are prohibited from creating or editing Sends that use password authorization. + /// + public bool DisablePasswordSends { get; init; } + + /// + /// Indicates whether the org users are prohibited from creating or editing Sends that use email verification. + /// + public bool DisableEmailVerifiedSends { get; init; } } public class SendPolicyRequirementFactory : BasePolicyRequirementFactory @@ -30,10 +45,20 @@ public override SendPolicyRequirement Create(IEnumerable policyDe .Select(p => p.GetDataModel()) .Aggregate( new SendPolicyRequirement(), - (result, data) => new SendPolicyRequirement + (result, data) => { - DisableSend = result.DisableSend || data.DisableSend, - DisableHideEmail = result.DisableHideEmail || data.DisableHideEmail + var disableNoAuthSends = result.DisableNoAuthSends || data.DisableNoAuthSends; + var disablePasswordSends = result.DisablePasswordSends || data.DisablePasswordSends; + var disableEmailVerifiedSends = result.DisableEmailVerifiedSends || data.DisableEmailVerifiedSends; + return new SendPolicyRequirement + { + DisableSend = result.DisableSend || data.DisableSend + || (disableNoAuthSends && disablePasswordSends && disableEmailVerifiedSends), + DisableHideEmail = result.DisableHideEmail || data.DisableHideEmail, + DisableNoAuthSends = disableNoAuthSends, + DisablePasswordSends = disablePasswordSends, + DisableEmailVerifiedSends = disableEmailVerifiedSends, + }; }); } } diff --git a/src/Core/Tools/SendFeatures/Services/SendValidationService.cs b/src/Core/Tools/SendFeatures/Services/SendValidationService.cs index a951d6233367..b409d1fc1fce 100644 --- a/src/Core/Tools/SendFeatures/Services/SendValidationService.cs +++ b/src/Core/Tools/SendFeatures/Services/SendValidationService.cs @@ -13,6 +13,7 @@ using Bit.Core.Services; using Bit.Core.Settings; using Bit.Core.Tools.Entities; +using Bit.Core.Tools.Enums; using Bit.Core.Utilities; namespace Bit.Core.Tools.Services; @@ -74,13 +75,37 @@ public async Task ValidateUserCanSaveAsync(Guid? userId, Send send) throw new BadRequestException("Due to an Enterprise Policy, you are only able to delete an existing Send."); } - if (send.HideEmail.GetValueOrDefault()) + var sendOptionsPolicies = await _policyService.GetPoliciesApplicableToUserAsync(userId.Value, PolicyType.SendOptions); + var sendOptionsPolicyData = sendOptionsPolicies + .Select(p => CoreHelpers.LoadClassFromJsonData(p.PolicyData)) + .Where(d => d != null) + .ToList(); + + if (sendOptionsPolicyData.Any(d => d.DisableSend)) { - var sendOptionsPolicies = await _policyService.GetPoliciesApplicableToUserAsync(userId.Value, PolicyType.SendOptions); - if (sendOptionsPolicies.Any(p => CoreHelpers.LoadClassFromJsonData(p.PolicyData)?.DisableHideEmail ?? false)) - { - 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."); - } + throw new BadRequestException("Due to an Enterprise Policy, you are only able to delete an existing Send."); + } + + if (send.HideEmail.GetValueOrDefault() && sendOptionsPolicyData.Any(d => d.DisableHideEmail)) + { + 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."); + } + + var authType = send.AuthType ?? AuthType.None; + + if (authType == AuthType.None && sendOptionsPolicyData.Any(d => d.DisableNoAuthSends)) + { + throw new BadRequestException("Due to an Enterprise Policy, you are not allowed to create or edit Sends without authentication."); + } + + if (authType == AuthType.Password && sendOptionsPolicyData.Any(d => d.DisablePasswordSends)) + { + throw new BadRequestException("Due to an Enterprise Policy, you are not allowed to create or edit Sends that use password authentication."); + } + + if (authType == AuthType.Email && sendOptionsPolicyData.Any(d => d.DisableEmailVerifiedSends)) + { + throw new BadRequestException("Due to an Enterprise Policy, you are not allowed to create or edit Sends that use email verification."); } } @@ -102,6 +127,23 @@ public async Task ValidateUserCanSaveAsync_vNext(Guid? userId, Send send) { 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."); } + + var authType = send.AuthType ?? AuthType.None; + + if (sendRequirement.DisableNoAuthSends && authType == AuthType.None) + { + throw new BadRequestException("Due to an Enterprise Policy, you are not allowed to create or edit Sends without authentication."); + } + + if (sendRequirement.DisablePasswordSends && authType == AuthType.Password) + { + throw new BadRequestException("Due to an Enterprise Policy, you are not allowed to create or edit Sends that use password authentication."); + } + + if (sendRequirement.DisableEmailVerifiedSends && authType == AuthType.Email) + { + throw new BadRequestException("Due to an Enterprise Policy, you are not allowed to create or edit Sends that use email verification."); + } } public async Task StorageRemainingForSendAsync(Send send) diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SendPolicyRequirementFactoryTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SendPolicyRequirementFactoryTests.cs index 8afbdc94e7d6..5e3c6b597bea 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SendPolicyRequirementFactoryTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SendPolicyRequirementFactoryTests.cs @@ -82,4 +82,140 @@ SutProvider sutProvider Assert.True(actual.DisableHideEmail); } + + [Theory, BitAutoData] + public void DisableNoAuthSends_IsFalse_IfNoPolicies(SutProvider sutProvider) + { + var actual = sutProvider.Sut.Create([]); + + Assert.False(actual.DisableNoAuthSends); + } + + [Theory, BitAutoData] + public void DisableNoAuthSends_IsFalse_IfNotConfigured( + [PolicyDetails(PolicyType.SendOptions)] PolicyDetails[] policies, + SutProvider sutProvider + ) + { + policies[0].SetDataModel(new SendPolicyData { DisableNoAuthSends = false }); + policies[1].SetDataModel(new SendPolicyData { DisableNoAuthSends = false }); + + var actual = sutProvider.Sut.Create(policies); + + Assert.False(actual.DisableNoAuthSends); + } + + [Theory, BitAutoData] + public void DisableNoAuthSends_IsTrue_IfAnyConfigured( + [PolicyDetails(PolicyType.SendOptions)] PolicyDetails[] policies, + SutProvider sutProvider + ) + { + policies[0].SetDataModel(new SendPolicyData { DisableNoAuthSends = true }); + policies[1].SetDataModel(new SendPolicyData { DisableNoAuthSends = false }); + + var actual = sutProvider.Sut.Create(policies); + + Assert.True(actual.DisableNoAuthSends); + } + + [Theory, BitAutoData] + public void DisablePasswordSends_IsFalse_IfNoPolicies(SutProvider sutProvider) + { + var actual = sutProvider.Sut.Create([]); + + Assert.False(actual.DisablePasswordSends); + } + + [Theory, BitAutoData] + public void DisablePasswordSends_IsFalse_IfNotConfigured( + [PolicyDetails(PolicyType.SendOptions)] PolicyDetails[] policies, + SutProvider sutProvider + ) + { + policies[0].SetDataModel(new SendPolicyData { DisablePasswordSends = false }); + policies[1].SetDataModel(new SendPolicyData { DisablePasswordSends = false }); + + var actual = sutProvider.Sut.Create(policies); + + Assert.False(actual.DisablePasswordSends); + } + + [Theory, BitAutoData] + public void DisablePasswordSends_IsTrue_IfAnyConfigured( + [PolicyDetails(PolicyType.SendOptions)] PolicyDetails[] policies, + SutProvider sutProvider + ) + { + policies[0].SetDataModel(new SendPolicyData { DisablePasswordSends = true }); + policies[1].SetDataModel(new SendPolicyData { DisablePasswordSends = false }); + + var actual = sutProvider.Sut.Create(policies); + + Assert.True(actual.DisablePasswordSends); + } + + [Theory, BitAutoData] + public void DisableEmailVerifiedSends_IsFalse_IfNoPolicies(SutProvider sutProvider) + { + var actual = sutProvider.Sut.Create([]); + + Assert.False(actual.DisableEmailVerifiedSends); + } + + [Theory, BitAutoData] + public void DisableEmailVerifiedSends_IsFalse_IfNotConfigured( + [PolicyDetails(PolicyType.SendOptions)] PolicyDetails[] policies, + SutProvider sutProvider + ) + { + policies[0].SetDataModel(new SendPolicyData { DisableEmailVerifiedSends = false }); + policies[1].SetDataModel(new SendPolicyData { DisableEmailVerifiedSends = false }); + + var actual = sutProvider.Sut.Create(policies); + + Assert.False(actual.DisableEmailVerifiedSends); + } + + [Theory, BitAutoData] + public void DisableEmailVerifiedSends_IsTrue_IfAnyConfigured( + [PolicyDetails(PolicyType.SendOptions)] PolicyDetails[] policies, + SutProvider sutProvider + ) + { + policies[0].SetDataModel(new SendPolicyData { DisableEmailVerifiedSends = true }); + policies[1].SetDataModel(new SendPolicyData { DisableEmailVerifiedSends = false }); + + var actual = sutProvider.Sut.Create(policies); + + Assert.True(actual.DisableEmailVerifiedSends); + } + + [Theory, BitAutoData] + public void DisableSend_IsFalse_IfOnlyTwoAuthTypesDisabledAcrossOrgs( + [PolicyDetails(PolicyType.SendOptions)] PolicyDetails[] policies, + SutProvider sutProvider + ) + { + policies[0].SetDataModel(new SendPolicyData { DisableNoAuthSends = true }); + policies[1].SetDataModel(new SendPolicyData { DisablePasswordSends = true }); + + var actual = sutProvider.Sut.Create(policies); + + Assert.False(actual.DisableSend); + } + + [Theory, BitAutoData] + public void DisableSend_IsTrue_IfAllThreeAuthTypesDisabledAcrossOrgs( + [PolicyDetails(PolicyType.SendOptions)] PolicyDetails[] policies, + SutProvider sutProvider + ) + { + policies[0].SetDataModel(new SendPolicyData { DisableNoAuthSends = true, DisablePasswordSends = true }); + policies[1].SetDataModel(new SendPolicyData { DisableEmailVerifiedSends = true }); + + var actual = sutProvider.Sut.Create(policies); + + Assert.True(actual.DisableSend); + } } diff --git a/test/Core.Test/Tools/Services/SendValidationServiceTests.cs b/test/Core.Test/Tools/Services/SendValidationServiceTests.cs index 8adce1a29f84..dafe2d4cf10c 100644 --- a/test/Core.Test/Tools/Services/SendValidationServiceTests.cs +++ b/test/Core.Test/Tools/Services/SendValidationServiceTests.cs @@ -1,12 +1,20 @@ 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.Billing.Pricing; using Bit.Core.Billing.Pricing.Premium; +using Bit.Core.Context; using Bit.Core.Entities; +using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Tools.Entities; using Bit.Core.Tools.Enums; using Bit.Core.Tools.Services; +using Bit.Core.Utilities; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using NSubstitute; @@ -117,4 +125,259 @@ public async Task StorageRemainingForSendAsync_OrgSend_DoesNotCallPricingService // Assert - should NOT call pricing service for org sends await sutProvider.GetDependency().DidNotReceive().GetAvailablePremiumPlan(); } + + [Theory, BitAutoData] + public async Task ValidateUserCanSaveAsync_vNext_DisableNoAuthSends_ThrowsWhenAuthTypeIsNull( + Guid userId, + Send send, + SutProvider sutProvider) + { + send.AuthType = null; + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.PolicyRequirements) + .Returns(true); + sutProvider.GetDependency() + .GetAsync(userId) + .Returns(new SendPolicyRequirement { DisableNoAuthSends = true }); + + await Assert.ThrowsAsync( + () => sutProvider.Sut.ValidateUserCanSaveAsync(userId, send)); + } + + [Theory, BitAutoData] + public async Task ValidateUserCanSaveAsync_vNext_DisableNoAuthSends_ThrowsWhenAuthTypeIsNone( + Guid userId, + Send send, + SutProvider sutProvider) + { + send.AuthType = AuthType.None; + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.PolicyRequirements) + .Returns(true); + sutProvider.GetDependency() + .GetAsync(userId) + .Returns(new SendPolicyRequirement { DisableNoAuthSends = true }); + + await Assert.ThrowsAsync( + () => sutProvider.Sut.ValidateUserCanSaveAsync(userId, send)); + } + + [Theory, BitAutoData] + public async Task ValidateUserCanSaveAsync_vNext_DisableNoAuthSends_AllowsOtherAuthTypes( + Guid userId, + Send send, + SutProvider sutProvider) + { + send.AuthType = AuthType.Password; + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.PolicyRequirements) + .Returns(true); + sutProvider.GetDependency() + .GetAsync(userId) + .Returns(new SendPolicyRequirement { DisableNoAuthSends = true }); + + // Should not throw + await sutProvider.Sut.ValidateUserCanSaveAsync(userId, send); + } + + [Theory, BitAutoData] + public async Task ValidateUserCanSaveAsync_vNext_DisablePasswordSends_ThrowsWhenPasswordAuth( + Guid userId, + Send send, + SutProvider sutProvider) + { + send.AuthType = AuthType.Password; + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.PolicyRequirements) + .Returns(true); + sutProvider.GetDependency() + .GetAsync(userId) + .Returns(new SendPolicyRequirement { DisablePasswordSends = true }); + + await Assert.ThrowsAsync( + () => sutProvider.Sut.ValidateUserCanSaveAsync(userId, send)); + } + + [Theory, BitAutoData] + public async Task ValidateUserCanSaveAsync_vNext_DisablePasswordSends_AllowsOtherAuthTypes( + Guid userId, + Send send, + SutProvider sutProvider) + { + send.AuthType = AuthType.Email; + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.PolicyRequirements) + .Returns(true); + sutProvider.GetDependency() + .GetAsync(userId) + .Returns(new SendPolicyRequirement { DisablePasswordSends = true }); + + // Should not throw + await sutProvider.Sut.ValidateUserCanSaveAsync(userId, send); + } + + [Theory, BitAutoData] + public async Task ValidateUserCanSaveAsync_vNext_DisableEmailVerifiedSends_ThrowsWhenEmailAuth( + Guid userId, + Send send, + SutProvider sutProvider) + { + send.AuthType = AuthType.Email; + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.PolicyRequirements) + .Returns(true); + sutProvider.GetDependency() + .GetAsync(userId) + .Returns(new SendPolicyRequirement { DisableEmailVerifiedSends = true }); + + await Assert.ThrowsAsync( + () => sutProvider.Sut.ValidateUserCanSaveAsync(userId, send)); + } + + [Theory, BitAutoData] + public async Task ValidateUserCanSaveAsync_vNext_DisableEmailVerifiedSends_AllowsOtherAuthTypes( + Guid userId, + Send send, + SutProvider sutProvider) + { + send.AuthType = AuthType.None; + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.PolicyRequirements) + .Returns(true); + sutProvider.GetDependency() + .GetAsync(userId) + .Returns(new SendPolicyRequirement { DisableEmailVerifiedSends = true }); + + // Should not throw + await sutProvider.Sut.ValidateUserCanSaveAsync(userId, send); + } + + private static OrganizationUserPolicyDetails SendOptionsPolicyWith(SendPolicyData data) => + new() + { + PolicyType = PolicyType.SendOptions, + PolicyEnabled = true, + PolicyData = CoreHelpers.ClassToJsonData(data), + }; + + private static void SetupLegacyPath( + Guid userId, + SutProvider sutProvider, + SendPolicyData policyData) + { + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.PolicyRequirements) + .Returns(false); + sutProvider.GetDependency().Organizations = + [new CurrentContextOrganization()]; + sutProvider.GetDependency() + .AnyPoliciesApplicableToUserAsync(userId, PolicyType.DisableSend) + .Returns(false); + sutProvider.GetDependency() + .GetPoliciesApplicableToUserAsync(userId, PolicyType.SendOptions) + .Returns([SendOptionsPolicyWith(policyData)]); + } + + [Theory, BitAutoData] + public async Task ValidateUserCanSaveAsync_Legacy_DisableSendOnSendOptions_Throws( + Guid userId, + Send send, + SutProvider sutProvider) + { + SetupLegacyPath(userId, sutProvider, new SendPolicyData { DisableSend = true }); + + await Assert.ThrowsAsync( + () => sutProvider.Sut.ValidateUserCanSaveAsync(userId, send)); + } + + [Theory, BitAutoData] + public async Task ValidateUserCanSaveAsync_Legacy_DisableNoAuthSends_ThrowsWhenAuthTypeIsNull( + Guid userId, + Send send, + SutProvider sutProvider) + { + send.AuthType = null; + SetupLegacyPath(userId, sutProvider, new SendPolicyData { DisableNoAuthSends = true }); + + await Assert.ThrowsAsync( + () => sutProvider.Sut.ValidateUserCanSaveAsync(userId, send)); + } + + [Theory, BitAutoData] + public async Task ValidateUserCanSaveAsync_Legacy_DisableNoAuthSends_ThrowsWhenAuthTypeIsNone( + Guid userId, + Send send, + SutProvider sutProvider) + { + send.AuthType = AuthType.None; + SetupLegacyPath(userId, sutProvider, new SendPolicyData { DisableNoAuthSends = true }); + + await Assert.ThrowsAsync( + () => sutProvider.Sut.ValidateUserCanSaveAsync(userId, send)); + } + + [Theory, BitAutoData] + public async Task ValidateUserCanSaveAsync_Legacy_DisableNoAuthSends_AllowsOtherAuthTypes( + Guid userId, + Send send, + SutProvider sutProvider) + { + send.AuthType = AuthType.Password; + SetupLegacyPath(userId, sutProvider, new SendPolicyData { DisableNoAuthSends = true }); + + // Should not throw + await sutProvider.Sut.ValidateUserCanSaveAsync(userId, send); + } + + [Theory, BitAutoData] + public async Task ValidateUserCanSaveAsync_Legacy_DisablePasswordSends_ThrowsWhenPasswordAuth( + Guid userId, + Send send, + SutProvider sutProvider) + { + send.AuthType = AuthType.Password; + SetupLegacyPath(userId, sutProvider, new SendPolicyData { DisablePasswordSends = true }); + + await Assert.ThrowsAsync( + () => sutProvider.Sut.ValidateUserCanSaveAsync(userId, send)); + } + + [Theory, BitAutoData] + public async Task ValidateUserCanSaveAsync_Legacy_DisablePasswordSends_AllowsOtherAuthTypes( + Guid userId, + Send send, + SutProvider sutProvider) + { + send.AuthType = AuthType.Email; + SetupLegacyPath(userId, sutProvider, new SendPolicyData { DisablePasswordSends = true }); + + // Should not throw + await sutProvider.Sut.ValidateUserCanSaveAsync(userId, send); + } + + [Theory, BitAutoData] + public async Task ValidateUserCanSaveAsync_Legacy_DisableEmailVerifiedSends_ThrowsWhenEmailAuth( + Guid userId, + Send send, + SutProvider sutProvider) + { + send.AuthType = AuthType.Email; + SetupLegacyPath(userId, sutProvider, new SendPolicyData { DisableEmailVerifiedSends = true }); + + await Assert.ThrowsAsync( + () => sutProvider.Sut.ValidateUserCanSaveAsync(userId, send)); + } + + [Theory, BitAutoData] + public async Task ValidateUserCanSaveAsync_Legacy_DisableEmailVerifiedSends_AllowsOtherAuthTypes( + Guid userId, + Send send, + SutProvider sutProvider) + { + send.AuthType = AuthType.None; + SetupLegacyPath(userId, sutProvider, new SendPolicyData { DisableEmailVerifiedSends = true }); + + // Should not throw + await sutProvider.Sut.ValidateUserCanSaveAsync(userId, send); + } }