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