[PM-28555] Add idempotent sproc to create My Items collections (#6801)

* Add sproc to create multiple default collections. 
  SqlBulkCopy implementation is overkill for most cases.
  This provides a lighter weight sproc implementation for smaller
  data sets.
* DRY up collection arrangement
* DRY up tests because bulk and non-bulk share same behavior
* use EF native AddRange instead of bulk insert, because
  we expect smaller data sizes on self-host
This commit is contained in:
Thomas Rittson
2026-01-16 08:49:25 +10:00
committed by GitHub
parent 51d90cce3d
commit ebb0712e33
17 changed files with 449 additions and 195 deletions

View File

@@ -0,0 +1,53 @@
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Utilities;
namespace Bit.Core.AdminConsole.OrganizationFeatures.Collections;
public static class CollectionUtils
{
/// <summary>
/// Arranges Collection and CollectionUser objects to create default user collections.
/// </summary>
/// <param name="organizationId">The organization ID.</param>
/// <param name="organizationUserIds">The IDs for organization users who need default collections.</param>
/// <param name="defaultCollectionName">The encrypted string to use as the default collection name.</param>
/// <returns>A tuple containing the collections and collection users.</returns>
public static (ICollection<Collection> collections, ICollection<CollectionUser> collectionUsers)
BuildDefaultUserCollections(Guid organizationId, IEnumerable<Guid> organizationUserIds,
string defaultCollectionName)
{
var now = DateTime.UtcNow;
var collectionUsers = new List<CollectionUser>();
var collections = new List<Collection>();
foreach (var orgUserId in organizationUserIds)
{
var collectionId = CoreHelpers.GenerateComb();
collections.Add(new Collection
{
Id = collectionId,
OrganizationId = organizationId,
Name = defaultCollectionName,
CreationDate = now,
RevisionDate = now,
Type = CollectionType.DefaultUserCollection,
DefaultUserCollectionEmail = null
});
collectionUsers.Add(new CollectionUser
{
CollectionId = collectionId,
OrganizationUserId = orgUserId,
ReadOnly = false,
HidePasswords = false,
Manage = true,
});
}
return (collections, collectionUsers);
}
}

View File

@@ -4,9 +4,7 @@ using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.OrganizationConfirmation; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.OrganizationConfirmation;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies; using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
using Bit.Core.Entities;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Models.Data;
using Bit.Core.Platform.Push; using Bit.Core.Platform.Push;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Services; using Bit.Core.Services;
@@ -83,19 +81,10 @@ public class AutomaticallyConfirmOrganizationUserCommand(IOrganizationUserReposi
return; return;
} }
await collectionRepository.CreateAsync( await collectionRepository.CreateDefaultCollectionsAsync(
new Collection request.Organization!.Id,
{ [request.OrganizationUser!.Id],
OrganizationId = request.Organization!.Id, request.DefaultUserCollectionName);
Name = request.DefaultUserCollectionName,
Type = CollectionType.DefaultUserCollection
},
groups: null,
[new CollectionAccessSelection
{
Id = request.OrganizationUser!.Id,
Manage = true
}]);
} }
catch (Exception ex) catch (Exception ex)
{ {

View File

@@ -14,7 +14,6 @@ using Bit.Core.Billing.Enums;
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
using Bit.Core.Models.Data;
using Bit.Core.Platform.Push; using Bit.Core.Platform.Push;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Services; using Bit.Core.Services;
@@ -294,21 +293,10 @@ public class ConfirmOrganizationUserCommand : IConfirmOrganizationUserCommand
return; return;
} }
var defaultCollection = new Collection await _collectionRepository.CreateDefaultCollectionsAsync(
{ organizationUser.OrganizationId,
OrganizationId = organizationUser.OrganizationId, [organizationUser.Id],
Name = defaultUserCollectionName, defaultUserCollectionName);
Type = CollectionType.DefaultUserCollection
};
var collectionUser = new CollectionAccessSelection
{
Id = organizationUser.Id,
ReadOnly = false,
HidePasswords = false,
Manage = true
};
await _collectionRepository.CreateAsync(defaultCollection, groups: null, users: [collectionUser]);
} }
/// <summary> /// <summary>
@@ -339,7 +327,7 @@ public class ConfirmOrganizationUserCommand : IConfirmOrganizationUserCommand
return; return;
} }
await _collectionRepository.UpsertDefaultCollectionsAsync(organizationId, eligibleOrganizationUserIds, defaultUserCollectionName); await _collectionRepository.CreateDefaultCollectionsAsync(organizationId, eligibleOrganizationUserIds, defaultUserCollectionName);
} }
/// <summary> /// <summary>

View File

@@ -57,14 +57,15 @@ public class OrganizationDataOwnershipPolicyValidator(
var userOrgIds = requirements var userOrgIds = requirements
.Select(requirement => requirement.GetDefaultCollectionRequestOnPolicyEnable(policyUpdate.OrganizationId)) .Select(requirement => requirement.GetDefaultCollectionRequestOnPolicyEnable(policyUpdate.OrganizationId))
.Where(request => request.ShouldCreateDefaultCollection) .Where(request => request.ShouldCreateDefaultCollection)
.Select(request => request.OrganizationUserId); .Select(request => request.OrganizationUserId)
.ToList();
if (!userOrgIds.Any()) if (!userOrgIds.Any())
{ {
return; return;
} }
await collectionRepository.UpsertDefaultCollectionsAsync( await collectionRepository.CreateDefaultCollectionsBulkAsync(
policyUpdate.OrganizationId, policyUpdate.OrganizationId,
userOrgIds, userOrgIds,
defaultCollectionName); defaultCollectionName);

View File

@@ -64,11 +64,22 @@ public interface ICollectionRepository : IRepository<Collection, Guid>
IEnumerable<CollectionAccessSelection> users, IEnumerable<CollectionAccessSelection> groups); IEnumerable<CollectionAccessSelection> users, IEnumerable<CollectionAccessSelection> groups);
/// <summary> /// <summary>
/// Creates default user collections for the specified organization users if they do not already have one. /// Creates default user collections for the specified organization users.
/// Filters internally to only create collections for users who don't already have one.
/// </summary> /// </summary>
/// <param name="organizationId">The Organization ID.</param> /// <param name="organizationId">The Organization ID.</param>
/// <param name="organizationUserIds">The Organization User IDs to create default collections for.</param> /// <param name="organizationUserIds">The Organization User IDs to create default collections for.</param>
/// <param name="defaultCollectionName">The encrypted string to use as the default collection name.</param> /// <param name="defaultCollectionName">The encrypted string to use as the default collection name.</param>
/// <returns></returns> Task CreateDefaultCollectionsAsync(Guid organizationId, IEnumerable<Guid> organizationUserIds, string defaultCollectionName);
Task UpsertDefaultCollectionsAsync(Guid organizationId, IEnumerable<Guid> organizationUserIds, string defaultCollectionName);
/// <summary>
/// Creates default user collections for the specified organization users using bulk insert operations.
/// Use this if you need to create collections for > ~1k users.
/// Filters internally to only create collections for users who don't already have one.
/// </summary>
/// <param name="organizationId">The Organization ID.</param>
/// <param name="organizationUserIds">The Organization User IDs to create default collections for.</param>
/// <param name="defaultCollectionName">The encrypted string to use as the default collection name.</param>
Task CreateDefaultCollectionsBulkAsync(Guid organizationId, IEnumerable<Guid> organizationUserIds, string defaultCollectionName);
} }

