Skip to content
Closed
Show file tree
Hide file tree
Changes from 33 commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
f069faf
First pass at semaphore
eliykat Dec 30, 2025
a91f096
Make sqlbulkcopy check constraints
eliykat Dec 30, 2025
31eebe2
Update existing reads to use semaphore
eliykat Dec 31, 2025
e67727b
Update naming in existing bulk tests
eliykat Dec 31, 2025
ae9d18a
Add read sproc for semaphores
eliykat Dec 31, 2025
a53bb23
Add tests for cascade delete behavior
eliykat Dec 31, 2025
9cc89eb
Update existing bulk tests to assert semaphores
eliykat Dec 31, 2025
843fd7a
Fix validator using wrong method
eliykat Dec 31, 2025
90f2e2b
Rename upsert -> create to reflect behavior
eliykat Dec 31, 2025
7aef3da
Refine tests (failing)
eliykat Dec 31, 2025
53bc100
Assert semaphores in Create tests
eliykat Dec 31, 2025
36aad90
Explicit transaction for mssql
eliykat Dec 31, 2025
f668a0c
Fix sproc name
eliykat Dec 31, 2025
7ea237f
Remove redundant OrganizationId column; remove private read method usโ€ฆ
eliykat Dec 31, 2025
a6fcee7
Generate EF migrations
eliykat Dec 31, 2025
21d7276
Move filtering out of db layer
eliykat Jan 1, 2026
6bcc17f
Update existing code paths to use new method
eliykat Jan 1, 2026
8212af5
dotnet format
eliykat Jan 1, 2026
a9e8409
DRY arrangement code between repositories
eliykat Jan 1, 2026
ebbdc94
Return uniform error
eliykat Jan 1, 2026
897719e
Move semaphore into helper method for consistent creation date
eliykat Jan 1, 2026
38bab44
dotnet format
eliykat Jan 1, 2026
2bf8487
local code review feedback
eliykat Jan 1, 2026
8ab5ad1
Merge remote-tracking branch 'origin/main' into ac/pm-28555/add-semapโ€ฆ
eliykat Jan 1, 2026
415182a
Fix test
eliykat Jan 1, 2026
8427367
Use guid array instead of json
eliykat Jan 1, 2026
5180092
Trigger code review
eliykat Jan 2, 2026
0face3a
Add explicit transaction in EF
eliykat Jan 2, 2026
1a42f6f
Add general catch block
eliykat Jan 2, 2026
c84dd78
Rethrow exception
eliykat Jan 2, 2026
aa1c0a4
Rui PR feedback
eliykat Jan 3, 2026
2b7caf1
Use TwoGuidIdArray
eliykat Jan 3, 2026
2fc0257
Move transaction up to code layer
eliykat Jan 3, 2026
fc351ce
Rethrow exception
eliykat Jan 3, 2026
8f345e0
First pass at reverting semaphore approach
eliykat Jan 6, 2026
91c51fd
Merge remote-tracking branch 'origin/main' into ac/pm-28555/add-semapโ€ฆ
eliykat Jan 6, 2026
dc5c06f
Revert Bulk implementation
eliykat Jan 6, 2026
1f4fc9b
dotnet format
eliykat Jan 6, 2026
4c32e20
Revert unnecessary ef changes
eliykat Jan 6, 2026
32dd846
Bump migration date
eliykat Jan 6, 2026
3d801be
Revert unnecessary new tests
eliykat Jan 6, 2026
9d86aff
DRY up tests
eliykat Jan 6, 2026
88036ea
local review feedback
eliykat Jan 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
๏ปฟnamespace Bit.Core.AdminConsole.Collections;

public class DuplicateDefaultCollectionException()
: Exception("A My Items collection already exists for one or more of the specified organization members.");

Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
๏ปฟusing Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Utilities;

namespace Bit.Core.AdminConsole.OrganizationFeatures.Collections;

public static class CollectionUtils
{
/// <summary>
/// Arranges semaphore, 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 semaphores, collections, and collection users.</returns>
public static (IEnumerable<DefaultCollectionSemaphore> semaphores,
IEnumerable<Collection> collections,
IEnumerable<CollectionUser> collectionUsers)
BuildDefaultUserCollections(Guid organizationId, IEnumerable<Guid> organizationUserIds,
string defaultCollectionName)
{
var now = DateTime.UtcNow;

var semaphores = new List<DefaultCollectionSemaphore>();
var collectionUsers = new List<CollectionUser>();
var collections = new List<Collection>();

foreach (var orgUserId in organizationUserIds)
{
var collectionId = CoreHelpers.GenerateComb();

semaphores.Add(new DefaultCollectionSemaphore
{
OrganizationUserId = orgUserId,
CreationDate = now
});

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 (semaphores, collections, collectionUsers);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,7 @@
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Models.Data;
using Bit.Core.Platform.Push;
using Bit.Core.Repositories;
using Bit.Core.Services;
Expand Down Expand Up @@ -79,19 +77,10 @@ private async Task CreateDefaultCollectionsAsync(AutomaticallyConfirmOrganizatio
return;
}

await collectionRepository.CreateAsync(
new Collection
{
OrganizationId = request.Organization!.Id,
Name = request.DefaultUserCollectionName,
Type = CollectionType.DefaultUserCollection
},
groups: null,
[new CollectionAccessSelection
{
Id = request.OrganizationUser!.Id,
Manage = true
}]);
await collectionRepository.CreateDefaultCollectionsAsync(
request.Organization!.Id,
[request.OrganizationUser!.Id],
request.DefaultUserCollectionName);
}
catch (Exception ex)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Models.Data;
using Bit.Core.Platform.Push;
using Bit.Core.Repositories;
using Bit.Core.Services;
Expand Down Expand Up @@ -297,21 +296,10 @@ private async Task CreateDefaultCollectionAsync(OrganizationUser organizationUse
return;
}

var defaultCollection = new Collection
{
OrganizationId = organizationUser.OrganizationId,
Name = 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]);
await _collectionRepository.CreateDefaultCollectionsAsync(
organizationUser.OrganizationId,
[organizationUser.Id],
defaultUserCollectionName);
}

