Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 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
0732302
Merge branch 'main' into tools/pm-31884/send-access-controls-policy
mcamirault Apr 27, 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
@@ -0,0 +1,8 @@
namespace Bit.Core.AdminConsole.Models.Data.Organizations.Policies;

public enum SendWhoCanAccessType
{
Any,
PasswordProtected,
SpecificPeople
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,9 @@ public class SendControlsPolicyData : IPolicyDataModel
public bool DisableSend { get; set; }
[Display(Name = "DisableHideEmail")]
public bool DisableHideEmail { get; set; }
[Display(Name = "AllowedAccessControl")]
public SendWhoCanAccessType? WhoCanAccess { get; set; }
[Display(Name = "AllowedDomains")]
[StringLength(1000)]
public string? AllowedDomains { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyUpdateEvents.Interfaces;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Tools.Enums;
using Bit.Core.Tools.Repositories;
using Bit.Core.Tools.Services;

namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyEventHandlers;

Expand All @@ -13,7 +16,8 @@ namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyEventHandler
/// </summary>
public class SendControlsSyncPolicyEvent(
IPolicyRepository policyRepository,
TimeProvider timeProvider) : IOnPolicyPostUpdateEvent
TimeProvider timeProvider,
ISendRepository sendRepository) : IOnPolicyPostUpdateEvent, IPolicyValidationEvent
{
public PolicyType Type => PolicyType.SendControls;

Expand All @@ -37,6 +41,8 @@ await UpsertLegacyPolicyAsync(
PolicyType.SendOptions,
enabled: postUpsertedPolicyState.Enabled && sendControlsPolicyData.DisableHideEmail,
policyData: sendOptionsData);

await UpdateSendsByPolicyAsync(postUpsertedPolicyState, sendControlsPolicyData);
}

private async Task UpsertLegacyPolicyAsync<T>(
Expand All @@ -60,4 +66,49 @@ private async Task UpsertLegacyPolicyAsync<T>(

await policyRepository.UpsertAsync(policy);
}

public Task<string> ValidateAsync(SavePolicyModel policyRequest, Policy? currentPolicy)
{
var dataModel = policyRequest.PolicyUpdate.GetDataModel<SendControlsPolicyData>();
if (dataModel.AllowedDomains is not null && dataModel.WhoCanAccess != SendWhoCanAccessType.SpecificPeople)
{
return Task.FromResult("Allowed domains can only be set when the required access type is set to specific people");
}
return Task.FromResult(string.Empty);
}

private async Task UpdateSendsByPolicyAsync(Policy postUpsertedPolicyState, SendControlsPolicyData sendControlsPolicyData)
{
var orgSendIds = await sendRepository.GetIdsByOrganizationIdAsync(postUpsertedPolicyState.OrganizationId);
foreach (var sendIdsChunk in orgSendIds.Chunk(50))
{
var enabled = new List<Guid>();
var disabled = new List<Guid>();
var sendsChunk = await sendRepository.GetManyByIdsAsync(sendIdsChunk);
foreach (var send in sendsChunk)
{
if (
// If the policy is disabled then we want to re-enable any Sends that were previously disabled
postUpsertedPolicyState.Enabled &&
(sendControlsPolicyData.DisableSend ||
(sendControlsPolicyData.DisableHideEmail && (send.HideEmail ?? false)) ||
(sendControlsPolicyData.WhoCanAccess == SendWhoCanAccessType.PasswordProtected && send.AuthType != AuthType.Password) ||
(sendControlsPolicyData.WhoCanAccess == SendWhoCanAccessType.SpecificPeople && send.AuthType != AuthType.Email) ||
(sendControlsPolicyData.WhoCanAccess == SendWhoCanAccessType.SpecificPeople && !SendValidationService.SendAllEmailsHaveAllowedDomains(send.Emails, sendControlsPolicyData.AllowedDomains))))
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.

Suggested change
(sendControlsPolicyData.WhoCanAccess == SendWhoCanAccessType.SpecificPeople && !SendValidationService.SendAllEmailsHaveAllowedDomains(send.Emails, sendControlsPolicyData.AllowedDomains))))
(sendControlsPolicyData.WhoCanAccess == SendWhoCanAccessType.SpecificPeople
&& sendControlsPolicyData.AllowedDomains != null
&& !SendValidationService.SendAllEmailsHaveAllowedDomains(send.Emails, sendControlsPolicyData.AllowedDomains))))

❓ without the suggested change, does the sync logic exactly match the validation logic?

I think the current logic in SendValidationService will allow create and edit operations on Sends when the allowed domain list is null, but that the sync logic will disable every existing Send when the allowed domain list is null or empty.

The suggested change applies the same null guard used in SendValidationService to the sync logic.

{
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.

Claude review suggested that a malformed email could cause a silent abort of the sync logic, what do you think? Are you confident that existing email validation would prevent this kind of situation?

If not, here is the recommendation. I am undecided on this, since I recall implementing email validation logic on the clients.

Suggested change
{
// - Policy disabled β†’ fall through to `else` β†’ re-enable every Send (restores anything this policy had previously disabled).
// - Policy enabled β†’ check compliance; non-compliant Sends get disabled, compliant ones get (re-)enabled.
if (postUpsertedPolicyState.Enabled && IsNonCompliant(send, sendControlsPolicyData))

With the addition of a helper method:

private static bool IsNonCompliant(Send send, SendControlsPolicyData policyData)
  {
      if (policyData.DisableSend)                                                                                                                                                                                                                                      
      {
          return true;                                                                                                                                                                                                                                                 
      }                
      if (policyData.DisableHideEmail && (send.HideEmail ?? false))                                                                                                                                                                                                    
      {
          return true;                                                                                                                                                                                                                                                 
      }                
      if (policyData.WhoCanAccess == SendWhoCanAccessType.PasswordProtected
          && send.AuthType != AuthType.Password)                                                                                                                                                                                                                       
      {
          return true;                                                                                                                                                                                                                                                 
      }                
      if (policyData.WhoCanAccess == SendWhoCanAccessType.SpecificPeople)
      {                                                                                                                                                                                                                                                                
          if (send.AuthType != AuthType.Email)
          {                                                                                                                                                                                                                                                            
              return true;
          }
          try
          {
              if (!SendValidationService.SendAllEmailsHaveAllowedDomains(send.Emails, policyData.AllowedDomains))
              {                                                                                                                                                                                                                                                        
                  return true;
              }                                                                                                                                                                                                                                                        
          }            
          catch (BadRequestException)
          {                                                                                                                                                                                                                                                            
              // Historical Send rows may contain a malformed email that MailAddress rejects.
              // We can't verify such a Send against the allowed-domains list, so treat it as                                                                                                                                                                          
              // non-compliant and disable it rather than aborting the org-wide sweep.
              return true;                                                                                                                                                                                                                                             
          }
      }                                                                                                                                                                                                                                                                
      return false;    
  }

disabled.Add(send.Id);
} else
{
enabled.Add(send.Id);
}
}
if (enabled.Count > 0) {
await sendRepository.UpdateManyDisabledAsync(enabled, false);
}
if (disabled.Count > 0)
{
await sendRepository.UpdateManyDisabledAsync(disabled, true);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,16 @@ public class SendControlsPolicyRequirement : IPolicyRequirement
/// Indicates whether the user is prohibited from hiding their email from the recipient of a Send.
/// </summary>
public bool DisableHideEmail { get; init; }

/// <summary>
/// Indicates the access control that a user must specify on their Sends
/// </summary>
public SendWhoCanAccessType? WhoCanAccess { get; init; }

/// <summary>
/// Indicates the domains the emails of an email-protected Send must use
/// </summary>
public string? AllowedDomains { get; init; }
}

public class SendControlsPolicyRequirementFactory : BasePolicyRequirementFactory<SendControlsPolicyRequirement>
Expand All @@ -35,6 +45,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
Comment thread
mcamirault marked this conversation as resolved.
});
}
}
20 changes: 20 additions & 0 deletions src/Core/Tools/Repositories/ISendRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -78,4 +78,24 @@ public interface ISendRepository : IRepository<Send, Guid>
/// <param name="sends">A list of sends with updated data</param>
UpdateEncryptedDataForKeyRotation UpdateForKeyRotation(Guid userId,
IEnumerable<Send> sends);

