1
0
mirror of https://github.com/bitwarden/server.git synced 2024-11-21 12:05:42 +01:00

[PM-3797 Part 5] Add reset password keys to key rotation (#3445)

* Add reset password validator with tests

* add organization user rotation methods to repository
- move organization user TVP helper to admin console ownership

* rename account recovery to reset password

* formatting

* move registration of RotateUserKeyCommand to Core and make internal

* add admin console ValidatorServiceCollectionExtensions
This commit is contained in:
Jake Fink 2023-12-14 15:05:19 -05:00 committed by GitHub
parent da0bf77a39
commit b77ee017e3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 372 additions and 42 deletions

View File

@ -118,3 +118,9 @@ public class OrganizationUserBulkRequestModel
[Required] [Required]
public IEnumerable<Guid> Ids { get; set; } public IEnumerable<Guid> Ids { get; set; }
} }
public class ResetPasswordWithOrgIdRequestModel : OrganizationUserResetPasswordEnrollmentRequestModel
{
[Required]
public Guid OrganizationId { get; set; }
}

View File

@ -0,0 +1,64 @@
using Bit.Api.AdminConsole.Models.Request.Organizations;
using Bit.Api.Auth.Validators;
using Bit.Core.Entities;
using Bit.Core.Exceptions;
using Bit.Core.Repositories;
namespace Bit.Api.AdminConsole.Validators;
/// <summary>
/// Organization user implementation for <see cref="IRotationValidator{T,R}"/>
/// Currently responsible for validation of user reset password keys (used by admins to perform account recovery) during user key rotation
/// </summary>
public class OrganizationUserRotationValidator : IRotationValidator<IEnumerable<ResetPasswordWithOrgIdRequestModel>,
IReadOnlyList<OrganizationUser>>
{
private readonly IOrganizationUserRepository _organizationUserRepository;
public OrganizationUserRotationValidator(IOrganizationUserRepository organizationUserRepository) =>
_organizationUserRepository = organizationUserRepository;
public async Task<IReadOnlyList<OrganizationUser>> ValidateAsync(User user,
IEnumerable<ResetPasswordWithOrgIdRequestModel> resetPasswordKeys)
{
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
var result = new List<OrganizationUser>();
if (resetPasswordKeys == null || !resetPasswordKeys.Any())
{
return result;
}
var existing = await _organizationUserRepository.GetManyByUserAsync(user.Id);
if (existing == null || !existing.Any())
{
return result;
}
// Exclude any account recovery that do not have a key.
existing = existing.Where(o => o.ResetPasswordKey != null).ToList();
foreach (var ou in existing)
{
var organizationUser = resetPasswordKeys.FirstOrDefault(a => a.OrganizationId == ou.OrganizationId);
if (organizationUser == null)
{
throw new BadRequestException("All existing reset password keys must be included in the rotation.");
}
if (organizationUser.ResetPasswordKey == null)
{
throw new BadRequestException("Reset Password keys cannot be set to null during rotation.");
}
ou.ResetPasswordKey = organizationUser.ResetPasswordKey;
result.Add(ou);
}
return result;
}
}

View File

