Skip to content
Open
Original file line number Diff line number Diff line change
Expand Up @@ -171,4 +171,21 @@ Task<ICollection<Guid>> ConfirmManyOrganizationUsersAsync(
/// <param name="organizationUser">The organization user entity with updated properties (status, userId, key)</param>
/// <returns>An action that can be executed within a transaction</returns>
Func<DbConnection, DbTransaction, Task> BuildConfirmOwnerAction(OrganizationUser organizationUser);

/// <summary>
/// Returns a delegate that updates the status, key, and revision date of the given
/// organization user.
/// </summary>
/// <param name="id">Id of the organization user to update</param>
/// <param name="status">The status to set</param>
/// <param name="key">The key to set</param>
/// <param name="revisionDate">The revision date to set</param>
DatabaseTransactionAction UpdateStatusAndKeyById(Guid id,
OrganizationUserStatusType status, string? key, DateTime revisionDate);

/// <summary>
/// Returns a delegate that deletes organization users and their associated data.
/// </summary>
/// <param name="ids">Ids of the organization users to delete</param>
DatabaseTransactionAction DeleteManyByIds(IEnumerable<Guid> ids);
}
12 changes: 12 additions & 0 deletions src/Core/Auth/Repositories/IEmergencyAccessRepository.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Bit.Core.Auth.Entities;
using Bit.Core.Auth.Enums;
using Bit.Core.Auth.Models.Data;
using Bit.Core.KeyManagement.UserKey;

Expand Down Expand Up @@ -43,6 +44,17 @@ public interface IEmergencyAccessRepository : IRepository<EmergencyAccess, Guid>
UpdateEncryptedDataForKeyRotation UpdateForKeyRotation(Guid grantorId,
IEnumerable<EmergencyAccess> emergencyAccessKeys);

/// <summary>
/// Returns a delegate that updates the status, encrypted key, and revision date of the given
/// emergency access record.
/// </summary>
/// <param name="id">Id of the emergency access record to update</param>
/// <param name="status">The status to set</param>
/// <param name="keyEncrypted">The encrypted key to set</param>
/// <param name="revisionDate">The revision date to set</param>
DatabaseTransactionAction UpdateStatusAndKeyEncryptedById(Guid id,
EmergencyAccessStatusType status, string? keyEncrypted, DateTime revisionDate);

/// <summary>
/// Deletes multiple emergency access records by their IDs
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
using Bit.Core.KeyManagement.Models.Data;
using Bit.Core.KeyManagement.Repositories;
using Bit.Core.Platform.Push;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Microsoft.Extensions.Logging;

namespace Bit.Core.KeyManagement.Commands;
Expand All @@ -19,17 +21,29 @@ public class RegenerateUserAsymmetricKeysCommand : IRegenerateUserAsymmetricKeys
private readonly ILogger<RegenerateUserAsymmetricKeysCommand> _logger;
private readonly IUserAsymmetricKeysRepository _userAsymmetricKeysRepository;
private readonly IPushNotificationService _pushService;
private readonly IEmergencyAccessRepository _emergencyAccessRepository;
private readonly IOrganizationUserRepository _organizationUserRepository;
private readonly IEventService _eventService;
private readonly IMailService _mailService;

public RegenerateUserAsymmetricKeysCommand(
ICurrentContext currentContext,
IUserAsymmetricKeysRepository userAsymmetricKeysRepository,
IPushNotificationService pushService,
ILogger<RegenerateUserAsymmetricKeysCommand> logger)
ILogger<RegenerateUserAsymmetricKeysCommand> logger,
IEmergencyAccessRepository emergencyAccessRepository,
IOrganizationUserRepository organizationUserRepository,
IEventService eventService,
IMailService mailService)
{
_currentContext = currentContext;
_logger = logger;
_userAsymmetricKeysRepository = userAsymmetricKeysRepository;
_pushService = pushService;
_emergencyAccessRepository = emergencyAccessRepository;
_organizationUserRepository = organizationUserRepository;
_eventService = eventService;
_mailService = mailService;
}

