From 34f8450c9ab0648b94ab6cc72769918d7af33b31 Mon Sep 17 00:00:00 2001 From: Mike Amirault Date: Tue, 14 Jul 2026 13:16:47 -0400 Subject: [PATCH 1/2] [PM-39998] Omit org owner and admin Sends from policy enforcement --- .../SendControlsSyncPolicyEvent.cs | 10 ++- .../SendControlsSyncPolicyEventTests.cs | 69 +++++++++++++++++++ 2 files changed, 77 insertions(+), 2 deletions(-) diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyEventHandlers/SendControlsSyncPolicyEvent.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyEventHandlers/SendControlsSyncPolicyEvent.cs index f7c043776c9c..11ad32b51bae 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyEventHandlers/SendControlsSyncPolicyEvent.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyEventHandlers/SendControlsSyncPolicyEvent.cs @@ -5,6 +5,7 @@ using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyUpdateEvents.Interfaces; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Exceptions; +using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Tools.Entities; using Bit.Core.Tools.Enums; @@ -21,7 +22,8 @@ public class SendControlsSyncPolicyEvent( IPolicyRepository policyRepository, TimeProvider timeProvider, ISendRepository sendRepository, - IFeatureService featureService) : IOnPolicyPostUpdateEvent, IPolicyValidationEvent + IFeatureService featureService, + IOrganizationUserRepository orgUserRepository) : IOnPolicyPostUpdateEvent, IPolicyValidationEvent { public PolicyType Type => PolicyType.SendControls; @@ -88,6 +90,9 @@ public Task ValidateAsync(SavePolicyModel policyRequest, Policy? current private async Task UpdateSendsByPolicyAsync(Policy postUpsertedPolicyState, SendControlsPolicyData sendControlsPolicyData) { var orgSendIds = await sendRepository.GetIdsByOrganizationIdAsync(postUpsertedPolicyState.OrganizationId); + // We fetch all of the owners and admins in the org so we can ignore their Sends when enforcing policy compliance + // This could be a heavy call in theory but in practice owners and admins should be a minority of org users + var orgOwnerAndAdminUserIds = (await orgUserRepository.GetManyByMinimumRoleAsync(postUpsertedPolicyState.OrganizationId, Core.Enums.OrganizationUserType.Admin)).Select(oud => oud.GetUserId()); foreach (var sendIdsChunk in orgSendIds.Chunk(50)) { var enabled = new List(); @@ -97,7 +102,8 @@ private async Task UpdateSendsByPolicyAsync(Policy postUpsertedPolicyState, Send { if ( // If the policy is disabled then we want to re-enable any Sends that were previously disabled - postUpsertedPolicyState.Enabled && SendIsNonCompliant(send, sendControlsPolicyData)) + // If the Send was created by an Owner or an Admin in the organization we ignore it + postUpsertedPolicyState.Enabled && !orgOwnerAndAdminUserIds.Contains(send.UserId) && SendIsNonCompliant(send, sendControlsPolicyData)) { disabled.Add(send.Id); } diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyEventHandlers/SendControlsSyncPolicyEventTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyEventHandlers/SendControlsSyncPolicyEventTests.cs index 2f04d04b91d6..423d614eb728 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyEventHandlers/SendControlsSyncPolicyEventTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyEventHandlers/SendControlsSyncPolicyEventTests.cs @@ -4,6 +4,8 @@ using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models; using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyEventHandlers; using Bit.Core.AdminConsole.Repositories; +using Bit.Core.Models.Data.Organizations.OrganizationUsers; +using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Test.AdminConsole.AutoFixture; using Bit.Core.Tools.Entities; @@ -575,4 +577,71 @@ await sutProvider.GetDependency() .Received(1) .UpdateManyDisabledAsync(Arg.Is>(l => l.Count() == 1 && l.ElementAt(0) == nonCompliantSend.Id), true); } + + [Theory, BitAutoData] + public async Task ExecutePostUpsertSideEffectAsync_IgnoresOwnersAndAdminsNonCompliantSends( + [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); + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.SendControlsExistingSends) + .Returns(true); + + var adminOrganizationUser = new OrganizationUserUserDetails + { + UserId = Guid.NewGuid(), + Type = Enums.OrganizationUserType.Admin, + }; + var adminNoncompliantSend = new Send + { + Id = Guid.NewGuid(), + Type = SendType.File, + UserId = adminOrganizationUser.UserId, + }; + var ownerOrganizationUser = new OrganizationUserUserDetails + { + UserId = Guid.NewGuid(), + Type = Enums.OrganizationUserType.Owner, + }; + var ownerNoncompliantSend = new Send + { + Id = Guid.NewGuid(), + Type = SendType.File, + UserId = ownerOrganizationUser.UserId, + }; + var sendIds = new List([adminNoncompliantSend.Id, ownerNoncompliantSend.Id]); + sutProvider.GetDependency() + .GetIdsByOrganizationIdAsync(policyUpdate.OrganizationId) + .Returns(sendIds); + sutProvider.GetDependency() + .GetManyByIdsAsync(Arg.Any>()) + .Returns([adminNoncompliantSend, ownerNoncompliantSend]); + sutProvider.GetDependency() + .GetManyByMinimumRoleAsync(policyUpdate.OrganizationId, Enums.OrganizationUserType.Admin) + .Returns([adminOrganizationUser, ownerOrganizationUser]); + + await sutProvider.Sut.ExecutePostUpsertSideEffectAsync( + new SavePolicyModel(policyUpdate), postUpsertedPolicy, null); + + await sutProvider.GetDependency() + .Received(1) + .UpdateManyDisabledAsync(Arg.Is>(l => l.Count == 2 && l.Contains(adminNoncompliantSend.Id) && l.Contains(ownerNoncompliantSend.Id)), false); + await sutProvider.GetDependency() + .Received(0) + .UpdateManyDisabledAsync(Arg.Any>(), true); + } } From f427c243b3621d2eafa3659b2bf4ee4bf2fe1d73 Mon Sep 17 00:00:00 2001 From: Mike Amirault Date: Tue, 14 Jul 2026 13:54:44 -0400 Subject: [PATCH 2/2] Add UserId to EF projection --- .../AdminConsole/Repositories/OrganizationUserRepository.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationUserRepository.cs b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationUserRepository.cs index 09ca0d937ecb..788e15d07063 100644 --- a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationUserRepository.cs +++ b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationUserRepository.cs @@ -806,7 +806,8 @@ public async Task> GetManyByMinimumRole .Select(e => new OrganizationUserUserDetails() { Id = e.Id, - Email = e.Email ?? e.User.Email + Email = e.Email ?? e.User.Email, + UserId = e.UserId ?? e.User.Id }); return await query.ToListAsync(); }