@ -1,4 +1,5 @@
using Bit.Api.AdminConsole.Models.Response; using Bit.Api.AdminConsole.Models.Request.Organizations;
using Bit.Api.AdminConsole.Models.Response;
using Bit.Api.Auth.Models.Request; using Bit.Api.Auth.Models.Request;
using Bit.Api.Auth.Models.Request.Accounts; using Bit.Api.Auth.Models.Request.Accounts;
using Bit.Api.Auth.Validators; using Bit.Api.Auth.Validators;
@ -72,6 +73,9 @@ public class AccountsController : Controller
private readonly IRotationValidator<IEnumerable<SendWithIdRequestModel>, IReadOnlyList<Send>> _sendValidator; private readonly IRotationValidator<IEnumerable<SendWithIdRequestModel>, IReadOnlyList<Send>> _sendValidator;
private readonly IRotationValidator<IEnumerable<EmergencyAccessWithIdRequestModel>, IEnumerable<EmergencyAccess>> private readonly IRotationValidator<IEnumerable<EmergencyAccessWithIdRequestModel>, IEnumerable<EmergencyAccess>>
_emergencyAccessValidator; _emergencyAccessValidator;
private readonly IRotationValidator<IEnumerable<ResetPasswordWithOrgIdRequestModel>,
IReadOnlyList<OrganizationUser>>
_organizationUserValidator;
public AccountsController( public AccountsController(
@ -96,7 +100,9 @@ public class AccountsController : Controller
IRotationValidator<IEnumerable<FolderWithIdRequestModel>, IEnumerable<Folder>> folderValidator, IRotationValidator<IEnumerable<FolderWithIdRequestModel>, IEnumerable<Folder>> folderValidator,
IRotationValidator<IEnumerable<SendWithIdRequestModel>, IReadOnlyList<Send>> sendValidator, IRotationValidator<IEnumerable<SendWithIdRequestModel>, IReadOnlyList<Send>> sendValidator,
IRotationValidator<IEnumerable<EmergencyAccessWithIdRequestModel>, IEnumerable<EmergencyAccess>> IRotationValidator<IEnumerable<EmergencyAccessWithIdRequestModel>, IEnumerable<EmergencyAccess>>
emergencyAccessValidator emergencyAccessValidator,
IRotationValidator<IEnumerable<ResetPasswordWithOrgIdRequestModel>, IReadOnlyList<OrganizationUser>>
organizationUserValidator
) )
{ {
_cipherRepository = cipherRepository; _cipherRepository = cipherRepository;
@ -120,6 +126,7 @@ public class AccountsController : Controller
_folderValidator = folderValidator; _folderValidator = folderValidator;
_sendValidator = sendValidator; _sendValidator = sendValidator;
_emergencyAccessValidator = emergencyAccessValidator; _emergencyAccessValidator = emergencyAccessValidator;
_organizationUserValidator = organizationUserValidator;
} }
#region DEPRECATED (Moved to Identity Service) #region DEPRECATED (Moved to Identity Service)
@ -428,8 +435,8 @@ public class AccountsController : Controller
Ciphers = await _cipherValidator.ValidateAsync(user, model.Ciphers), Ciphers = await _cipherValidator.ValidateAsync(user, model.Ciphers),
Folders = await _folderValidator.ValidateAsync(user, model.Folders), Folders = await _folderValidator.ValidateAsync(user, model.Folders),
Sends = await _sendValidator.ValidateAsync(user, model.Sends), Sends = await _sendValidator.ValidateAsync(user, model.Sends),
EmergencyAccessKeys = await _emergencyAccessValidator.ValidateAsync(user, model.EmergencyAccessKeys), EmergencyAccesses = await _emergencyAccessValidator.ValidateAsync(user, model.EmergencyAccessKeys),
ResetPasswordKeys = new List<OrganizationUser>(), OrganizationUsers = await _organizationUserValidator.ValidateAsync(user, model.ResetPasswordKeys)
}; };
result = await _rotateUserKeyCommand.RotateUserKeyAsync(user, dataModel); result = await _rotateUserKeyCommand.RotateUserKeyAsync(user, dataModel);

View File

@ -18,6 +18,6 @@ public class UpdateKeyRequestModel
public IEnumerable<FolderWithIdRequestModel> Folders { get; set; } public IEnumerable<FolderWithIdRequestModel> Folders { get; set; }
public IEnumerable<SendWithIdRequestModel> Sends { get; set; } public IEnumerable<SendWithIdRequestModel> Sends { get; set; }
public IEnumerable<EmergencyAccessWithIdRequestModel> EmergencyAccessKeys { get; set; } public IEnumerable<EmergencyAccessWithIdRequestModel> EmergencyAccessKeys { get; set; }
public IEnumerable<OrganizationUserUpdateRequestModel> ResetPasswordKeys { get; set; } public IEnumerable<ResetPasswordWithOrgIdRequestModel> ResetPasswordKeys { get; set; }
} }

View File