View File

@@ -160,6 +160,21 @@ public static class DapperHelpers
return ids.ToArrayTVP("GuidId"); return ids.ToArrayTVP("GuidId");
} }
public static DataTable ToTwoGuidIdArrayTVP(this IEnumerable<(Guid id1, Guid id2)> values)
{
var table = new DataTable();
table.SetTypeName("[dbo].[TwoGuidIdArray]");
table.Columns.Add("Id1", typeof(Guid));
table.Columns.Add("Id2", typeof(Guid));
foreach (var value in values)
{
table.Rows.Add(value.id1, value.id2);
}
return table;
}
public static DataTable ToArrayTVP<T>(this IEnumerable<T> values, string columnName) public static DataTable ToArrayTVP<T>(this IEnumerable<T> values, string columnName)
{ {
var table = new DataTable(); var table = new DataTable();

View File

@@ -1,6 +1,7 @@
using System.Data; using System.Data;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Text.Json; using System.Text.Json;
using Bit.Core.AdminConsole.OrganizationFeatures.Collections;
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Models.Data; using Bit.Core.Models.Data;
@@ -360,7 +361,45 @@ public class CollectionRepository : Repository<Collection, Guid>, ICollectionRep
} }
} }
public async Task UpsertDefaultCollectionsAsync(Guid organizationId, IEnumerable<Guid> organizationUserIds, string defaultCollectionName) public async Task CreateDefaultCollectionsAsync(Guid organizationId, IEnumerable<Guid> organizationUserIds, string defaultCollectionName)
{
organizationUserIds = organizationUserIds.ToList();
if (!organizationUserIds.Any())
{
return;
}
var organizationUserCollectionIds = organizationUserIds
.Select(ou => (ou, CoreHelpers.GenerateComb()))
.ToTwoGuidIdArrayTVP();
await using var connection = new SqlConnection(ConnectionString);
await connection.OpenAsync();
await using var transaction = connection.BeginTransaction();
try
{
await connection.ExecuteAsync(
"[dbo].[Collection_CreateDefaultCollections]",
new
{
OrganizationId = organizationId,
DefaultCollectionName = defaultCollectionName,
OrganizationUserCollectionIds = organizationUserCollectionIds
},
commandType: CommandType.StoredProcedure,
transaction: transaction);
await transaction.CommitAsync();
}
catch
{
await transaction.RollbackAsync();
throw;
}
}
public async Task CreateDefaultCollectionsBulkAsync(Guid organizationId, IEnumerable<Guid> organizationUserIds, string defaultCollectionName)
{ {
organizationUserIds = organizationUserIds.ToList(); organizationUserIds = organizationUserIds.ToList();
if (!organizationUserIds.Any()) if (!organizationUserIds.Any())
@@ -377,7 +416,8 @@ public class CollectionRepository : Repository<Collection, Guid>, ICollectionRep
var missingDefaultCollectionUserIds = organizationUserIds.Except(orgUserIdWithDefaultCollection); var missingDefaultCollectionUserIds = organizationUserIds.Except(orgUserIdWithDefaultCollection);
var (collectionUsers, collections) = BuildDefaultCollectionForUsers(organizationId, missingDefaultCollectionUserIds, defaultCollectionName); var (collections, collectionUsers) =
CollectionUtils.BuildDefaultUserCollections(organizationId, missingDefaultCollectionUserIds, defaultCollectionName);
if (!collectionUsers.Any() || !collections.Any()) if (!collectionUsers.Any() || !collections.Any())
{ {
@@ -387,11 +427,11 @@ public class CollectionRepository : Repository<Collection, Guid>, ICollectionRep
await BulkResourceCreationService.CreateCollectionsAsync(connection, transaction, collections); await BulkResourceCreationService.CreateCollectionsAsync(connection, transaction, collections);
await BulkResourceCreationService.CreateCollectionsUsersAsync(connection, transaction, collectionUsers); await BulkResourceCreationService.CreateCollectionsUsersAsync(connection, transaction, collectionUsers);
transaction.Commit(); await transaction.CommitAsync();
} }
catch catch
{ {
transaction.Rollback(); await transaction.RollbackAsync();
throw; throw;
} }
} }
@@ -421,40 +461,6 @@ public class CollectionRepository : Repository<Collection, Guid>, ICollectionRep
return organizationUserIds.ToHashSet(); return organizationUserIds.ToHashSet();
} }
private (List<CollectionUser> collectionUser, List<Collection> collection) BuildDefaultCollectionForUsers(Guid organizationId, IEnumerable<Guid> missingDefaultCollectionUserIds, string defaultCollectionName)
{
var collectionUsers = new List<CollectionUser>();
var collections = new List<Collection>();
foreach (var orgUserId in missingDefaultCollectionUserIds)
{
var collectionId = CoreHelpers.GenerateComb();
collections.Add(new Collection
{
Id = collectionId,
OrganizationId = organizationId,
Name = defaultCollectionName,
CreationDate = DateTime.UtcNow,
RevisionDate = DateTime.UtcNow,
Type = CollectionType.DefaultUserCollection,
DefaultUserCollectionEmail = null
});
collectionUsers.Add(new CollectionUser
{
CollectionId = collectionId,
OrganizationUserId = orgUserId,
ReadOnly = false,
HidePasswords = false,
Manage = true,
});
}
return (collectionUsers, collections);
}
public class CollectionWithGroupsAndUsers : Collection public class CollectionWithGroupsAndUsers : Collection
{ {
public CollectionWithGroupsAndUsers() { } public CollectionWithGroupsAndUsers() { }

View File

@@ -1,11 +1,10 @@
using AutoMapper; using AutoMapper;
using Bit.Core.AdminConsole.OrganizationFeatures.Collections;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Models.Data; using Bit.Core.Models.Data;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Utilities;
using Bit.Infrastructure.EntityFramework.Models; using Bit.Infrastructure.EntityFramework.Models;
using Bit.Infrastructure.EntityFramework.Repositories.Queries; using Bit.Infrastructure.EntityFramework.Repositories.Queries;
using LinqToDB.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
@@ -794,7 +793,7 @@ public class CollectionRepository : Repository<Core.Entities.Collection, Collect
// SaveChangesAsync is expected to be called outside this method // SaveChangesAsync is expected to be called outside this method
} }
public async Task UpsertDefaultCollectionsAsync(Guid organizationId, IEnumerable<Guid> organizationUserIds, string defaultCollectionName) public async Task CreateDefaultCollectionsAsync(Guid organizationId, IEnumerable<Guid> organizationUserIds, string defaultCollectionName)
{ {
organizationUserIds = organizationUserIds.ToList(); organizationUserIds = organizationUserIds.ToList();
if (!organizationUserIds.Any()) if (!organizationUserIds.Any())
@@ -808,15 +807,15 @@ public class CollectionRepository : Repository<Core.Entities.Collection, Collect
var orgUserIdWithDefaultCollection = await GetOrgUserIdsWithDefaultCollectionAsync(dbContext, organizationId); var orgUserIdWithDefaultCollection = await GetOrgUserIdsWithDefaultCollectionAsync(dbContext, organizationId);
var missingDefaultCollectionUserIds = organizationUserIds.Except(orgUserIdWithDefaultCollection); var missingDefaultCollectionUserIds = organizationUserIds.Except(orgUserIdWithDefaultCollection);
var (collectionUsers, collections) = BuildDefaultCollectionForUsers(organizationId, missingDefaultCollectionUserIds, defaultCollectionName); var (collections, collectionUsers) = CollectionUtils.BuildDefaultUserCollections(organizationId, missingDefaultCollectionUserIds, defaultCollectionName);
if (!collectionUsers.Any() || !collections.Any()) if (!collections.Any() || !collectionUsers.Any())
{ {
return; return;
} }
await dbContext.BulkCopyAsync(collections); await dbContext.Collections.AddRangeAsync(Mapper.Map<IEnumerable<Collection>>(collections));
await dbContext.BulkCopyAsync(collectionUsers); await dbContext.CollectionUsers.AddRangeAsync(Mapper.Map<IEnumerable<CollectionUser>>(collectionUsers));
await dbContext.SaveChangesAsync(); await dbContext.SaveChangesAsync();
} }
@@ -844,37 +843,7 @@ public class CollectionRepository : Repository<Core.Entities.Collection, Collect
return results.ToHashSet(); return results.ToHashSet();
} }
private (List<CollectionUser> collectionUser, List<Collection> collection) BuildDefaultCollectionForUsers(Guid organizationId, IEnumerable<Guid> missingDefaultCollectionUserIds, string defaultCollectionName) public Task CreateDefaultCollectionsBulkAsync(Guid organizationId, IEnumerable<Guid> organizationUserIds,
{ string defaultCollectionName) =>
var collectionUsers = new List<CollectionUser>(); CreateDefaultCollectionsAsync(organizationId, organizationUserIds, defaultCollectionName);
var collections = new List<Collection>();
foreach (var orgUserId in missingDefaultCollectionUserIds)
{
var collectionId = CoreHelpers.GenerateComb();
collections.Add(new Collection
{
Id = collectionId,
OrganizationId = organizationId,
Name = defaultCollectionName,
CreationDate = DateTime.UtcNow,
RevisionDate = DateTime.UtcNow,
Type = CollectionType.DefaultUserCollection,
DefaultUserCollectionEmail = null
});
collectionUsers.Add(new CollectionUser
{
CollectionId = collectionId,
OrganizationUserId = orgUserId,
ReadOnly = false,
HidePasswords = false,
Manage = true,
});
}
return (collectionUsers, collections);
}
} }