/// <summary>
/// Updates the 'Disabled' field for Sends by IDs in bulk
/// </summary>
/// <param name="ids">A list of Send IDs to update</param>
/// <param name="disabled">The value to set the 'Disabled' field to</param>
Task UpdateManyDisabledAsync(IEnumerable<Guid> ids, bool disabled);

/// <summary>
/// Fetches the IDs of all <see cref="Send"/>ss of all Users that are members of an Organization
/// </summary>
/// <param name="organizationId">The ID of the organization to fetch Sends for</param>
Task<IEnumerable<Guid>> GetIdsByOrganizationIdAsync(Guid organizationId);

/// <summary>
/// Load <see cref="Send"/>s in bulk by IDs
/// </summary>
/// <param name="ids">The IDs of the <see cref="Send"/>ss to load</param>
/// <returns></returns>
Task<ICollection<Send>> GetManyByIdsAsync(IEnumerable<Guid> ids);
}
42 changes: 35 additions & 7 deletions src/Core/Tools/SendFeatures/Services/SendValidationService.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
ο»Ώ// FIXME: Update this file to be null safe and then delete the line below

#nullable disable

using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
using Bit.Core.Billing.Pricing;
Expand All @@ -10,6 +9,7 @@
using Bit.Core.Services;
using Bit.Core.Settings;
using Bit.Core.Tools.Entities;
using Bit.Core.Utilities;