@ -7,6 +7,8 @@ using Stripe;
using Bit.Core.Utilities; using Bit.Core.Utilities;
using IdentityModel; using IdentityModel;
using System.Globalization; using System.Globalization;
using Bit.Api.AdminConsole.Models.Request.Organizations;
using Bit.Api.AdminConsole.Validators;
using Bit.Api.Auth.Models.Request; using Bit.Api.Auth.Models.Request;
using Bit.Api.Auth.Validators; using Bit.Api.Auth.Validators;
using Bit.Api.Tools.Models.Request; using Bit.Api.Tools.Models.Request;
@ -22,8 +24,8 @@ using Bit.SharedWeb.Utilities;
using Microsoft.AspNetCore.Diagnostics.HealthChecks; using Microsoft.AspNetCore.Diagnostics.HealthChecks;
using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.DependencyInjection.Extensions;
using Bit.Core.Auth.Identity; using Bit.Core.Auth.Identity;
using Bit.Core.Auth.UserFeatures.UserKey; using Bit.Core.Auth.UserFeatures;
using Bit.Core.Auth.UserFeatures.UserKey.Implementations; using Bit.Core.Entities;
using Bit.Core.OrganizationFeatures.OrganizationSubscriptions; using Bit.Core.OrganizationFeatures.OrganizationSubscriptions;
using Bit.Core.Tools.Entities; using Bit.Core.Tools.Entities;
using Bit.Core.Vault.Entities; using Bit.Core.Vault.Entities;
@ -143,7 +145,7 @@ public class Startup
services.AddScoped<AuthenticatorTokenProvider>(); services.AddScoped<AuthenticatorTokenProvider>();
// Key Rotation // Key Rotation
services.AddScoped<IRotateUserKeyCommand, RotateUserKeyCommand>(); services.AddUserKeyCommands(globalSettings);
services services
.AddScoped<IRotationValidator<IEnumerable<CipherWithIdRequestModel>, IEnumerable<Cipher>>, .AddScoped<IRotationValidator<IEnumerable<CipherWithIdRequestModel>, IEnumerable<Cipher>>,
CipherRotationValidator>(); CipherRotationValidator>();
@ -156,6 +158,11 @@ public class Startup
services services
.AddScoped<IRotationValidator<IEnumerable<EmergencyAccessWithIdRequestModel>, IEnumerable<EmergencyAccess>>, .AddScoped<IRotationValidator<IEnumerable<EmergencyAccessWithIdRequestModel>, IEnumerable<EmergencyAccess>>,
EmergencyAccessRotationValidator>(); EmergencyAccessRotationValidator>();
services
.AddScoped<IRotationValidator<IEnumerable<ResetPasswordWithOrgIdRequestModel>,
IReadOnlyList<OrganizationUser>>
, OrganizationUserRotationValidator>();
// Services // Services
services.AddBaseServices(globalSettings); services.AddBaseServices(globalSettings);

View File

@ -1,4 +1,5 @@
using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Enums;
using Bit.Core.Auth.UserFeatures.UserKey;
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Models.Data; using Bit.Core.Models.Data;
@ -42,4 +43,13 @@ public interface IOrganizationUserRepository : IRepository<OrganizationUser, Gui
Task RestoreAsync(Guid id, OrganizationUserStatusType status); Task RestoreAsync(Guid id, OrganizationUserStatusType status);
Task<IEnumerable<OrganizationUserPolicyDetails>> GetByUserIdWithPolicyDetailsAsync(Guid userId, PolicyType policyType); Task<IEnumerable<OrganizationUserPolicyDetails>> GetByUserIdWithPolicyDetailsAsync(Guid userId, PolicyType policyType);
Task<int> GetOccupiedSmSeatCountByOrganizationIdAsync(Guid organizationId); Task<int> GetOccupiedSmSeatCountByOrganizationIdAsync(Guid organizationId);
/// <summary>
/// Updates encrypted data for organization users during a key rotation
/// </summary>
/// <param name="userId">The user that initiated the key rotation</param>
/// <param name="resetPasswordKeys">A list of organization users with updated reset password keys</param>
UpdateEncryptedDataForKeyRotation UpdateForKeyRotation(Guid userId,
IEnumerable<OrganizationUser> resetPasswordKeys);
} }

View File

@ -13,6 +13,6 @@ public class RotateUserKeyData
public IEnumerable<Cipher> Ciphers { get; set; } public IEnumerable<Cipher> Ciphers { get; set; }
public IEnumerable<Folder> Folders { get; set; } public IEnumerable<Folder> Folders { get; set; }
public IReadOnlyList<Send> Sends { get; set; } public IReadOnlyList<Send> Sends { get; set; }
public IEnumerable<EmergencyAccess> EmergencyAccessKeys { get; set; } public IEnumerable<EmergencyAccess> EmergencyAccesses { get; set; }
public IEnumerable<OrganizationUser> ResetPasswordKeys { get; set; } public IReadOnlyList<OrganizationUser> OrganizationUsers { get; set; }
} }