/// <summary>
Expand Down Expand Up @@ -347,6 +335,6 @@ private async Task CreateManyDefaultCollectionsAsync(Guid organizationId,
return;
}

await _collectionRepository.UpsertDefaultCollectionsAsync(organizationId, eligibleOrganizationUserIds, defaultUserCollectionName);
await _collectionRepository.CreateDefaultCollectionsAsync(organizationId, eligibleOrganizationUserIds, defaultUserCollectionName);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -64,16 +64,26 @@ private async Task UpsertDefaultCollectionsForUsersAsync(PolicyUpdate policyUpda
var userOrgIds = requirements
.Select(requirement => requirement.GetDefaultCollectionRequestOnPolicyEnable(policyUpdate.OrganizationId))
.Where(request => request.ShouldCreateDefaultCollection)
.Select(request => request.OrganizationUserId);
.Select(request => request.OrganizationUserId)
.ToList();

if (!userOrgIds.Any())
{
return;
}

await collectionRepository.UpsertDefaultCollectionsAsync(
// Filter out users who already have default collections
var existingSemaphores = await collectionRepository.GetDefaultCollectionSemaphoresAsync(userOrgIds);
var usersNeedingDefaultCollections = userOrgIds.Except(existingSemaphores).ToList();

if (!usersNeedingDefaultCollections.Any())
{
return;
}

await collectionRepository.CreateDefaultCollectionsBulkAsync(
policyUpdate.OrganizationId,
userOrgIds,
usersNeedingDefaultCollections,
defaultCollectionName);
}
}
7 changes: 7 additions & 0 deletions src/Core/Entities/DefaultCollectionSemaphore.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
๏ปฟnamespace Bit.Core.Entities;

public class DefaultCollectionSemaphore
{
public Guid OrganizationUserId { get; set; }
public DateTime CreationDate { get; set; } = DateTime.UtcNow;
}
33 changes: 30 additions & 3 deletions src/Core/Repositories/ICollectionRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,11 +64,38 @@ Task CreateOrUpdateAccessForManyAsync(Guid organizationId, IEnumerable<Guid> col
IEnumerable<CollectionAccessSelection> users, IEnumerable<CollectionAccessSelection> groups);

/// <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.
/// Throws an exception if any user already has a default collection for the organization.
/// </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>
/// <returns></returns>
Task UpsertDefaultCollectionsAsync(Guid organizationId, IEnumerable<Guid> organizationUserIds, string defaultCollectionName);
Task CreateDefaultCollectionsAsync(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.
/// Throws an exception if any user already has a default collection for the organization.
/// </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>
/// <remarks>
/// If any of the OrganizationUsers may already have default collections, the caller should first filter out these
/// users using GetDefaultCollectionSemaphoresAsync before calling this method.
/// </remarks>
Task CreateDefaultCollectionsBulkAsync(Guid organizationId, IEnumerable<Guid> organizationUserIds, string defaultCollectionName);

/// <summary>
/// Gets default collection semaphores for the given organizationUserIds.
/// If an organizationUserId is missing from the result set, they do not have a semaphore set.
/// </summary>
/// <param name="organizationUserIds">The organization User IDs to check semaphores for.</param>
/// <returns>Collection of organization user IDs that have default collection semaphores.</returns>
/// <remarks>
/// The semaphore table is used to ensure that an organizationUser can only have 1 default collection.
/// (That is, a user may only have 1 default collection per organization.)
/// If a semaphore is returned, that user already has a default collection for that organization.
/// </remarks>
Task<HashSet<Guid>> GetDefaultCollectionSemaphoresAsync(IEnumerable<Guid> organizationUserIds);
}
15 changes: 15 additions & 0 deletions src/Infrastructure.Dapper/DapperHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,21 @@ public static DataTable ToGuidIdArrayTVP(this IEnumerable<Guid> ids)
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)
{
var table = new DataTable();
Expand Down
Loading
Loading