diff --git a/src/Core/AdminConsole/Models/Data/Organizations/Policies/SendControlsPolicyData.cs b/src/Core/AdminConsole/Models/Data/Organizations/Policies/SendControlsPolicyData.cs index 4aa09aa1ce48..840fcbe4e55e 100644 --- a/src/Core/AdminConsole/Models/Data/Organizations/Policies/SendControlsPolicyData.cs +++ b/src/Core/AdminConsole/Models/Data/Organizations/Policies/SendControlsPolicyData.cs @@ -1,4 +1,5 @@ using System.ComponentModel.DataAnnotations; +using Bit.Core.Tools.Enums; namespace Bit.Core.AdminConsole.Models.Data.Organizations.Policies; @@ -13,4 +14,6 @@ public class SendControlsPolicyData : IPolicyDataModel [Display(Name = "AllowedDomains")] [StringLength(1000)] public string? AllowedDomains { get; set; } + [Display(Name = "AllowedSendTypes")] + public SendType[]? AllowedSendTypes { get; set; } } diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyEventHandlers/SendControlsSyncPolicyEvent.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyEventHandlers/SendControlsSyncPolicyEvent.cs index b93a5f8318a3..464ce2194058 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyEventHandlers/SendControlsSyncPolicyEvent.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyEventHandlers/SendControlsSyncPolicyEvent.cs @@ -148,6 +148,10 @@ private static bool SendIsNonCompliant(Send send, SendControlsPolicyData policyD return true; } } + if (policyData.AllowedSendTypes != null && !policyData.AllowedSendTypes.Contains(send.Type)) + { + return true; + } return false; } } diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SendControlsPolicyRequirement.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SendControlsPolicyRequirement.cs index ea291b176af4..ba3337508992 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SendControlsPolicyRequirement.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SendControlsPolicyRequirement.cs @@ -1,5 +1,6 @@ using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; +using Bit.Core.Tools.Enums; namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; @@ -29,6 +30,11 @@ public class SendControlsPolicyRequirement : IPolicyRequirement /// Indicates the domains the emails of an email-protected Send must use /// public string? AllowedDomains { get; init; } + + /// + /// Indicates the types of Send that can be created + /// + public SendType[]? AllowedSendTypes { get; init; } } public class SendControlsPolicyRequirementFactory : BasePolicyRequirementFactory @@ -46,7 +52,8 @@ public override SendControlsPolicyRequirement Create(IEnumerable DisableSend = result.DisableSend || data.DisableSend, DisableHideEmail = result.DisableHideEmail || data.DisableHideEmail, WhoCanAccess = result.WhoCanAccess ?? data.WhoCanAccess, - AllowedDomains = result.AllowedDomains ?? data.AllowedDomains + AllowedDomains = result.AllowedDomains ?? data.AllowedDomains, + AllowedSendTypes = result.AllowedSendTypes ?? data.AllowedSendTypes, }); } } diff --git a/src/Core/Tools/SendFeatures/Services/SendValidationService.cs b/src/Core/Tools/SendFeatures/Services/SendValidationService.cs index f78a377ff62c..4065944575be 100644 --- a/src/Core/Tools/SendFeatures/Services/SendValidationService.cs +++ b/src/Core/Tools/SendFeatures/Services/SendValidationService.cs @@ -9,6 +9,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; @@ -102,6 +103,11 @@ public async Task ValidateUserCanSaveAsync(Guid? userId, Send send) { throw new BadRequestException($"Due to an Enterprise Policy your Sends must be protected by email verification and access granted only to the following domain(s): {sendControlsRequirement.AllowedDomains}"); } + + if (sendControlsRequirement.AllowedSendTypes != null && !sendControlsRequirement.AllowedSendTypes.Contains(send.Type)) + { + throw new BadRequestException($"Due to an Enterprise policy your Sends must be of the following types: {string.Join(", ", sendControlsRequirement.AllowedSendTypes.Select(st => st == SendType.Text ? "Text" : st == SendType.File ? "File" : "Unknown"))}"); + } } public static bool SendAllEmailsHaveAllowedDomains(string? emailsString, string? domainsString) diff --git a/src/Sql/dbo/Tools/Stored Procedures/Send_ReadIdsByOrganizationId.sql b/src/Sql/dbo/Tools/Stored Procedures/Send_ReadIdsByOrganizationId.sql index a2ac2e040aa5..f9338434ce55 100644 --- a/src/Sql/dbo/Tools/Stored Procedures/Send_ReadIdsByOrganizationId.sql +++ b/src/Sql/dbo/Tools/Stored Procedures/Send_ReadIdsByOrganizationId.sql @@ -8,17 +8,18 @@ BEGIN DECLARE @OrgUserIds AS [GuidIdArray]; INSERT INTO @OrgUserIds SELECT DISTINCT - UserId + [UserId] FROM [dbo].[OrganizationUserView] WHERE - OrganizationId = @OrganizationId + [OrganizationId] = @OrganizationId + AND [UserId] IS NOT NULL -- Get the IDs of all Sends associated with those users -- SELECT - Id + [Id] FROM [dbo].[SendView] WHERE - UserId IN (SELECT [Id] FROM @OrgUserIds) + [UserId] IN (SELECT [Id] FROM @OrgUserIds) END \ No newline at end of file diff --git a/src/Sql/dbo/Tools/Stored Procedures/Send_UpdateDisabledByIds.sql b/src/Sql/dbo/Tools/Stored Procedures/Send_UpdateDisabledByIds.sql index 1958dc2b4ff2..41f0009cf3ce 100644 --- a/src/Sql/dbo/Tools/Stored Procedures/Send_UpdateDisabledByIds.sql +++ b/src/Sql/dbo/Tools/Stored Procedures/Send_UpdateDisabledByIds.sql @@ -19,7 +19,7 @@ BEGIN INSERT INTO @UserIds SELECT DISTINCT - UserId + [UserId] FROM [dbo].[Send] WHERE diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyEventHandlers/SendControlsSyncPolicyEventTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyEventHandlers/SendControlsSyncPolicyEventTests.cs index 66fe91683d22..4ae8c3774831 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyEventHandlers/SendControlsSyncPolicyEventTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyEventHandlers/SendControlsSyncPolicyEventTests.cs @@ -449,4 +449,52 @@ await sutProvider.GetDependency() .Received(1) .UpdateManyDisabledAsync(Arg.Is>(l => l.Count == 3 && l.Contains(nonCompliantSend1.Id) && l.Contains(nonCompliantSend2.Id) && l.Contains(nonCompliantSend3.Id)), true); } + + [Theory, BitAutoData] + public async Task ExecutePostUpsertSideEffectAsync_AllowedSendTypesDisablesNoncompliantSends( + [PolicyUpdate(PolicyType.SendControls, enabled: true)] PolicyUpdate policyUpdate, + [Policy(PolicyType.SendControls, enabled: true)] Policy postUpsertedPolicy, + [Policy(PolicyType.DisableSend, enabled: false)] Policy existingDisableSendPolicy, + [Policy(PolicyType.SendOptions, enabled: false)] Policy existingSendOptionsPolicy, + SutProvider sutProvider) + { + postUpsertedPolicy.OrganizationId = policyUpdate.OrganizationId; + existingDisableSendPolicy.OrganizationId = policyUpdate.OrganizationId; + existingSendOptionsPolicy.OrganizationId = policyUpdate.OrganizationId; + postUpsertedPolicy.SetDataModel(new SendControlsPolicyData { AllowedSendTypes = [SendType.Text] }); + + sutProvider.GetDependency() + .GetByOrganizationIdTypeAsync(policyUpdate.OrganizationId, PolicyType.DisableSend) + .Returns(existingDisableSendPolicy); + sutProvider.GetDependency() + .GetByOrganizationIdTypeAsync(policyUpdate.OrganizationId, PolicyType.SendOptions) + .Returns(existingSendOptionsPolicy); + var compliantSend = new Send + { + Id = Guid.NewGuid(), + Type = SendType.Text + }; + var nonCompliantSend = new Send + { + Id = Guid.NewGuid(), + Type = SendType.File + }; + var sendIds = new List([compliantSend.Id, nonCompliantSend.Id]); + sutProvider.GetDependency() + .GetIdsByOrganizationIdAsync(policyUpdate.OrganizationId) + .Returns(sendIds); + sutProvider.GetDependency() + .GetManyByIdsAsync(Arg.Any>()) + .Returns([compliantSend, nonCompliantSend]); + + await sutProvider.Sut.ExecutePostUpsertSideEffectAsync( + new SavePolicyModel(policyUpdate), postUpsertedPolicy, null); + + await sutProvider.GetDependency() + .Received(1) + .UpdateManyDisabledAsync(Arg.Is>(l => l.Count() == 1 && l.ElementAt(0) == compliantSend.Id), false); + await sutProvider.GetDependency() + .Received(1) + .UpdateManyDisabledAsync(Arg.Is>(l => l.Count() == 1 && l.ElementAt(0) == nonCompliantSend.Id), true); + } } diff --git a/test/Infrastructure.IntegrationTest/packages.lock.json b/test/Infrastructure.IntegrationTest/packages.lock.json index 32f32f7fdf03..eca7f595296c 100644 --- a/test/Infrastructure.IntegrationTest/packages.lock.json +++ b/test/Infrastructure.IntegrationTest/packages.lock.json @@ -18,6 +18,17 @@ "Microsoft.Extensions.Primitives": "10.0.8" } }, + "Microsoft.Extensions.Diagnostics.Testing": { + "type": "Direct", + "requested": "[10.5.0, 10.5.0]", + "resolved": "10.5.0", + "contentHash": "EiERK24AmaxMa7hkgV5UqdfIlrMxg3n+7XdkaV3FWLoyEzca6gvGiaRZofl6KzAtvMBJoZvb5a+EyYqq+M9CoQ==", + "dependencies": { + "Microsoft.Extensions.Logging": "10.0.6", + "Microsoft.Extensions.Options.ConfigurationExtensions": "10.0.6", + "Microsoft.Extensions.Telemetry.Abstractions": "10.5.0" + } + }, "Microsoft.Extensions.Logging": { "type": "Direct", "requested": "[10.0.8, 10.0.8]", @@ -731,6 +742,15 @@ "StackExchange.Redis": "2.7.27" } }, + "Microsoft.Extensions.Compliance.Abstractions": { + "type": "Transitive", + "resolved": "10.5.0", + "contentHash": "xbWZji13Vb2jDJNtwVrKpI09jd8x3n3fL+GzhiLK+8O5Wc2A+GyqCZalST2fV46Pf0QfCwkXf83y+3/rDkCd7A==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.6", + "Microsoft.Extensions.ObjectPool": "10.0.6" + } + }, "Microsoft.Extensions.Configuration.Abstractions": { "type": "Transitive", "resolved": "10.0.8", @@ -905,6 +925,11 @@ "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.8" } }, + "Microsoft.Extensions.ObjectPool": { + "type": "Transitive", + "resolved": "10.0.6", + "contentHash": "2Jafd4fdxxiwiQ08mcF+Lf3vqikkQZusGVThOKZNSmPDceGk4IwkjeHL7OEb9Ov8q9ICY5wofL98CS153K5VvQ==" + }, "Microsoft.Extensions.Options": { "type": "Transitive", "resolved": "10.0.8", @@ -931,6 +956,17 @@ "resolved": "10.0.8", "contentHash": "OBPo4nYhMyIbtueoC10CBm6AGAbo/A9IV8QQ/6ryZS7VvmqpGT7hunazeHLxFawRzn3oLOq4jhqhpBX4tfswWQ==" }, + "Microsoft.Extensions.Telemetry.Abstractions": { + "type": "Transitive", + "resolved": "10.5.0", + "contentHash": "VmU7e6xHqoubWKl7y9MtWyQAjlDpvbds3gY8ZKMS/1GxY2+U1/aMNnMj09aOXAa3p5qhHSSkBzDJvyokCjVkPg==", + "dependencies": { + "Microsoft.Extensions.Compliance.Abstractions": "10.5.0", + "Microsoft.Extensions.Logging.Abstractions": "10.0.6", + "Microsoft.Extensions.ObjectPool": "10.0.6", + "Microsoft.Extensions.Options": "10.0.6" + } + }, "Microsoft.Identity.Client": { "type": "Transitive", "resolved": "4.66.1", @@ -1518,7 +1554,7 @@ "infrastructure.dapper": { "type": "Project", "dependencies": { - "Core": "[2026.6.0, )", + "Core": "[2026.6.1, )", "Dapper": "[2.1.66, 2.1.66]" } }, @@ -1526,7 +1562,7 @@ "type": "Project", "dependencies": { "AutoMapper": "[14.0.0, 14.0.0]", - "Core": "[2026.6.0, )", + "Core": "[2026.6.1, )", "Microsoft.EntityFrameworkCore.Relational": "[8.0.8, 8.0.8]", "Microsoft.EntityFrameworkCore.SqlServer": "[8.0.8, 8.0.8]", "Microsoft.EntityFrameworkCore.Sqlite": "[8.0.8, 8.0.8]", @@ -1539,7 +1575,7 @@ "migrator": { "type": "Project", "dependencies": { - "Core": "[2026.6.0, )", + "Core": "[2026.6.1, )", "Microsoft.Extensions.Logging": "[10.0.8, 10.0.8]", "dbup-sqlserver": "[7.2.0, 7.2.0]" } @@ -1547,22 +1583,22 @@ "mysqlmigrations": { "type": "Project", "dependencies": { - "Core": "[2026.6.0, )", - "Infrastructure.EntityFramework": "[2026.6.0, )" + "Core": "[2026.6.1, )", + "Infrastructure.EntityFramework": "[2026.6.1, )" } }, "postgresmigrations": { "type": "Project", "dependencies": { - "Core": "[2026.6.0, )", - "Infrastructure.EntityFramework": "[2026.6.0, )" + "Core": "[2026.6.1, )", + "Infrastructure.EntityFramework": "[2026.6.1, )" } }, "sqlitemigrations": { "type": "Project", "dependencies": { - "Core": "[2026.6.0, )", - "Infrastructure.EntityFramework": "[2026.6.0, )" + "Core": "[2026.6.1, )", + "Infrastructure.EntityFramework": "[2026.6.1, )" } } } diff --git a/util/Migrator/DbScripts/2026-06-22_00_UpdateSendReadIdsByOrgId.sql b/util/Migrator/DbScripts/2026-06-22_00_UpdateSendReadIdsByOrgId.sql new file mode 100644 index 000000000000..7225e80b4ab7 --- /dev/null +++ b/util/Migrator/DbScripts/2026-06-22_00_UpdateSendReadIdsByOrgId.sql @@ -0,0 +1,26 @@ +CREATE OR ALTER PROCEDURE [dbo].[Send_ReadIdsByOrganizationId] + @OrganizationId UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + -- Get the IDs of all users in an org -- + DECLARE @OrgUserIds AS [GuidIdArray]; + INSERT INTO @OrgUserIds + SELECT DISTINCT + [UserId] + FROM + [dbo].[OrganizationUserView] + WHERE + [OrganizationId] = @OrganizationId + AND [UserId] IS NOT NULL + + -- Get the IDs of all Sends associated with those users -- + SELECT + [Id] + FROM + [dbo].[SendView] + WHERE + [UserId] IN (SELECT [Id] FROM @OrgUserIds) +END +GO \ No newline at end of file