View File

@@ -17,8 +17,6 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using DP = Microsoft.AspNetCore.DataProtection; using DP = Microsoft.AspNetCore.DataProtection;
#nullable enable
namespace Bit.Infrastructure.EntityFramework.Repositories; namespace Bit.Infrastructure.EntityFramework.Repositories;
public class DatabaseContext : DbContext public class DatabaseContext : DbContext

View File

@@ -0,0 +1,69 @@
-- Creates default user collections for organization users
-- Filters out existing default collections at database level
CREATE PROCEDURE [dbo].[Collection_CreateDefaultCollections]
@OrganizationId UNIQUEIDENTIFIER,
@DefaultCollectionName VARCHAR(MAX),
@OrganizationUserCollectionIds AS [dbo].[TwoGuidIdArray] READONLY -- OrganizationUserId, CollectionId
AS
BEGIN
SET NOCOUNT ON
DECLARE @Now DATETIME2(7) = GETUTCDATE()
-- Filter to only users who don't have default collections
SELECT ids.Id1, ids.Id2
INTO #FilteredIds
FROM @OrganizationUserCollectionIds ids
WHERE NOT EXISTS (
SELECT 1
FROM [dbo].[CollectionUser] cu
INNER JOIN [dbo].[Collection] c ON c.Id = cu.CollectionId
WHERE c.OrganizationId = @OrganizationId
AND c.[Type] = 1 -- CollectionType.DefaultUserCollection
AND cu.OrganizationUserId = ids.Id1
);
-- Insert collections only for users who don't have default collections yet
INSERT INTO [dbo].[Collection]
(
[Id],
[OrganizationId],
[Name],
[CreationDate],
[RevisionDate],
[Type],
[ExternalId],
[DefaultUserCollectionEmail]
)
SELECT
ids.Id2, -- CollectionId
@OrganizationId,
@DefaultCollectionName,
@Now,
@Now,
1, -- CollectionType.DefaultUserCollection
NULL,
NULL
FROM
#FilteredIds ids;
-- Insert collection user mappings
INSERT INTO [dbo].[CollectionUser]
(
[CollectionId],
[OrganizationUserId],
[ReadOnly],
[HidePasswords],
[Manage]
)
SELECT
ids.Id2, -- CollectionId
ids.Id1, -- OrganizationUserId
0, -- ReadOnly = false
0, -- HidePasswords = false
1 -- Manage = true
FROM
#FilteredIds ids;
DROP TABLE #FilteredIds;
END