public async Task RegenerateKeysAsync(UserAsymmetricKeys userAsymmetricKeys,
Expand All @@ -55,17 +69,61 @@ x.Status is EmergencyAccessStatusType.Confirmed or EmergencyAccessStatusType.Rec
"User asymmetric keys regeneration requested. UserId: {userId} OrganizationMembership: {inOrganizations} DesignatedEmergencyAccess: {hasDesignatedEmergencyAccess} DeviceType: {deviceType}",
userAsymmetricKeys.UserId, inOrganizations, hasDesignatedEmergencyAccess, _currentContext.DeviceType);

// For now, don't regenerate asymmetric keys for user's with organization membership and designated emergency access.
if (inOrganizations || hasDesignatedEmergencyAccess)
var updateDataActions = new List<DatabaseTransactionAction>();
var utcNow = DateTime.UtcNow;

var eaToReset = designatedEmergencyAccess
.Where(ea => ea.Status is EmergencyAccessStatusType.Confirmed
or EmergencyAccessStatusType.RecoveryInitiated
or EmergencyAccessStatusType.RecoveryApproved)
.ToList();
foreach (var ea in eaToReset)
{
updateDataActions.Add(_emergencyAccessRepository.UpdateStatusAndKeyEncryptedById(
ea.Id, EmergencyAccessStatusType.Accepted, null, utcNow));
}

var orgUsersToReset = usersOrganizationAccounts
.Where(ou => ou.Status == OrganizationUserStatusType.Confirmed)
.ToList();
foreach (var orgUser in orgUsersToReset)
{
throw new BadRequestException("Key regeneration not supported for this user.");
updateDataActions.Add(_organizationUserRepository.UpdateStatusAndKeyById(
orgUser.Id, OrganizationUserStatusType.Accepted, null, utcNow));
}

await _userAsymmetricKeysRepository.RegenerateUserAsymmetricKeysAsync(userAsymmetricKeys);
var orgUsersToRemove = usersOrganizationAccounts
.Where(ou => ou.Status == OrganizationUserStatusType.Revoked)
.ToList();
if (orgUsersToRemove.Count > 0)
{
updateDataActions.Add(
_organizationUserRepository.DeleteManyByIds(orgUsersToRemove.Select(ou => ou.Id)));
}

await _userAsymmetricKeysRepository.RegenerateUserAsymmetricKeysAsync(
userAsymmetricKeys, updateDataActions);

_logger.LogInformation(
"User's asymmetric keys regenerated. UserId: {userId} OrganizationMembership: {inOrganizations} DesignatedEmergencyAccess: {hasDesignatedEmergencyAccess} DeviceType: {deviceType}",
userAsymmetricKeys.UserId, inOrganizations, hasDesignatedEmergencyAccess, _currentContext.DeviceType);

await _pushService.PushSyncSettingsAsync(userId.Value);

foreach (var orgUser in orgUsersToRemove)
{
await _eventService.LogOrganizationUserEventAsync(orgUser, EventType.OrganizationUser_Left);
}

foreach (var ea in eaToReset)
{
if (ea.GranteeEmail is null || ea.GrantorEmail is null)
{
continue;
}

await _mailService.SendEmergencyAccessAcceptedEmailAsync(
Comment thread
mzieniukbw marked this conversation as resolved.
ea.GranteeEmail, ea.GrantorEmail);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
#nullable enable
using Bit.Core.KeyManagement.Models.Data;
using Bit.Core.Repositories;

namespace Bit.Core.KeyManagement.Repositories;

public interface IUserAsymmetricKeysRepository
{
Task RegenerateUserAsymmetricKeysAsync(UserAsymmetricKeys userAsymmetricKeys);
Task RegenerateUserAsymmetricKeysAsync(UserAsymmetricKeys userAsymmetricKeys,
IEnumerable<DatabaseTransactionAction> updateDataActions);
}
9 changes: 9 additions & 0 deletions src/Core/Repositories/DatabaseTransactionAction.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using System.Data.Common;

namespace Bit.Core.Repositories;

/// <summary>
/// A database operation that participates in an existing database connection and transaction.
/// Used to compose multiple repository operations into a single atomic transaction.
/// </summary>
public delegate Task DatabaseTransactionAction(DbConnection connection, DbTransaction transaction);
Original file line number Diff line number Diff line change
Expand Up @@ -763,4 +763,43 @@ public async Task<ICollection<OrganizationUser>> GetManyPendingAutoConfirmAsync(
return results.ToList();
}
}