View File

@ -17,6 +17,7 @@ public class RotateUserKeyCommand : IRotateUserKeyCommand
private readonly IFolderRepository _folderRepository; private readonly IFolderRepository _folderRepository;
private readonly ISendRepository _sendRepository; private readonly ISendRepository _sendRepository;
private readonly IEmergencyAccessRepository _emergencyAccessRepository; private readonly IEmergencyAccessRepository _emergencyAccessRepository;
private readonly IOrganizationUserRepository _organizationUserRepository;
private readonly IPushNotificationService _pushService; private readonly IPushNotificationService _pushService;
private readonly IdentityErrorDescriber _identityErrorDescriber; private readonly IdentityErrorDescriber _identityErrorDescriber;
@ -33,7 +34,7 @@ public class RotateUserKeyCommand : IRotateUserKeyCommand
/// <param name="errors">Provides a password mismatch error if master password hash validation fails</param> /// <param name="errors">Provides a password mismatch error if master password hash validation fails</param>
public RotateUserKeyCommand(IUserService userService, IUserRepository userRepository, public RotateUserKeyCommand(IUserService userService, IUserRepository userRepository,
ICipherRepository cipherRepository, IFolderRepository folderRepository, ISendRepository sendRepository, ICipherRepository cipherRepository, IFolderRepository folderRepository, ISendRepository sendRepository,
IEmergencyAccessRepository emergencyAccessRepository, IEmergencyAccessRepository emergencyAccessRepository, IOrganizationUserRepository organizationUserRepository,
IPushNotificationService pushService, IdentityErrorDescriber errors) IPushNotificationService pushService, IdentityErrorDescriber errors)
{ {
_userService = userService; _userService = userService;
@ -42,6 +43,7 @@ public class RotateUserKeyCommand : IRotateUserKeyCommand
_folderRepository = folderRepository; _folderRepository = folderRepository;
_sendRepository = sendRepository; _sendRepository = sendRepository;
_emergencyAccessRepository = emergencyAccessRepository; _emergencyAccessRepository = emergencyAccessRepository;
_organizationUserRepository = organizationUserRepository;
_pushService = pushService; _pushService = pushService;
_identityErrorDescriber = errors; _identityErrorDescriber = errors;
} }
@ -65,8 +67,8 @@ public class RotateUserKeyCommand : IRotateUserKeyCommand
user.SecurityStamp = Guid.NewGuid().ToString(); user.SecurityStamp = Guid.NewGuid().ToString();
user.Key = model.Key; user.Key = model.Key;
user.PrivateKey = model.PrivateKey; user.PrivateKey = model.PrivateKey;
if (model.Ciphers.Any() || model.Folders.Any() || model.Sends.Any() || model.EmergencyAccessKeys.Any() || if (model.Ciphers.Any() || model.Folders.Any() || model.Sends.Any() || model.EmergencyAccesses.Any() ||
model.ResetPasswordKeys.Any()) model.OrganizationUsers.Any())
{ {
List<UpdateEncryptedDataForKeyRotation> saveEncryptedDataActions = new(); List<UpdateEncryptedDataForKeyRotation> saveEncryptedDataActions = new();
@ -85,10 +87,16 @@ public class RotateUserKeyCommand : IRotateUserKeyCommand
saveEncryptedDataActions.Add(_sendRepository.UpdateForKeyRotation(user.Id, model.Sends)); saveEncryptedDataActions.Add(_sendRepository.UpdateForKeyRotation(user.Id, model.Sends));
} }
if (model.EmergencyAccessKeys.Any()) if (model.EmergencyAccesses.Any())
{ {
saveEncryptedDataActions.Add( saveEncryptedDataActions.Add(
_emergencyAccessRepository.UpdateForKeyRotation(user.Id, model.EmergencyAccessKeys)); _emergencyAccessRepository.UpdateForKeyRotation(user.Id, model.EmergencyAccesses));
}
if (model.OrganizationUsers.Any())
{
saveEncryptedDataActions.Add(
_organizationUserRepository.UpdateForKeyRotation(user.Id, model.OrganizationUsers));
} }
await _userRepository.UpdateUserKeyAndEncryptedDataAsync(user, saveEncryptedDataActions); await _userRepository.UpdateUserKeyAndEncryptedDataAsync(user, saveEncryptedDataActions);