View File

@@ -10,7 +10,6 @@ using Bit.Core.AdminConsole.Utilities.v2;
using Bit.Core.AdminConsole.Utilities.v2.Validation; using Bit.Core.AdminConsole.Utilities.v2.Validation;
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Models.Data;
using Bit.Core.Platform.Push; using Bit.Core.Platform.Push;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Services; using Bit.Core.Services;
@@ -204,14 +203,10 @@ public class AutomaticallyConfirmUsersCommandTests
await sutProvider.GetDependency<ICollectionRepository>() await sutProvider.GetDependency<ICollectionRepository>()
.Received(1) .Received(1)
.CreateAsync( .CreateDefaultCollectionsAsync(
Arg.Is<Collection>(c => organization.Id,
c.OrganizationId == organization.Id && Arg.Is<IEnumerable<Guid>>(ids => ids.Single() == organizationUser.Id),
c.Name == defaultCollectionName && defaultCollectionName);
c.Type == CollectionType.DefaultUserCollection),
Arg.Is<IEnumerable<CollectionAccessSelection>>(groups => groups == null),
Arg.Is<IEnumerable<CollectionAccessSelection>>(access =>
access.FirstOrDefault(x => x.Id == organizationUser.Id && x.Manage) != null));
} }
[Theory] [Theory]
@@ -253,9 +248,7 @@ public class AutomaticallyConfirmUsersCommandTests
await sutProvider.GetDependency<ICollectionRepository>() await sutProvider.GetDependency<ICollectionRepository>()
.DidNotReceive() .DidNotReceive()
.CreateAsync(Arg.Any<Collection>(), .CreateDefaultCollectionsAsync(Arg.Any<Guid>(), Arg.Any<IEnumerable<Guid>>(), Arg.Any<string>());
Arg.Any<IEnumerable<CollectionAccessSelection>>(),
Arg.Any<IEnumerable<CollectionAccessSelection>>());
} }
[Theory] [Theory]
@@ -291,9 +284,7 @@ public class AutomaticallyConfirmUsersCommandTests
var collectionException = new Exception("Collection creation failed"); var collectionException = new Exception("Collection creation failed");
sutProvider.GetDependency<ICollectionRepository>() sutProvider.GetDependency<ICollectionRepository>()
.CreateAsync(Arg.Any<Collection>(), .CreateDefaultCollectionsAsync(Arg.Any<Guid>(), Arg.Any<IEnumerable<Guid>>(), Arg.Any<string>())
Arg.Any<IEnumerable<CollectionAccessSelection>>(),
Arg.Any<IEnumerable<CollectionAccessSelection>>())
.ThrowsAsync(collectionException); .ThrowsAsync(collectionException);
// Act // Act

View File

@@ -13,7 +13,6 @@ using Bit.Core.Billing.Enums;
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
using Bit.Core.Models.Data;
using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.Models.Data.Organizations.OrganizationUsers;
using Bit.Core.Platform.Push; using Bit.Core.Platform.Push;
using Bit.Core.Repositories; using Bit.Core.Repositories;
@@ -493,15 +492,10 @@ public class ConfirmOrganizationUserCommandTests
await sutProvider.GetDependency<ICollectionRepository>() await sutProvider.GetDependency<ICollectionRepository>()
.Received(1) .Received(1)
.CreateAsync( .CreateDefaultCollectionsAsync(
Arg.Is<Collection>(c => organization.Id,
c.Name == collectionName && Arg.Is<IEnumerable<Guid>>(ids => ids.Single() == orgUser.Id),
c.OrganizationId == organization.Id && collectionName);
c.Type == CollectionType.DefaultUserCollection),
Arg.Any<IEnumerable<CollectionAccessSelection>>(),
Arg.Is<IEnumerable<CollectionAccessSelection>>(cu =>
cu.Single().Id == orgUser.Id &&
cu.Single().Manage));
} }
[Theory, BitAutoData] [Theory, BitAutoData]
@@ -522,7 +516,7 @@ public class ConfirmOrganizationUserCommandTests
await sutProvider.GetDependency<ICollectionRepository>() await sutProvider.GetDependency<ICollectionRepository>()
.DidNotReceive() .DidNotReceive()
.UpsertDefaultCollectionsAsync(Arg.Any<Guid>(), Arg.Any<IEnumerable<Guid>>(), Arg.Any<string>()); .CreateDefaultCollectionsAsync(Arg.Any<Guid>(), Arg.Any<IEnumerable<Guid>>(), Arg.Any<string>());
} }
[Theory, BitAutoData] [Theory, BitAutoData]
@@ -539,24 +533,15 @@ public class ConfirmOrganizationUserCommandTests
sutProvider.GetDependency<IOrganizationUserRepository>().GetManyAsync(default).ReturnsForAnyArgs(new[] { orgUser }); sutProvider.GetDependency<IOrganizationUserRepository>().GetManyAsync(default).ReturnsForAnyArgs(new[] { orgUser });
sutProvider.GetDependency<IUserRepository>().GetManyAsync(default).ReturnsForAnyArgs(new[] { user }); sutProvider.GetDependency<IUserRepository>().GetManyAsync(default).ReturnsForAnyArgs(new[] { user });
var policyDetails = new PolicyDetails
{
OrganizationId = org.Id,
OrganizationUserId = orgUser.Id,
IsProvider = false,
OrganizationUserStatus = orgUser.Status,
OrganizationUserType = orgUser.Type,
PolicyType = PolicyType.OrganizationDataOwnership
};
sutProvider.GetDependency<IPolicyRequirementQuery>() sutProvider.GetDependency<IPolicyRequirementQuery>()
.GetAsync<OrganizationDataOwnershipPolicyRequirement>(orgUser.UserId!.Value) .GetAsync<OrganizationDataOwnershipPolicyRequirement>(orgUser.UserId!.Value)
.Returns(new OrganizationDataOwnershipPolicyRequirement(OrganizationDataOwnershipState.Disabled, [policyDetails])); .Returns(new OrganizationDataOwnershipPolicyRequirement(OrganizationDataOwnershipState.Disabled, []));
await sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id, collectionName); await sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id, collectionName);
await sutProvider.GetDependency<ICollectionRepository>() await sutProvider.GetDependency<ICollectionRepository>()
.DidNotReceive() .DidNotReceive()
.UpsertDefaultCollectionsAsync(Arg.Any<Guid>(), Arg.Any<IEnumerable<Guid>>(), Arg.Any<string>()); .CreateDefaultCollectionsAsync(Arg.Any<Guid>(), Arg.Any<IEnumerable<Guid>>(), Arg.Any<string>());
} }
[Theory, BitAutoData] [Theory, BitAutoData]

