Refactor organization initialization methods in IOrganizationRepository and implementations

- Introduced BuildUpdateOrganizationAction method to create an action for updating organization properties during initialization.
- Replaced the InitializePendingOrganizationAsync method with ExecuteOrganizationInitializationUpdatesAsync to handle multiple update actions in a single transaction.
- Updated Dapper and Entity Framework implementations to support the new action-based approach for organization initialization, enhancing transaction management and code clarity.
This commit is contained in:
Rui Tome
2026-01-29 17:05:51 +00:00
parent 0e5213cbbb
commit b285ce4349
3 changed files with 53 additions and 210 deletions

View File

@@ -2,6 +2,7 @@
using Bit.Core.AdminConsole.Enums.Provider;
using Bit.Core.Models.Data.Organizations;
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
using Bit.Core.OrganizationFeatures.OrganizationUsers.Interfaces;
#nullable enable
@@ -66,23 +67,16 @@ public interface IOrganizationRepository : IRepository<Organization, Guid>
Task IncrementSeatCountAsync(Guid organizationId, int increaseAmount, DateTime requestDate);
/// <summary>
/// Initializes a pending organization by enabling it, setting keys, confirming the first owner,
/// and optionally creating a default collection. All operations are performed atomically.
/// Builds an action that updates an organization for initialization (sets keys, status, and enabled state).
/// </summary>
/// <param name="organizationId">The ID of the organization to initialize</param>
/// <param name="publicKey">Organization public key</param>
/// <param name="privateKey">Organization private key</param>
/// <param name="organizationUserId">The ID of the organization user to confirm</param>
/// <param name="userId">The ID of the user to verify</param>
/// <param name="userKey">The user's encrypted key</param>
/// <param name="collectionName">Optional name for the default collection</param>
/// <param name="organization">The organization entity with updated properties</param>
/// <returns>An action that can be executed within a transaction</returns>
OrganizationInitializationUpdateAction BuildUpdateOrganizationAction(Organization organization);
/// <summary>
/// Executes organization initialization updates within a single transaction.
/// </summary>
/// <param name="updateActions">Collection of initialization update delegates to execute</param>
/// <returns>A task representing the asynchronous operation</returns>
Task InitializePendingOrganizationAsync(
Guid organizationId,
string publicKey,
string privateKey,
Guid organizationUserId,
Guid userId,
string userKey,
string collectionName);
Task ExecuteOrganizationInitializationUpdatesAsync(IEnumerable<OrganizationInitializationUpdateAction> updateActions);
}

View File