/// <inheritdoc />
public DatabaseTransactionAction UpdateStatusAndKeyById(Guid id,
OrganizationUserStatusType status, string? key, DateTime revisionDate)
{
return async (connection, transaction) =>
{
await connection.ExecuteAsync(
"[dbo].[OrganizationUser_UpdateStatusKeyById]",
new
{
Id = id,
Status = (short)status,
Key = key,
RevisionDate = revisionDate
},
transaction: transaction,
commandType: CommandType.StoredProcedure);
};
}

/// <inheritdoc />
public DatabaseTransactionAction DeleteManyByIds(IEnumerable<Guid> ids)
{
return async (connection, transaction) =>
{
var idsList = ids.ToList();
if (idsList.Count == 0)
{
return;
}

await connection.ExecuteAsync(
"[dbo].[OrganizationUser_DeleteByIds]",
new { Ids = idsList.ToGuidIdArrayTVP() },
transaction: transaction,
commandType: CommandType.StoredProcedure);
};
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.Data;
using Bit.Core.Auth.Entities;
using Bit.Core.Auth.Enums;
using Bit.Core.Auth.Models.Data;
using Bit.Core.KeyManagement.UserKey;
using Bit.Core.Repositories;
Expand Down Expand Up @@ -177,6 +178,26 @@ INNER JOIN
};
}

/// <inheritdoc />
public DatabaseTransactionAction UpdateStatusAndKeyEncryptedById(Guid id,
EmergencyAccessStatusType status, string? keyEncrypted, DateTime revisionDate)
{
return async (connection, transaction) =>
{
await connection.ExecuteAsync(
"[dbo].[EmergencyAccess_UpdateStatusKeyEncryptedById]",
new
{
Id = id,
Status = (byte)status,
KeyEncrypted = keyEncrypted,
RevisionDate = revisionDate
},
transaction: transaction,
commandType: CommandType.StoredProcedure);
};
}

/// <inheritdoc />
public async Task DeleteManyAsync(ICollection<Guid> emergencyAccessIds)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Data;
using Bit.Core.KeyManagement.Models.Data;
using Bit.Core.KeyManagement.Repositories;
using Bit.Core.Repositories;
using Bit.Core.Settings;
using Bit.Infrastructure.Dapper.Repositories;
using Dapper;
Expand All @@ -21,16 +22,33 @@ public UserAsymmetricKeysRepository(string connectionString, string readOnlyConn
{
}

public async Task RegenerateUserAsymmetricKeysAsync(UserAsymmetricKeys userAsymmetricKeys)
public async Task RegenerateUserAsymmetricKeysAsync(UserAsymmetricKeys userAsymmetricKeys,
IEnumerable<DatabaseTransactionAction> updateDataActions)
{
await using var connection = new SqlConnection(ConnectionString);
await connection.OpenAsync();
await using var transaction = connection.BeginTransaction();
try
{
await connection.ExecuteAsync("[dbo].[UserAsymmetricKeys_Regenerate]",
new
{
userAsymmetricKeys.UserId,
userAsymmetricKeys.PublicKey,
PrivateKey = userAsymmetricKeys.UserKeyEncryptedPrivateKey
}, transaction: transaction, commandType: CommandType.StoredProcedure);

await connection.ExecuteAsync("[dbo].[UserAsymmetricKeys_Regenerate]",
new
foreach (var action in updateDataActions)
{
userAsymmetricKeys.UserId,
userAsymmetricKeys.PublicKey,
PrivateKey = userAsymmetricKeys.UserKeyEncryptedPrivateKey
}, commandType: CommandType.StoredProcedure);
await action(connection, transaction);
}

await transaction.CommitAsync();
}
catch
{
await transaction.RollbackAsync();
throw;
}
}
}
Loading
Loading