View File

@@ -38,7 +38,7 @@ public class OrganizationDataOwnershipPolicyValidatorTests
// Assert // Assert
await sutProvider.GetDependency<ICollectionRepository>() await sutProvider.GetDependency<ICollectionRepository>()
.DidNotReceive() .DidNotReceive()
.UpsertDefaultCollectionsAsync(Arg.Any<Guid>(), Arg.Any<IEnumerable<Guid>>(), Arg.Any<string>()); .CreateDefaultCollectionsBulkAsync(Arg.Any<Guid>(), Arg.Any<IEnumerable<Guid>>(), Arg.Any<string>());
} }
[Theory, BitAutoData] [Theory, BitAutoData]
@@ -60,7 +60,7 @@ public class OrganizationDataOwnershipPolicyValidatorTests
// Assert // Assert
await sutProvider.GetDependency<ICollectionRepository>() await sutProvider.GetDependency<ICollectionRepository>()
.DidNotReceive() .DidNotReceive()
.UpsertDefaultCollectionsAsync(Arg.Any<Guid>(), Arg.Any<IEnumerable<Guid>>(), Arg.Any<string>()); .CreateDefaultCollectionsBulkAsync(Arg.Any<Guid>(), Arg.Any<IEnumerable<Guid>>(), Arg.Any<string>());
} }
[Theory, BitAutoData] [Theory, BitAutoData]
@@ -86,7 +86,7 @@ public class OrganizationDataOwnershipPolicyValidatorTests
// Assert // Assert
await collectionRepository await collectionRepository
.DidNotReceive() .DidNotReceive()
.UpsertDefaultCollectionsAsync( .CreateDefaultCollectionsBulkAsync(
Arg.Any<Guid>(), Arg.Any<Guid>(),
Arg.Any<IEnumerable<Guid>>(), Arg.Any<IEnumerable<Guid>>(),
Arg.Any<string>()); Arg.Any<string>());
@@ -172,10 +172,10 @@ public class OrganizationDataOwnershipPolicyValidatorTests
// Act // Act
await sut.ExecuteSideEffectsAsync(policyRequest, postUpdatedPolicy, previousPolicyState); await sut.ExecuteSideEffectsAsync(policyRequest, postUpdatedPolicy, previousPolicyState);
// Assert // Assert - Should call with all user IDs (repository does internal filtering)
await collectionRepository await collectionRepository
.Received(1) .Received(1)
.UpsertDefaultCollectionsAsync( .CreateDefaultCollectionsBulkAsync(
policyUpdate.OrganizationId, policyUpdate.OrganizationId,
Arg.Is<IEnumerable<Guid>>(ids => ids.Count() == 3), Arg.Is<IEnumerable<Guid>>(ids => ids.Count() == 3),
_defaultUserCollectionName); _defaultUserCollectionName);
@@ -210,7 +210,7 @@ public class OrganizationDataOwnershipPolicyValidatorTests
// Assert // Assert
await sutProvider.GetDependency<ICollectionRepository>() await sutProvider.GetDependency<ICollectionRepository>()
.DidNotReceive() .DidNotReceive()
.UpsertDefaultCollectionsAsync(Arg.Any<Guid>(), Arg.Any<IEnumerable<Guid>>(), Arg.Any<string>()); .CreateDefaultCollectionsBulkAsync(Arg.Any<Guid>(), Arg.Any<IEnumerable<Guid>>(), Arg.Any<string>());
} }
private static IPolicyRepository ArrangePolicyRepository(IEnumerable<OrganizationPolicyDetails> policyDetails) private static IPolicyRepository ArrangePolicyRepository(IEnumerable<OrganizationPolicyDetails> policyDetails)
@@ -251,7 +251,7 @@ public class OrganizationDataOwnershipPolicyValidatorTests
// Assert // Assert
await sutProvider.GetDependency<ICollectionRepository>() await sutProvider.GetDependency<ICollectionRepository>()
.DidNotReceiveWithAnyArgs() .DidNotReceiveWithAnyArgs()
.UpsertDefaultCollectionsAsync(default, default, default); .CreateDefaultCollectionsBulkAsync(default, default, default);
} }
[Theory, BitAutoData] [Theory, BitAutoData]
@@ -273,7 +273,7 @@ public class OrganizationDataOwnershipPolicyValidatorTests
// Assert // Assert
await sutProvider.GetDependency<ICollectionRepository>() await sutProvider.GetDependency<ICollectionRepository>()
.DidNotReceiveWithAnyArgs() .DidNotReceiveWithAnyArgs()
.UpsertDefaultCollectionsAsync(default, default, default); .CreateDefaultCollectionsBulkAsync(default, default, default);
} }
[Theory, BitAutoData] [Theory, BitAutoData]
@@ -299,7 +299,7 @@ public class OrganizationDataOwnershipPolicyValidatorTests
// Assert // Assert
await collectionRepository await collectionRepository
.DidNotReceiveWithAnyArgs() .DidNotReceiveWithAnyArgs()
.UpsertDefaultCollectionsAsync( .CreateDefaultCollectionsBulkAsync(
default, default,
default, default,
default); default);
@@ -336,10 +336,10 @@ public class OrganizationDataOwnershipPolicyValidatorTests
// Act // Act
await sut.ExecutePostUpsertSideEffectAsync(policyRequest, postUpdatedPolicy, previousPolicyState); await sut.ExecutePostUpsertSideEffectAsync(policyRequest, postUpdatedPolicy, previousPolicyState);
// Assert // Assert - Should call with all user IDs (repository does internal filtering)
await collectionRepository await collectionRepository
.Received(1) .Received(1)
.UpsertDefaultCollectionsAsync( .CreateDefaultCollectionsBulkAsync(
policyUpdate.OrganizationId, policyUpdate.OrganizationId,
Arg.Is<IEnumerable<Guid>>(ids => ids.Count() == 3), Arg.Is<IEnumerable<Guid>>(ids => ids.Count() == 3),
_defaultUserCollectionName); _defaultUserCollectionName);
@@ -367,6 +367,6 @@ public class OrganizationDataOwnershipPolicyValidatorTests
// Assert // Assert
await sutProvider.GetDependency<ICollectionRepository>() await sutProvider.GetDependency<ICollectionRepository>()
.DidNotReceiveWithAnyArgs() .DidNotReceiveWithAnyArgs()
.UpsertDefaultCollectionsAsync(default, default, default); .CreateDefaultCollectionsBulkAsync(default, default, default);
} }
} }

