mirror of
https://github.com/bitwarden/server.git
synced 2026-01-31 14:13:18 +08:00
[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:
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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() { }
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
Reference in New Issue
Block a user