@@ -3,17 +3,14 @@ using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Enums.Provider;
using Bit.Core.Auth.Entities;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Models.Data;
using Bit.Core.Models.Data.Organizations;
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
using Bit.Core.OrganizationFeatures.OrganizationUsers.Interfaces;
using Bit.Core.Repositories;
using Bit.Core.Settings;
using Bit.Core.Utilities;
using Dapper;
using Microsoft.Data.SqlClient;
using Microsoft.Extensions.Logging;
using CollectionWithGroupsAndUsers = Bit.Infrastructure.Dapper.Repositories.CollectionRepository.CollectionWithGroupsAndUsers;
#nullable enable
@@ -256,134 +253,36 @@ public class OrganizationRepository : Repository<Organization, Guid>, IOrganizat
commandType: CommandType.StoredProcedure);
}
public async Task InitializePendingOrganizationAsync(
Guid organizationId,
string publicKey,
string privateKey,
Guid organizationUserId,
Guid userId,
string userKey,
string collectionName)
public OrganizationInitializationUpdateAction BuildUpdateOrganizationAction(Organization organization)
{
await using var connection = new SqlConnection(ConnectionString);
await connection.OpenAsync();
await using var transaction = await connection.BeginTransactionAsync();
try
return async (SqlConnection? connection, SqlTransaction? transaction, object? context) =>
{
// 1. Update Organization
var organization = (await connection.QueryAsync<Organization>(
"[dbo].[Organization_ReadById]",
new { Id = organizationId },
commandType: CommandType.StoredProcedure,
transaction: transaction)).SingleOrDefault();
if (organization == null)
{
throw new InvalidOperationException($"Organization with ID {organizationId} not found.");
}
organization.Enabled = true;
organization.Status = OrganizationStatusType.Created;
organization.PublicKey = publicKey;
organization.PrivateKey = privateKey;
organization.RevisionDate = DateTime.UtcNow;
await connection.ExecuteAsync(
await connection!.ExecuteAsync(
"[dbo].[Organization_Update]",
organization,
commandType: CommandType.StoredProcedure,
transaction: transaction);
};
}
// 2. Update OrganizationUser
var organizationUser = (await connection.QueryAsync<OrganizationUser>(
"[dbo].[OrganizationUser_ReadById]",
new { Id = organizationUserId },
commandType: CommandType.StoredProcedure,
transaction: transaction)).SingleOrDefault();
public async Task ExecuteOrganizationInitializationUpdatesAsync(IEnumerable<OrganizationInitializationUpdateAction> updateActions)
{
await using var connection = new SqlConnection(ConnectionString);
await connection.OpenAsync();
await using var transaction = (SqlTransaction)await connection.BeginTransactionAsync();
if (organizationUser == null)
try
{
foreach (var action in updateActions)
{
throw new InvalidOperationException($"OrganizationUser with ID {organizationUserId} not found.");
await action(connection, transaction);
}
organizationUser.Status = OrganizationUserStatusType.Confirmed;
organizationUser.UserId = userId;
organizationUser.Key = userKey;
organizationUser.Email = null;
await connection.ExecuteAsync(
"[dbo].[OrganizationUser_Update]",
organizationUser,
commandType: CommandType.StoredProcedure,
transaction: transaction);
// 3. Update User (verify email if needed)
var user = (await connection.QueryAsync<User>(
"[dbo].[User_ReadById]",
new { Id = userId },
commandType: CommandType.StoredProcedure,
transaction: transaction)).SingleOrDefault();
if (user == null)
{
throw new InvalidOperationException($"User with ID {userId} not found.");
}
if (user.EmailVerified == false)
{
user.EmailVerified = true;
user.RevisionDate = DateTime.UtcNow;
await connection.ExecuteAsync(
"[dbo].[User_Update]",
user,
commandType: CommandType.StoredProcedure,
transaction: transaction);
}
// 4. Create default collection if name provided
if (!string.IsNullOrWhiteSpace(collectionName))
{
var collection = new Collection
{
Id = CoreHelpers.GenerateComb(),
Name = collectionName,
OrganizationId = organizationId,
CreationDate = DateTime.UtcNow,
RevisionDate = DateTime.UtcNow
};
var collectionUsers = new[]
{
new CollectionAccessSelection
{
Id = organizationUserId,
HidePasswords = false,
ReadOnly = false,
Manage = true
}
};
var collectionWithAccess = new CollectionWithGroupsAndUsers(
collection,
Enumerable.Empty<CollectionAccessSelection>(),
collectionUsers);
await connection.ExecuteAsync(
"[dbo].[Collection_CreateWithGroupsAndUsers]",
collectionWithAccess,
commandType: CommandType.StoredProcedure,
transaction: transaction);
}
await transaction.CommitAsync();
}
catch (Exception ex)
{
_logger.LogError(ex,
"Failed to initialize pending organization {OrganizationId}. Rolling back transaction.",
organizationId);
"Failed to initialize organization. Rolling back transaction.");
await transaction.RollbackAsync();
throw;
}

View File

@@ -9,10 +9,10 @@ using Bit.Core.Billing.Enums;
using Bit.Core.Enums;
using Bit.Core.Models.Data.Organizations;
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
using Bit.Core.OrganizationFeatures.OrganizationUsers.Interfaces;
using Bit.Core.Repositories;
using Bit.Core.Utilities;
using Bit.Infrastructure.EntityFramework.Models;
using LinqToDB.Tools;
using Microsoft.Data.SqlClient;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
@@ -443,14 +443,27 @@ public class OrganizationRepository : Repository<Core.AdminConsole.Entities.Orga
.SetProperty(o => o.RevisionDate, requestDate));
}
public async Task InitializePendingOrganizationAsync(
Guid organizationId,
string publicKey,
string privateKey,
Guid organizationUserId,
Guid userId,
string userKey,
string collectionName)
public OrganizationInitializationUpdateAction BuildUpdateOrganizationAction(Core.AdminConsole.Entities.Organization organization)
{
return async (SqlConnection _, SqlTransaction _, object context) =>
{
var dbContext = (DatabaseContext)context;
var efOrganization = await dbContext.Organizations.FindAsync(organization.Id);
if (efOrganization != null)
{
efOrganization.Enabled = organization.Enabled;
efOrganization.Status = organization.Status;
efOrganization.PublicKey = organization.PublicKey;
efOrganization.PrivateKey = organization.PrivateKey;
efOrganization.RevisionDate = organization.RevisionDate;
await dbContext.SaveChangesAsync();
}
};
}
public async Task ExecuteOrganizationInitializationUpdatesAsync(IEnumerable<OrganizationInitializationUpdateAction> updateActions)
{
using var scope = ServiceScopeFactory.CreateScope();
var dbContext = GetDatabaseContext(scope);
@@ -459,79 +472,16 @@ public class OrganizationRepository : Repository<Core.AdminConsole.Entities.Orga
try
{
// 1. Update Organization
var organization = await dbContext.Organizations.FindAsync(organizationId);
if (organization == null)
foreach (var action in updateActions)
{
throw new InvalidOperationException($"Organization with ID {organizationId} not found.");
await action(null, null, dbContext);
}
organization.Enabled = true;
organization.Status = OrganizationStatusType.Created;
organization.PublicKey = publicKey;
organization.PrivateKey = privateKey;
organization.RevisionDate = DateTime.UtcNow;
// 2. Update OrganizationUser
var organizationUser = await dbContext.OrganizationUsers.FindAsync(organizationUserId);
if (organizationUser == null)
{
throw new InvalidOperationException($"OrganizationUser with ID {organizationUserId} not found.");
}
organizationUser.Status = OrganizationUserStatusType.Confirmed;
organizationUser.UserId = userId;
organizationUser.Key = userKey;
organizationUser.Email = null;
// 3. Update User (verify email if needed)
var user = await dbContext.Users.FindAsync(userId);
if (user == null)
{
throw new InvalidOperationException($"User with ID {userId} not found.");
}
if (user.EmailVerified == false)
{
user.EmailVerified = true;
user.RevisionDate = DateTime.UtcNow;
}
// 4. Create default collection if name provided
if (!string.IsNullOrWhiteSpace(collectionName))
{
var collection = new Collection
{
Id = CoreHelpers.GenerateComb(),
Name = collectionName,
OrganizationId = organizationId,
CreationDate = DateTime.UtcNow,
RevisionDate = DateTime.UtcNow
};
await dbContext.Collections.AddAsync(collection);
// Create CollectionUser association with Manage permissions
var collectionUser = new CollectionUser
{
CollectionId = collection.Id,
OrganizationUserId = organizationUserId,
HidePasswords = false,
ReadOnly = false,
Manage = true
};
await dbContext.CollectionUsers.AddAsync(collectionUser);
}
await dbContext.SaveChangesAsync();
await transaction.CommitAsync();
}
catch (Exception ex)
{
_logger.LogError(ex,
"Failed to initialize pending organization {OrganizationId}. Rolling back transaction.",
organizationId);
"Failed to initialize organization. Rolling back transaction.");
await transaction.RollbackAsync();
throw;
}