View File

@ -1,5 +1,7 @@
 
using Bit.Core.Auth.UserFeatures.UserKey;
using Bit.Core.Auth.UserFeatures.UserKey.Implementations;
using Bit.Core.Auth.UserFeatures.UserMasterPassword; using Bit.Core.Auth.UserFeatures.UserMasterPassword;
using Bit.Core.Auth.UserFeatures.UserMasterPassword.Interfaces; using Bit.Core.Auth.UserFeatures.UserMasterPassword.Interfaces;
using Bit.Core.Auth.UserFeatures.WebAuthnLogin; using Bit.Core.Auth.UserFeatures.WebAuthnLogin;
@ -19,6 +21,11 @@ public static class UserServiceCollectionExtensions
services.AddWebAuthnLoginCommands(); services.AddWebAuthnLoginCommands();
} }
public static void AddUserKeyCommands(this IServiceCollection services, IGlobalSettings globalSettings)
{
services.AddScoped<IRotateUserKeyCommand, RotateUserKeyCommand>();
}
private static void AddUserPasswordCommands(this IServiceCollection services) private static void AddUserPasswordCommands(this IServiceCollection services)
{ {
services.AddScoped<ISetInitialMasterPasswordCommand, SetInitialMasterPasswordCommand>(); services.AddScoped<ISetInitialMasterPasswordCommand, SetInitialMasterPasswordCommand>();

View File

@ -0,0 +1,34 @@
using System.Data;
using Bit.Core.Entities;
using Dapper;
namespace Bit.Infrastructure.Dapper.AdminConsole.Helpers;
public static class OrganizationUserHelpers
{
public static DataTable ToTvp(this IEnumerable<OrganizationUser> orgUsers)
{
var table = new DataTable();
table.SetTypeName("[dbo].[OrganizationUserType2]");
var columnData = new List<(string name, Type type, Func<OrganizationUser, object> getter)>
{
(nameof(OrganizationUser.Id), typeof(Guid), ou => ou.Id),
(nameof(OrganizationUser.OrganizationId), typeof(Guid), ou => ou.OrganizationId),
(nameof(OrganizationUser.UserId), typeof(Guid), ou => ou.UserId),
(nameof(OrganizationUser.Email), typeof(string), ou => ou.Email),
(nameof(OrganizationUser.Key), typeof(string), ou => ou.Key),
(nameof(OrganizationUser.Status), typeof(byte), ou => ou.Status),
(nameof(OrganizationUser.Type), typeof(byte), ou => ou.Type),
(nameof(OrganizationUser.AccessAll), typeof(bool), ou => ou.AccessAll),
(nameof(OrganizationUser.ExternalId), typeof(string), ou => ou.ExternalId),
(nameof(OrganizationUser.CreationDate), typeof(DateTime), ou => ou.CreationDate),
(nameof(OrganizationUser.RevisionDate), typeof(DateTime), ou => ou.RevisionDate),
(nameof(OrganizationUser.Permissions), typeof(string), ou => ou.Permissions),
(nameof(OrganizationUser.ResetPasswordKey), typeof(string), ou => ou.ResetPasswordKey),
(nameof(OrganizationUser.AccessSecretsManager), typeof(bool), ou => ou.AccessSecretsManager),
};
return orgUsers.BuildTable(table, columnData);
}
}

View File

@ -2,12 +2,14 @@
using System.Text.Json; using System.Text.Json;
using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Enums;
using Bit.Core.Auth.UserFeatures.UserKey;
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Models.Data; using Bit.Core.Models.Data;
using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.Models.Data.Organizations.OrganizationUsers;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Settings; using Bit.Core.Settings;
using Bit.Infrastructure.Dapper.AdminConsole.Helpers;
using Dapper; using Dapper;
using Microsoft.Data.SqlClient; using Microsoft.Data.SqlClient;
@ -520,4 +522,32 @@ public class OrganizationUserRepository : Repository<OrganizationUser, Guid>, IO
return results.ToList(); return results.ToList();
} }
} }
/// <inheritdoc />
public UpdateEncryptedDataForKeyRotation UpdateForKeyRotation(
Guid userId, IEnumerable<OrganizationUser> resetPasswordKeys)
{
return async (SqlConnection connection, SqlTransaction transaction) =>
{
const string sql = @"
UPDATE
[dbo].[OrganizationUser]
SET
[ResetPasswordKey] = AR.[ResetPasswordKey]
FROM
[dbo].[OrganizationUser] OU
INNER JOIN
@ResetPasswordKeys AR ON OU.Id = AR.Id
WHERE
OU.[UserId] = @UserId";
var organizationUsersTVP = resetPasswordKeys.ToTvp();
await connection.ExecuteAsync(
sql,
new { UserId = userId, resetPasswordKeys = organizationUsersTVP },
transaction: transaction,
commandType: CommandType.Text);
};
}
} }

