diff --git a/src/Core/AdminConsole/Repositories/IOrganizationRepository.cs b/src/Core/AdminConsole/Repositories/IOrganizationRepository.cs index 39118018df..f467256ae3 100644 --- a/src/Core/AdminConsole/Repositories/IOrganizationRepository.cs +++ b/src/Core/AdminConsole/Repositories/IOrganizationRepository.cs @@ -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 Task IncrementSeatCountAsync(Guid organizationId, int increaseAmount, DateTime requestDate); /// - /// 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). /// - /// The ID of the organization to initialize - /// Organization public key - /// Organization private key - /// The ID of the organization user to confirm - /// The ID of the user to verify - /// The user's encrypted key - /// Optional name for the default collection + /// The organization entity with updated properties + /// An action that can be executed within a transaction + OrganizationInitializationUpdateAction BuildUpdateOrganizationAction(Organization organization); + + /// + /// Executes organization initialization updates within a single transaction. + /// + /// Collection of initialization update delegates to execute /// A task representing the asynchronous operation - Task InitializePendingOrganizationAsync( - Guid organizationId, - string publicKey, - string privateKey, - Guid organizationUserId, - Guid userId, - string userKey, - string collectionName); + Task ExecuteOrganizationInitializationUpdatesAsync(IEnumerable updateActions); } diff --git a/src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationRepository.cs b/src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationRepository.cs index 79150dfeec..0a762b387e 100644 --- a/src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationRepository.cs +++ b/src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationRepository.cs @@ -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, 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( - "[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( - "[dbo].[OrganizationUser_ReadById]", - new { Id = organizationUserId }, - commandType: CommandType.StoredProcedure, - transaction: transaction)).SingleOrDefault(); + public async Task ExecuteOrganizationInitializationUpdatesAsync(IEnumerable 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( - "[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(), - 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; } diff --git a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationRepository.cs b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationRepository.cs index c8220472c2..4f00ebf76c 100644 --- a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationRepository.cs +++ b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationRepository.cs @@ -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 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 updateActions) { using var scope = ServiceScopeFactory.CreateScope(); var dbContext = GetDatabaseContext(scope); @@ -459,79 +472,16 @@ public class OrganizationRepository : Repository