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