Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
63 commits
Select commit Hold shift + click to select a range
2bb7c17
initial send controls
harr1424 Mar 2, 2026
6f51c42
update vNext methods and add test coverage for policy validators
harr1424 Mar 3, 2026
94b7025
Merge branch 'main' into tools/PM-31885-SendControls-Policy
harr1424 Mar 3, 2026
7c4c042
Merge remote-tracking branch 'origin/main' into tools/PM-31885-SendCo…
harr1424 Mar 3, 2026
bcc2960
add comments to tests
harr1424 Mar 4, 2026
1f7caf8
Merge branch 'main' into tools/PM-31885-SendControls-Policy
harr1424 Mar 4, 2026
d52424c
Apply suggestion from @mkincaid-bw
harr1424 Mar 6, 2026
235d832
renamne migrations for correct sorting
harr1424 Mar 6, 2026
e6bf8e6
Merge branch 'tools/PM-31885-SendControls-Policy' of github.com:bitwa…
harr1424 Mar 6, 2026
b0af868
Merge branch 'main' into tools/PM-31885-SendControls-Policy
harr1424 Mar 6, 2026
4a0728c
respond to csharp related review comments
harr1424 Mar 6, 2026
16cffeb
fix failing lints
harr1424 Mar 6, 2026
ea8527e
fix tests
harr1424 Mar 6, 2026
783b426
revise policy sync logic
harr1424 Mar 7, 2026
f8bbfa0
revise policy event logic and tests
harr1424 Mar 9, 2026
5022841
Merge branch 'main' into tools/PM-31885-SendControls-Policy
harr1424 Mar 9, 2026
9b1b7e7
Merge branch 'tools/PM-31885-SendControls-Policy' of github.com:bitwa…
harr1424 Mar 9, 2026
a6ea853
add integration tests
harr1424 Mar 10, 2026
e6c1f11
Merge branch 'main' into tools/PM-31885-SendControls-Policy
harr1424 Mar 10, 2026
ac6710d
OR legacy policy data with SendControls policy data
harr1424 Mar 11, 2026
c739d52
Merge branch 'main' into tools/PM-31885-SendControls-Policy
harr1424 Mar 17, 2026
8bc9f7d
remove migrations and associated integration test
harr1424 Mar 17, 2026
26baa10
Merge branch 'main' into tools/PM-31885-SendControls-Policy
harr1424 Mar 18, 2026
b544964
whitespacing and comment correction
harr1424 Mar 18, 2026
50fdaf2
aggregate kegacy Send policies in PolicyQuery and adjust PoliciesCont…
harr1424 Mar 18, 2026
84d0cd1
add comments to simplify post-migration cleanup
harr1424 Mar 18, 2026
f27e358
consolidate legacy Send policy synthesis from PoliciesController into…
harr1424 Mar 21, 2026
1f54881
respond to review comments and other minor fixes
harr1424 Mar 25, 2026
f779c2f
Merge branch 'main' into tools/PM-31885-SendControls-Policy
harr1424 Mar 25, 2026
d3cf561
[PM-31884] Add Send control policy access control fields
mcamirault Mar 31, 2026
697bae2
Disable and enable Sends based on policy compliance
mcamirault Apr 8, 2026
b23c233
Merge branch 'main' into tools/pm-31884/send-access-controls-policy
mcamirault Apr 8, 2026
947c3b5
Remove stray merge change
mcamirault Apr 8, 2026
657ee7d
Address PR comments
mcamirault Apr 9, 2026
2bafe52
More PR comment fixes
mcamirault Apr 10, 2026
c43c079
Adjust email domain restriction logic, consolidate into a function
mcamirault Apr 10, 2026
c4e326c
More PR comment fixes
mcamirault Apr 14, 2026
f3a4f16
Fix data migration and sproc files
mcamirault Apr 14, 2026
3a53965
Even out database load by fetching all org Send IDs and processing in…
mcamirault Apr 19, 2026
f0d09f5
Merge branch 'main' into tools/pm-31884/send-access-controls-policy
mcamirault Apr 20, 2026
44dd3cd
Fix tests and address review comments
mcamirault Apr 22, 2026
72a8b71
[PM-32187] Add Send Type restriction to Send Controls policy
mcamirault Apr 20, 2026
9e68219
Fix Send Type check breaking reenabling behavior
mcamirault Apr 23, 2026
0732302
Merge branch 'main' into tools/pm-31884/send-access-controls-policy
mcamirault Apr 27, 2026
425b7b5
Merge branch 'main' into tools/pm-31884/send-access-controls-policy
mcamirault Apr 29, 2026
183816b
More PR comment fixes
mcamirault May 4, 2026
a27de12
Merge branch 'main' into tools/pm-31884/send-access-controls-policy
mcamirault May 4, 2026
f567d52
Fix data migration script order
mcamirault May 4, 2026
9cb87f2
Lint fixes
mcamirault May 4, 2026
f3b4404
Address a few review comments
mcamirault May 5, 2026
85e412c
Fix migration script
mcamirault May 6, 2026
e7fbf37
Merge branch 'main' into tools/pm-31884/send-access-controls-policy
mcamirault May 8, 2026
ee4c853
Merge branch 'main' into tools/pm-31884/send-access-controls-policy
mcamirault May 13, 2026
c55aa58
Merge branch 'tools/pm-31884/send-access-controls-policy' into tools/…
mcamirault May 13, 2026
1436fb2
Fix data protection merge bug
mcamirault May 13, 2026
8a5b35a
Merge branch 'tools/pm-31884/send-access-controls-policy' into tools/…
mcamirault May 13, 2026
d768e97
Merge branch 'main' into tools/pm-31884/send-access-controls-policy
mcamirault May 15, 2026
3b628bd
Update date of data migration file
mcamirault May 15, 2026
5ee54bb
Merge branch 'tools/pm-31884/send-access-controls-policy' into tools/…
mcamirault May 15, 2026
bb074e8
Merge branch 'main' into tools/pm-32187/restrict-send-type-policy
mcamirault Jun 17, 2026
c358aaa
Fix merge conflicts, tests, update approach to storing Send type rest…
mcamirault Jun 22, 2026
f00128d
Address review comments, formatting issues, duplicate migration script
mcamirault Jun 22, 2026
9aa50d9
Add update script for Send_ReadIdsByOrganizationId proc
mcamirault Jun 22, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
ο»Ώusing System.ComponentModel.DataAnnotations;
using Bit.Core.Tools.Enums;