View File

@@ -0,0 +1,53 @@
using Bit.Core.Repositories;
using Xunit;
namespace Bit.Infrastructure.IntegrationTest.AdminConsole.Repositories.CollectionRepository;
public class CreateDefaultCollectionsBulkAsyncTests
{
[Theory, DatabaseData]
public async Task CreateDefaultCollectionsBulkAsync_CreatesDefaultCollections_Success(
IOrganizationRepository organizationRepository,
IUserRepository userRepository,
IOrganizationUserRepository organizationUserRepository,
ICollectionRepository collectionRepository)
{
await CreateDefaultCollectionsSharedTests.CreatesDefaultCollections_Success(
collectionRepository.CreateDefaultCollectionsBulkAsync,
organizationRepository,
userRepository,
organizationUserRepository,
collectionRepository);
}
[Theory, DatabaseData]
public async Task CreateDefaultCollectionsBulkAsync_CreatesForNewUsersOnly_AndIgnoresExistingUsers(
IOrganizationRepository organizationRepository,
IUserRepository userRepository,
IOrganizationUserRepository organizationUserRepository,
ICollectionRepository collectionRepository)
{
await CreateDefaultCollectionsSharedTests.CreatesForNewUsersOnly_AndIgnoresExistingUsers(
collectionRepository.CreateDefaultCollectionsBulkAsync,
organizationRepository,
userRepository,
organizationUserRepository,
collectionRepository);
}
[Theory, DatabaseData]
public async Task CreateDefaultCollectionsBulkAsync_IgnoresAllExistingUsers(
IOrganizationRepository organizationRepository,
IUserRepository userRepository,
IOrganizationUserRepository organizationUserRepository,
ICollectionRepository collectionRepository)
{
await CreateDefaultCollectionsSharedTests.IgnoresAllExistingUsers(
collectionRepository.CreateDefaultCollectionsBulkAsync,
organizationRepository,
userRepository,
organizationUserRepository,
collectionRepository);
}
}

View File