View File

@ -59,32 +59,6 @@ public static class DapperHelpers
return table; return table;
} }
public static DataTable ToTvp(this IEnumerable<OrganizationUser> orgUsers)
{
var table = new DataTable();
table.SetTypeName("[dbo].[OrganizationUserType2]");
var columnData = new List<(string name, Type type, Func<OrganizationUser, object> getter)>
{
(nameof(OrganizationUser.Id), typeof(Guid), ou => ou.Id),
(nameof(OrganizationUser.OrganizationId), typeof(Guid), ou => ou.OrganizationId),
(nameof(OrganizationUser.UserId), typeof(Guid), ou => ou.UserId),
(nameof(OrganizationUser.Email), typeof(string), ou => ou.Email),
(nameof(OrganizationUser.Key), typeof(string), ou => ou.Key),
(nameof(OrganizationUser.Status), typeof(byte), ou => ou.Status),
(nameof(OrganizationUser.Type), typeof(byte), ou => ou.Type),
(nameof(OrganizationUser.AccessAll), typeof(bool), ou => ou.AccessAll),
(nameof(OrganizationUser.ExternalId), typeof(string), ou => ou.ExternalId),
(nameof(OrganizationUser.CreationDate), typeof(DateTime), ou => ou.CreationDate),
(nameof(OrganizationUser.RevisionDate), typeof(DateTime), ou => ou.RevisionDate),
(nameof(OrganizationUser.Permissions), typeof(string), ou => ou.Permissions),
(nameof(OrganizationUser.ResetPasswordKey), typeof(string), ou => ou.ResetPasswordKey),
(nameof(OrganizationUser.AccessSecretsManager), typeof(bool), ou => ou.AccessSecretsManager),
};
return orgUsers.BuildTable(table, columnData);
}
public static DataTable ToTvp(this IEnumerable<OrganizationSponsorship> organizationSponsorships) public static DataTable ToTvp(this IEnumerable<OrganizationSponsorship> organizationSponsorships)
{ {
var table = new DataTable(); var table = new DataTable();

View File

@ -1,5 +1,6 @@
using AutoMapper; using AutoMapper;
using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Enums;
using Bit.Core.Auth.UserFeatures.UserKey;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Models.Data; using Bit.Core.Models.Data;
using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.Models.Data.Organizations.OrganizationUsers;
@ -640,4 +641,35 @@ public class OrganizationUserRepository : Repository<Core.Entities.OrganizationU
return await GetCountFromQuery(query); return await GetCountFromQuery(query);
} }
/// <inheritdoc />
public UpdateEncryptedDataForKeyRotation UpdateForKeyRotation(
Guid userId, IEnumerable<Core.Entities.OrganizationUser> resetPasswordKeys)
{
return async (_, _) =>
{
var newOrganizationUsers = resetPasswordKeys.ToList();
using var scope = ServiceScopeFactory.CreateScope();
var dbContext = GetDatabaseContext(scope);
// Get user organization users
var userOrganizationUsers = await GetDbSet(dbContext)
.Where(c => c.UserId == userId)
.ToListAsync();
// Filter to only organization users that are included
var validOrganizationUsers = userOrganizationUsers
.Where(organizationUser =>
newOrganizationUsers.Any(newOrganizationUser => newOrganizationUser.Id == organizationUser.Id));
foreach (var organizationUser in validOrganizationUsers)
{
var updateOrganizationUser =
newOrganizationUsers.First(newOrganizationUser => newOrganizationUser.Id == organizationUser.Id);
organizationUser.ResetPasswordKey = updateOrganizationUser.ResetPasswordKey;
}
await dbContext.SaveChangesAsync();
};
}
} }