namespace Bit.Core.AdminConsole.Models.Data.Organizations.Policies;

Expand All @@ -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; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -29,6 +30,11 @@ public class SendControlsPolicyRequirement : IPolicyRequirement
/// Indicates the domains the emails of an email-protected Send must use
/// </summary>
public string? AllowedDomains { get; init; }

/// <summary>
/// Indicates the types of Send that can be created
/// </summary>
public SendType[]? AllowedSendTypes { get; init; }
}

public class SendControlsPolicyRequirementFactory : BasePolicyRequirementFactory<SendControlsPolicyRequirement>
Expand All @@ -46,7 +52,8 @@ public override SendControlsPolicyRequirement Create(IEnumerable<PolicyDetails>
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,
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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"))}");
}
Comment on lines +107 to +110

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ DEBT: New AllowedSendTypes creation-time guard has no unit test.

Details

This branch is the bypass-resistant server-side enforcement (the CLI skips front-end checks), so it is the most important path to cover. The sync handler's equivalent logic got a test in this PR (SendControlsSyncPolicyEventTests), but SendValidationServiceTests.cs has no case asserting that a disallowed send.Type throws and an allowed one passes.

Project convention (CLAUDE.md): "ALWAYS add unit tests (with mocking) for any new feature development." Suggest adding a ValidateUserCanSaveAsync_WhenSendTypeRestrictedByPolicy test mirroring the existing domain/auth cases.

}

public static bool SendAllEmailsHaveAllowedDomains(string? emailsString, string? domainsString)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment thread
mcamirault marked this conversation as resolved.

-- 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
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ BEGIN

INSERT INTO @UserIds
SELECT DISTINCT
UserId
[UserId]
FROM
[dbo].[Send]
WHERE
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -449,4 +449,52 @@ await sutProvider.GetDependency<ISendRepository>()
.Received(1)
.UpdateManyDisabledAsync(Arg.Is<List<Guid>>(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<SendControlsSyncPolicyEvent> sutProvider)
{
postUpsertedPolicy.OrganizationId = policyUpdate.OrganizationId;
existingDisableSendPolicy.OrganizationId = policyUpdate.OrganizationId;
existingSendOptionsPolicy.OrganizationId = policyUpdate.OrganizationId;
postUpsertedPolicy.SetDataModel(new SendControlsPolicyData { AllowedSendTypes = [SendType.Text] });

sutProvider.GetDependency<IPolicyRepository>()
.GetByOrganizationIdTypeAsync(policyUpdate.OrganizationId, PolicyType.DisableSend)
.Returns(existingDisableSendPolicy);
sutProvider.GetDependency<IPolicyRepository>()
.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<Guid>([compliantSend.Id, nonCompliantSend.Id]);
sutProvider.GetDependency<ISendRepository>()
.GetIdsByOrganizationIdAsync(policyUpdate.OrganizationId)
.Returns(sendIds);
sutProvider.GetDependency<ISendRepository>()
.GetManyByIdsAsync(Arg.Any<IEnumerable<Guid>>())
.Returns([compliantSend, nonCompliantSend]);

await sutProvider.Sut.ExecutePostUpsertSideEffectAsync(
new SavePolicyModel(policyUpdate), postUpsertedPolicy, null);

await sutProvider.GetDependency<ISendRepository>()
.Received(1)
.UpdateManyDisabledAsync(Arg.Is<List<Guid>>(l => l.Count() == 1 && l.ElementAt(0) == compliantSend.Id), false);
await sutProvider.GetDependency<ISendRepository>()
.Received(1)
.UpdateManyDisabledAsync(Arg.Is<List<Guid>>(l => l.Count() == 1 && l.ElementAt(0) == nonCompliantSend.Id), true);
}
}
54 changes: 45 additions & 9 deletions test/Infrastructure.IntegrationTest/packages.lock.json

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not certain if this file should be checked in

Original file line number Diff line number Diff line change
Expand Up @@ -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]",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand All @@ -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",
Expand Down Expand Up @@ -1518,15 +1554,15 @@
"infrastructure.dapper": {
"type": "Project",
"dependencies": {
"Core": "[2026.6.0, )",
"Core": "[2026.6.1, )",
"Dapper": "[2.1.66, 2.1.66]"
}
},
"infrastructure.entityframework": {
"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]",
Expand All @@ -1539,30 +1575,30 @@
"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]"
}
},
"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, )"
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Loading