namespace Bit.Core.Tools.Services;

Expand Down Expand Up @@ -48,13 +48,13 @@ public async Task ValidateUserCanSaveAsync(Guid? userId, Send send)
}

// Once data migration has run, query only SendControls
// var sendControlsTask = _policyRequirementQuery.GetAsync<SendControlsPolicyRequirement>(userId.Value);
var sendControlsTask = _policyRequirementQuery.GetAsync<SendControlsPolicyRequirement>(userId.Value);
var disableSendTask = _policyRequirementQuery.GetAsync<DisableSendPolicyRequirement>(userId.Value);
var sendOptionsTask = _policyRequirementQuery.GetAsync<SendOptionsPolicyRequirement>(userId.Value);

await Task.WhenAll(disableSendTask, sendOptionsTask);
await Task.WhenAll(sendControlsTask, disableSendTask, sendOptionsTask);

// var sendControlsRequirement = sendControlsTask.Result;
var sendControlsRequirement = sendControlsTask.Result;
var disableSendRequirement = disableSendTask.Result;
var sendOptionsRequirement = sendOptionsTask.Result;

Expand All @@ -68,14 +68,42 @@ public async Task ValidateUserCanSaveAsync(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 passwordRequired = sendControlsRequirement.WhoCanAccess == SendWhoCanAccessType.PasswordProtected;
var emailsRequired = sendControlsRequirement.WhoCanAccess == SendWhoCanAccessType.SpecificPeople;
if ((passwordRequired && send.Password == null) || (emailsRequired && send.Emails == null))
{
var requiredAccessControl = passwordRequired ? "password" : emailsRequired ? "email verification" : "(cannot determine required auth)";
throw new BadRequestException($"Due to an Enterprise Policy your Sends must be protected by {requiredAccessControl}");
}

if (emailsRequired && sendControlsRequirement.AllowedDomains != null)
{
if (!SendAllEmailsHaveAllowedDomains(send.Emails, sendControlsRequirement.AllowedDomains))
{
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}");
}
}
}