View File

@ -0,0 +1,143 @@
using Bit.Api.AdminConsole.Models.Request.Organizations;
using Bit.Api.AdminConsole.Validators;
using Bit.Core.Entities;
using Bit.Core.Exceptions;
using Bit.Core.Repositories;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using Xunit;
namespace Bit.Api.Test.AdminConsole.Validators;
[SutProviderCustomize]
public class OrganizationUserRotationValidatorTests
{
[Theory]
[BitAutoData]
public async Task ValidateAsync_Success_ReturnsValid(
SutProvider<OrganizationUserRotationValidator> sutProvider, User user,
IEnumerable<ResetPasswordWithOrgIdRequestModel> resetPasswordKeys)
{
var existingUserResetPassword = resetPasswordKeys
.Select(a =>
new OrganizationUser
{
Id = new Guid(),
ResetPasswordKey = a.ResetPasswordKey,
OrganizationId = a.OrganizationId
}).ToList();
sutProvider.GetDependency<IOrganizationUserRepository>().GetManyByUserAsync(user.Id)
.Returns(existingUserResetPassword);
var result = await sutProvider.Sut.ValidateAsync(user, resetPasswordKeys);
Assert.Equal(result.Select(r => r.OrganizationId), resetPasswordKeys.Select(a => a.OrganizationId));
}
[Theory]
[BitAutoData]
public async Task ValidateAsync_NullResetPasswordKeys_ReturnsEmptyList(
SutProvider<OrganizationUserRotationValidator> sutProvider, User user)
{
// Arrange
IEnumerable<ResetPasswordWithOrgIdRequestModel> resetPasswordKeys = null;
// Act
var result = await sutProvider.Sut.ValidateAsync(user, resetPasswordKeys);
// Assert
Assert.NotNull(result);
Assert.Empty(result);
}
[Theory]
[BitAutoData]
public async Task ValidateAsync_NoOrgUsers_ReturnsEmptyList(
SutProvider<OrganizationUserRotationValidator> sutProvider, User user,
IEnumerable<ResetPasswordWithOrgIdRequestModel> resetPasswordKeys)
{
// Arrange
sutProvider.GetDependency<IOrganizationUserRepository>().GetManyByUserAsync(user.Id)
.Returns(new List<OrganizationUser>()); // Return an empty list
// Act
var result = await sutProvider.Sut.ValidateAsync(user, resetPasswordKeys);
// Assert
Assert.NotNull(result);
Assert.Empty(result);
}
[Theory]
[BitAutoData]
public async Task ValidateAsync_MissingResetPassword_Throws(
SutProvider<OrganizationUserRotationValidator> sutProvider, User user,
IEnumerable<ResetPasswordWithOrgIdRequestModel> resetPasswordKeys)
{
var existingUserResetPassword = resetPasswordKeys
.Select(a =>
new OrganizationUser
{
Id = new Guid(),
ResetPasswordKey = a.ResetPasswordKey,
OrganizationId = a.OrganizationId
}).ToList();
existingUserResetPassword.Add(new OrganizationUser
{
Id = Guid.NewGuid(),
ResetPasswordKey = "Missing ResetPasswordKey"
});
sutProvider.GetDependency<IOrganizationUserRepository>().GetManyByUserAsync(user.Id)
.Returns(existingUserResetPassword);
await Assert.ThrowsAsync<BadRequestException>(async () =>
await sutProvider.Sut.ValidateAsync(user, resetPasswordKeys));
}
[Theory]
[BitAutoData]
public async Task ValidateAsync_ResetPasswordDoesNotBelongToUser_NotReturned(
SutProvider<OrganizationUserRotationValidator> sutProvider, User user,
IEnumerable<ResetPasswordWithOrgIdRequestModel> resetPasswordKeys)
{
var existingUserResetPassword = resetPasswordKeys
.Select(a =>
new OrganizationUser
{
Id = new Guid(),
ResetPasswordKey = a.ResetPasswordKey,
OrganizationId = a.OrganizationId
}).ToList();
existingUserResetPassword.RemoveAt(0);
sutProvider.GetDependency<IOrganizationUserRepository>().GetManyByUserAsync(user.Id)
.Returns(existingUserResetPassword);
var result = await sutProvider.Sut.ValidateAsync(user, resetPasswordKeys);
Assert.DoesNotContain(result, c => c.Id == resetPasswordKeys.First().OrganizationId);
}
[Theory]
[BitAutoData]
public async Task ValidateAsync_AttemptToSetKeyToNull_Throws(
SutProvider<OrganizationUserRotationValidator> sutProvider, User user,
IEnumerable<ResetPasswordWithOrgIdRequestModel> resetPasswordKeys)
{
var existingUserResetPassword = resetPasswordKeys
.Select(a =>
new OrganizationUser
{
Id = new Guid(),
ResetPasswordKey = a.ResetPasswordKey,
OrganizationId = a.OrganizationId
}).ToList();
sutProvider.GetDependency<IOrganizationUserRepository>().GetManyByUserAsync(user.Id)
.Returns(existingUserResetPassword);
resetPasswordKeys.First().ResetPasswordKey = null;
await Assert.ThrowsAsync<BadRequestException>(async () =>
await sutProvider.Sut.ValidateAsync(user, resetPasswordKeys));
}
}