@@ -6,10 +6,14 @@ using Xunit;
namespace Bit.Infrastructure.IntegrationTest.AdminConsole.Repositories.CollectionRepository; namespace Bit.Infrastructure.IntegrationTest.AdminConsole.Repositories.CollectionRepository;
public class UpsertDefaultCollectionsTests /// <summary>
/// Shared tests for CreateDefaultCollections methods - both bulk and non-bulk implementations,
/// as they share the same behavior. Both test suites call the tests in this class.
/// </summary>
public static class CreateDefaultCollectionsSharedTests
{ {
[Theory, DatabaseData] public static async Task CreatesDefaultCollections_Success(
public async Task UpsertDefaultCollectionsAsync_ShouldCreateDefaultCollection_WhenUsersDoNotHaveDefaultCollection( Func<Guid, IEnumerable<Guid>, string, Task> createDefaultCollectionsFunc,
IOrganizationRepository organizationRepository, IOrganizationRepository organizationRepository,
IUserRepository userRepository, IUserRepository userRepository,
IOrganizationUserRepository organizationUserRepository, IOrganizationUserRepository organizationUserRepository,
@@ -21,14 +25,13 @@ public class UpsertDefaultCollectionsTests
var resultOrganizationUsers = await Task.WhenAll( var resultOrganizationUsers = await Task.WhenAll(
CreateUserForOrgAsync(userRepository, organizationUserRepository, organization), CreateUserForOrgAsync(userRepository, organizationUserRepository, organization),
CreateUserForOrgAsync(userRepository, organizationUserRepository, organization) CreateUserForOrgAsync(userRepository, organizationUserRepository, organization)
); );
var affectedOrgUserIds = resultOrganizationUsers.Select(organizationUser => organizationUser.Id).ToList();
var affectedOrgUserIds = resultOrganizationUsers.Select(organizationUser => organizationUser.Id);
var defaultCollectionName = $"default-name-{organization.Id}"; var defaultCollectionName = $"default-name-{organization.Id}";
// Act // Act
await collectionRepository.UpsertDefaultCollectionsAsync(organization.Id, affectedOrgUserIds, defaultCollectionName); await createDefaultCollectionsFunc(organization.Id, affectedOrgUserIds, defaultCollectionName);
// Assert // Assert
await AssertAllUsersHaveOneDefaultCollectionAsync(collectionRepository, resultOrganizationUsers, organization.Id); await AssertAllUsersHaveOneDefaultCollectionAsync(collectionRepository, resultOrganizationUsers, organization.Id);
@@ -36,8 +39,8 @@ public class UpsertDefaultCollectionsTests
await CleanupAsync(organizationRepository, userRepository, organization, resultOrganizationUsers); await CleanupAsync(organizationRepository, userRepository, organization, resultOrganizationUsers);
} }
[Theory, DatabaseData] public static async Task CreatesForNewUsersOnly_AndIgnoresExistingUsers(
public async Task UpsertDefaultCollectionsAsync_ShouldUpsertCreateDefaultCollection_ForUsersWithAndWithoutDefaultCollectionsExist( Func<Guid, IEnumerable<Guid>, string, Task> createDefaultCollectionsFunc,
IOrganizationRepository organizationRepository, IOrganizationRepository organizationRepository,
IUserRepository userRepository, IUserRepository userRepository,
IOrganizationUserRepository organizationUserRepository, IOrganizationUserRepository organizationUserRepository,
@@ -51,31 +54,30 @@ public class UpsertDefaultCollectionsTests
CreateUserForOrgAsync(userRepository, organizationUserRepository, organization) CreateUserForOrgAsync(userRepository, organizationUserRepository, organization)
); );
var arrangedOrgUserIds = arrangedOrganizationUsers.Select(organizationUser => organizationUser.Id); var arrangedOrgUserIds = arrangedOrganizationUsers.Select(organizationUser => organizationUser.Id).ToList();
var defaultCollectionName = $"default-name-{organization.Id}"; var defaultCollectionName = $"default-name-{organization.Id}";
await CreateUsersWithExistingDefaultCollectionsAsync(createDefaultCollectionsFunc, collectionRepository, organization.Id, arrangedOrgUserIds, defaultCollectionName, arrangedOrganizationUsers);
await CreateUsersWithExistingDefaultCollectionsAsync(collectionRepository, organization.Id, arrangedOrgUserIds, defaultCollectionName, arrangedOrganizationUsers); var newOrganizationUsers = new List<OrganizationUser>
var newOrganizationUsers = new List<OrganizationUser>()
{ {
await CreateUserForOrgAsync(userRepository, organizationUserRepository, organization) await CreateUserForOrgAsync(userRepository, organizationUserRepository, organization)
}; };
var affectedOrgUsers = newOrganizationUsers.Concat(arrangedOrganizationUsers); var affectedOrgUsers = newOrganizationUsers.Concat(arrangedOrganizationUsers).ToList();
var affectedOrgUserIds = affectedOrgUsers.Select(organizationUser => organizationUser.Id); var affectedOrgUserIds = affectedOrgUsers.Select(organizationUser => organizationUser.Id).ToList();
// Act // Act
await collectionRepository.UpsertDefaultCollectionsAsync(organization.Id, affectedOrgUserIds, defaultCollectionName); await createDefaultCollectionsFunc(organization.Id, affectedOrgUserIds, defaultCollectionName);
// Assert // Assert
await AssertAllUsersHaveOneDefaultCollectionAsync(collectionRepository, arrangedOrganizationUsers, organization.Id); await AssertAllUsersHaveOneDefaultCollectionAsync(collectionRepository, affectedOrgUsers, organization.Id);
await CleanupAsync(organizationRepository, userRepository, organization, affectedOrgUsers); await CleanupAsync(organizationRepository, userRepository, organization, affectedOrgUsers);
} }
[Theory, DatabaseData] public static async Task IgnoresAllExistingUsers(
public async Task UpsertDefaultCollectionsAsync_ShouldNotCreateDefaultCollection_WhenUsersAlreadyHaveOne( Func<Guid, IEnumerable<Guid>, string, Task> createDefaultCollectionsFunc,
IOrganizationRepository organizationRepository, IOrganizationRepository organizationRepository,
IUserRepository userRepository, IUserRepository userRepository,
IOrganizationUserRepository organizationUserRepository, IOrganizationUserRepository organizationUserRepository,
@@ -89,26 +91,29 @@ public class UpsertDefaultCollectionsTests
CreateUserForOrgAsync(userRepository, organizationUserRepository, organization) CreateUserForOrgAsync(userRepository, organizationUserRepository, organization)
); );
var affectedOrgUserIds = resultOrganizationUsers.Select(organizationUser => organizationUser.Id); var affectedOrgUserIds = resultOrganizationUsers.Select(organizationUser => organizationUser.Id).ToList();
var defaultCollectionName = $"default-name-{organization.Id}"; var defaultCollectionName = $"default-name-{organization.Id}";
await CreateUsersWithExistingDefaultCollectionsAsync(createDefaultCollectionsFunc, collectionRepository, organization.Id, affectedOrgUserIds, defaultCollectionName, resultOrganizationUsers);
await CreateUsersWithExistingDefaultCollectionsAsync(collectionRepository, organization.Id, affectedOrgUserIds, defaultCollectionName, resultOrganizationUsers); // Act - Try to create again, should silently filter and not create duplicates
await createDefaultCollectionsFunc(organization.Id, affectedOrgUserIds, defaultCollectionName);
// Act // Assert - Original collections should remain unchanged, still only one per user
await collectionRepository.UpsertDefaultCollectionsAsync(organization.Id, affectedOrgUserIds, defaultCollectionName);
// Assert
await AssertAllUsersHaveOneDefaultCollectionAsync(collectionRepository, resultOrganizationUsers, organization.Id); await AssertAllUsersHaveOneDefaultCollectionAsync(collectionRepository, resultOrganizationUsers, organization.Id);
await CleanupAsync(organizationRepository, userRepository, organization, resultOrganizationUsers); await CleanupAsync(organizationRepository, userRepository, organization, resultOrganizationUsers);
} }
private static async Task CreateUsersWithExistingDefaultCollectionsAsync(ICollectionRepository collectionRepository, private static async Task CreateUsersWithExistingDefaultCollectionsAsync(
Guid organizationId, IEnumerable<Guid> affectedOrgUserIds, string defaultCollectionName, Func<Guid, IEnumerable<Guid>, string, Task> createDefaultCollectionsFunc,
ICollectionRepository collectionRepository,
Guid organizationId,
IEnumerable<Guid> affectedOrgUserIds,
string defaultCollectionName,
OrganizationUser[] resultOrganizationUsers) OrganizationUser[] resultOrganizationUsers)
{ {
await collectionRepository.UpsertDefaultCollectionsAsync(organizationId, affectedOrgUserIds, defaultCollectionName); await createDefaultCollectionsFunc(organizationId, affectedOrgUserIds, defaultCollectionName);
await AssertAllUsersHaveOneDefaultCollectionAsync(collectionRepository, resultOrganizationUsers, organizationId); await AssertAllUsersHaveOneDefaultCollectionAsync(collectionRepository, resultOrganizationUsers, organizationId);
} }
@@ -131,7 +136,6 @@ public class UpsertDefaultCollectionsTests
private static async Task<OrganizationUser> CreateUserForOrgAsync(IUserRepository userRepository, private static async Task<OrganizationUser> CreateUserForOrgAsync(IUserRepository userRepository,
IOrganizationUserRepository organizationUserRepository, Organization organization) IOrganizationUserRepository organizationUserRepository, Organization organization)
{ {
var user = await userRepository.CreateTestUserAsync(); var user = await userRepository.CreateTestUserAsync();
var orgUser = await organizationUserRepository.CreateTestOrganizationUserAsync(organization, user); var orgUser = await organizationUserRepository.CreateTestOrganizationUserAsync(organization, user);

View File

@@ -0,0 +1,52 @@
using Bit.Core.Repositories;
using Xunit;
namespace Bit.Infrastructure.IntegrationTest.AdminConsole.Repositories.CollectionRepository;
public class CreateDefaultCollectionsAsyncTests
{
[Theory, DatabaseData]
public async Task CreateDefaultCollectionsAsync_CreatesDefaultCollections_Success(
IOrganizationRepository organizationRepository,
IUserRepository userRepository,
IOrganizationUserRepository organizationUserRepository,
ICollectionRepository collectionRepository)
{
await CreateDefaultCollectionsSharedTests.CreatesDefaultCollections_Success(
collectionRepository.CreateDefaultCollectionsAsync,
organizationRepository,
userRepository,
organizationUserRepository,
collectionRepository);
}
[Theory, DatabaseData]
public async Task CreateDefaultCollectionsAsync_CreatesForNewUsersOnly_AndIgnoresExistingUsers(
IOrganizationRepository organizationRepository,
IUserRepository userRepository,
IOrganizationUserRepository organizationUserRepository,
ICollectionRepository collectionRepository)
{
await CreateDefaultCollectionsSharedTests.CreatesForNewUsersOnly_AndIgnoresExistingUsers(
collectionRepository.CreateDefaultCollectionsAsync,
organizationRepository,
userRepository,
organizationUserRepository,
collectionRepository);
}
[Theory, DatabaseData]
public async Task CreateDefaultCollectionsAsync_IgnoresAllExistingUsers(
IOrganizationRepository organizationRepository,
IUserRepository userRepository,
IOrganizationUserRepository organizationUserRepository,
ICollectionRepository collectionRepository)
{
await CreateDefaultCollectionsSharedTests.IgnoresAllExistingUsers(
collectionRepository.CreateDefaultCollectionsAsync,
organizationRepository,
userRepository,
organizationUserRepository,
collectionRepository);
}
}

View File

@@ -0,0 +1,70 @@
-- Creates default user collections for organization users
-- Filters out existing default collections at database level
CREATE OR ALTER PROCEDURE [dbo].[Collection_CreateDefaultCollections]
@OrganizationId UNIQUEIDENTIFIER,
@DefaultCollectionName VARCHAR(MAX),
@OrganizationUserCollectionIds AS [dbo].[TwoGuidIdArray] READONLY -- OrganizationUserId, CollectionId
AS
BEGIN
SET NOCOUNT ON
DECLARE @Now DATETIME2(7) = GETUTCDATE()
-- Filter to only users who don't have default collections
SELECT ids.Id1, ids.Id2
INTO #FilteredIds
FROM @OrganizationUserCollectionIds ids
WHERE NOT EXISTS (
SELECT 1
FROM [dbo].[CollectionUser] cu
INNER JOIN [dbo].[Collection] c ON c.Id = cu.CollectionId
WHERE c.OrganizationId = @OrganizationId
AND c.[Type] = 1 -- CollectionType.DefaultUserCollection
AND cu.OrganizationUserId = ids.Id1
);
-- Insert collections only for users who don't have default collections yet
INSERT INTO [dbo].[Collection]
(
[Id],
[OrganizationId],
[Name],
[CreationDate],
[RevisionDate],
[Type],
[ExternalId],
[DefaultUserCollectionEmail]
)
SELECT
ids.Id2, -- CollectionId
@OrganizationId,
@DefaultCollectionName,
@Now,
@Now,
1, -- CollectionType.DefaultUserCollection
NULL,
NULL
FROM
#FilteredIds ids;
-- Insert collection user mappings
INSERT INTO [dbo].[CollectionUser]
(
[CollectionId],
[OrganizationUserId],
[ReadOnly],
[HidePasswords],
[Manage]
)
SELECT
ids.Id2, -- CollectionId
ids.Id1, -- OrganizationUserId
0, -- ReadOnly = false
0, -- HidePasswords = false
1 -- Manage = true
FROM
#FilteredIds ids;
DROP TABLE #FilteredIds;
END
GO