public static bool SendAllEmailsHaveAllowedDomains(string? emailsString, string? domainsString)
{
var domains = (domainsString ?? "").Split(",", StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
var emails = (emailsString ?? "").Split(",", StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
return emails.All(email => domains.Any(domain =>
{
var emailDomain = EmailValidation.GetDomain(email);
return emailDomain.Equals(domain, StringComparison.OrdinalIgnoreCase)
|| emailDomain.EndsWith("." + domain, StringComparison.OrdinalIgnoreCase);
}));
}

public async Task<long> StorageRemainingForSendAsync(Send send)
{
var storageBytesRemaining = 0L;
if (send.UserId.HasValue)
{
var user = await _userRepository.GetByIdAsync(send.UserId.Value);
var user = await _userRepository.GetByIdAsync(send.UserId.Value) ?? throw new NotFoundException("Send user not found");
if (!await _userService.CanAccessPremium(user))
{
throw new BadRequestException("You must have premium status to use file Sends.");
Expand Down Expand Up @@ -110,7 +138,7 @@ public async Task<long> StorageRemainingForSendAsync(Send send)
}
else if (send.OrganizationId.HasValue)
{
var org = await _organizationRepository.GetByIdAsync(send.OrganizationId.Value);
var org = await _organizationRepository.GetByIdAsync(send.OrganizationId.Value) ?? throw new NotFoundException("Send organization not found");
if (!org.MaxStorageGb.HasValue)
{
throw new BadRequestException("This organization cannot use file sends.");
Expand Down
31 changes: 31 additions & 0 deletions src/Infrastructure.Dapper/Tools/Repositories/SendRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,37 @@ INNER JOIN
};
}

public async Task UpdateManyDisabledAsync(IEnumerable<Guid> ids, bool disabled)
{
using var connection = new SqlConnection(ConnectionString);
await connection.ExecuteAsync(
$"[{Schema}].[Send_UpdateDisabledByIds]",
new { Ids = ids.ToGuidIdArrayTVP(), Disabled = disabled },
commandType: CommandType.StoredProcedure);
}

public async Task<IEnumerable<Guid>> GetIdsByOrganizationIdAsync(Guid organizationId)
{
using var connection = new SqlConnection(ConnectionString);
var sendIds = await connection.QueryAsync<Guid>(
$"[{Schema}].[Send_ReadIdsByOrgId]",
new { Id = organizationId },
commandType: CommandType.StoredProcedure);
return sendIds;
}

public async Task<ICollection<Send>> GetManyByIdsAsync(IEnumerable<Guid> ids)
{
using var connection = new SqlConnection(ConnectionString);
var results = await connection.QueryAsync<Send>(
$"[{Schema}].[Send_ReadByIds]",
new { Ids = ids.ToGuidIdArrayTVP() },
commandType: CommandType.StoredProcedure);
var sends = results.ToList();
UnprotectData(sends);
return sends;
}

private async Task ProtectDataAndSaveAsync(Send send, Func<Task> saveTask)
{
if (send == null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,4 +132,35 @@ public UpdateEncryptedDataForKeyRotation UpdateForKeyRotation(Guid userId,
};
}

/// <inheritdoc />
public async Task UpdateManyDisabledAsync(IEnumerable<Guid> ids, bool disabled)
{
using var scope = ServiceScopeFactory.CreateScope();
var dbContext = GetDatabaseContext(scope);
var sends = dbContext.Sends.Where(s => ids.Contains(s.Id));
await sends.ExecuteUpdateAsync(setters => setters
.SetProperty(s => s.Disabled, disabled)
.SetProperty(s => s.RevisionDate, DateTime.UtcNow)
);
var userIds = await sends.Select(s => s.User.Id).ToArrayAsync() ?? [];
await dbContext.UserBumpManyAccountRevisionDatesAsync(userIds);
await dbContext.SaveChangesAsync();
}

public async Task<IEnumerable<Guid>> GetIdsByOrganizationIdAsync(Guid organizationId)
{
using var scope = ServiceScopeFactory.CreateScope();
var dbContext = GetDatabaseContext(scope);
var orgUsers = await dbContext.OrganizationUsers.Where(ou => ou.OrganizationId == organizationId).ToListAsync();
var orgUserSendIds = await dbContext.Sends.Where(s => orgUsers.Any(ou => ou.UserId == s.UserId)).Select(s => s.Id).ToListAsync();
return Mapper.Map<List<Guid>>(orgUserSendIds);
}

public async Task<ICollection<Core.Tools.Entities.Send>> GetManyByIdsAsync(IEnumerable<Guid> ids)
{
using var scope = ServiceScopeFactory.CreateScope();
var dbContext = GetDatabaseContext(scope);
var results = await dbContext.Sends.Where(s => ids.Contains(s.Id)).ToListAsync();
return Mapper.Map<List<Core.Tools.Entities.Send>>(results);
}
}
13 changes: 13 additions & 0 deletions src/Sql/dbo/Tools/Stored Procedures/Send_ReadByIds.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
CREATE PROCEDURE [dbo].[Send_ReadByIds]
@Ids AS [dbo].[GuidIdArray] READONLY
AS
BEGIN
SET NOCOUNT ON

SELECT
*
FROM
[dbo].[SendView]
WHERE
[Id] IN (SELECT * FROM @Ids)
END
24 changes: 24 additions & 0 deletions src/Sql/dbo/Tools/Stored Procedures/Send_ReadIdsByOrgId.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
CREATE PROCEDURE [dbo].[Send_ReadIdsByOrgId]
@Id 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 = @Id

-- Get the IDs of all Sends associated with those users --
SELECT
Id
FROM
[dbo].[SendView]
WHERE
UserId IN (SELECT [Id] FROM @OrgUserIds)
END
Comment thread
mcamirault marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
CREATE PROCEDURE [dbo].[Send_UpdateDisabledByIds]
@Ids AS [dbo].[GuidIdArray] READONLY,
@Disabled BIT
AS
BEGIN
SET NOCOUNT ON

DECLARE @UserIds [dbo].[GuidIdarray]

-- Set field
UPDATE
[dbo].[Send]
SET
[Disabled] = @Disabled,
[RevisionDate] = GETUTCDATE()
WHERE
[Id] IN (SELECT * FROM @Ids)

INSERT INTO @UserIds
SELECT DISTINCT
UserId
FROM
[dbo].[Send]
WHERE
[Id] IN (SELECT * FROM @Ids)
AND [UserId] IS NOT NULL

-- Bump account revision dates
EXEC [dbo].[User_BumpManyAccountRevisionDates] @UserIds
END
Loading
Loading