View File

@ -1,4 +1,5 @@
using System.Security.Claims; using System.Security.Claims;
using Bit.Api.AdminConsole.Models.Request.Organizations;
using Bit.Api.Auth.Controllers; using Bit.Api.Auth.Controllers;
using Bit.Api.Auth.Models.Request; using Bit.Api.Auth.Models.Request;
using Bit.Api.Auth.Models.Request.Accounts; using Bit.Api.Auth.Models.Request.Accounts;
@ -60,6 +61,9 @@ public class AccountsControllerTests : IDisposable
private readonly IRotationValidator<IEnumerable<SendWithIdRequestModel>, IReadOnlyList<Send>> _sendValidator; private readonly IRotationValidator<IEnumerable<SendWithIdRequestModel>, IReadOnlyList<Send>> _sendValidator;
private readonly IRotationValidator<IEnumerable<EmergencyAccessWithIdRequestModel>, IEnumerable<EmergencyAccess>> private readonly IRotationValidator<IEnumerable<EmergencyAccessWithIdRequestModel>, IEnumerable<EmergencyAccess>>
_emergencyAccessValidator; _emergencyAccessValidator;
private readonly IRotationValidator<IEnumerable<ResetPasswordWithOrgIdRequestModel>,
IReadOnlyList<OrganizationUser>>
_resetPasswordValidator;
public AccountsControllerTests() public AccountsControllerTests()
@ -88,6 +92,9 @@ public class AccountsControllerTests : IDisposable
_sendValidator = Substitute.For<IRotationValidator<IEnumerable<SendWithIdRequestModel>, IReadOnlyList<Send>>>(); _sendValidator = Substitute.For<IRotationValidator<IEnumerable<SendWithIdRequestModel>, IReadOnlyList<Send>>>();
_emergencyAccessValidator = Substitute.For<IRotationValidator<IEnumerable<EmergencyAccessWithIdRequestModel>, _emergencyAccessValidator = Substitute.For<IRotationValidator<IEnumerable<EmergencyAccessWithIdRequestModel>,
IEnumerable<EmergencyAccess>>>(); IEnumerable<EmergencyAccess>>>();
_resetPasswordValidator = Substitute
.For<IRotationValidator<IEnumerable<ResetPasswordWithOrgIdRequestModel>,
IReadOnlyList<OrganizationUser>>>();
_sut = new AccountsController( _sut = new AccountsController(
_globalSettings, _globalSettings,
@ -110,7 +117,8 @@ public class AccountsControllerTests : IDisposable
_cipherValidator, _cipherValidator,
_folderValidator, _folderValidator,
_sendValidator, _sendValidator,
_emergencyAccessValidator _emergencyAccessValidator,
_resetPasswordValidator
); );
} }