mirror of
https://github.com/bitwarden/server
synced 2025-12-25 12:43:14 +00:00
[PM-12607] Move key rotation & validators to km ownership (#4941)
* Move key rotation & validators to km ownership * Fix build errors * Fix build errors * Fix import ordering * Update validator namespace * Move key rotation data to km ownership * Fix linting * Fix namespaces * Fix namespace * Fix namespaces * Move rotateuserkeycommandtests to km ownership
This commit is contained in:
45
src/Api/KeyManagement/Validators/CipherRotationValidator.cs
Normal file
45
src/Api/KeyManagement/Validators/CipherRotationValidator.cs
Normal file
@@ -0,0 +1,45 @@
|
||||
using Bit.Api.Vault.Models.Request;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Vault.Entities;
|
||||
using Bit.Core.Vault.Repositories;
|
||||
|
||||
namespace Bit.Api.KeyManagement.Validators;
|
||||
|
||||
public class CipherRotationValidator : IRotationValidator<IEnumerable<CipherWithIdRequestModel>, IEnumerable<Cipher>>
|
||||
{
|
||||
private readonly ICipherRepository _cipherRepository;
|
||||
|
||||
public CipherRotationValidator(ICipherRepository cipherRepository)
|
||||
{
|
||||
_cipherRepository = cipherRepository;
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<Cipher>> ValidateAsync(User user, IEnumerable<CipherWithIdRequestModel> ciphers)
|
||||
{
|
||||
var result = new List<Cipher>();
|
||||
|
||||
var existingCiphers = await _cipherRepository.GetManyByUserIdAsync(user.Id);
|
||||
if (existingCiphers == null)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
var existingUserCiphers = existingCiphers.Where(c => c.OrganizationId == null);
|
||||
if (existingUserCiphers.Count() == 0)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
foreach (var existing in existingUserCiphers)
|
||||
{
|
||||
var cipher = ciphers.FirstOrDefault(c => c.Id == existing.Id);
|
||||
if (cipher == null)
|
||||
{
|
||||
throw new BadRequestException("All existing ciphers must be included in the rotation.");
|
||||
}
|
||||
result.Add(cipher.ToCipher(existing));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
using Bit.Api.Auth.Models.Request;
|
||||
using Bit.Core.Auth.Entities;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
|
||||
namespace Bit.Api.KeyManagement.Validators;
|
||||
|
||||
public class EmergencyAccessRotationValidator : IRotationValidator<IEnumerable<EmergencyAccessWithIdRequestModel>,
|
||||
IEnumerable<EmergencyAccess>>
|
||||
{
|
||||
private readonly IEmergencyAccessRepository _emergencyAccessRepository;
|
||||
private readonly IUserService _userService;
|
||||
|
||||
public EmergencyAccessRotationValidator(IEmergencyAccessRepository emergencyAccessRepository,
|
||||
IUserService userService)
|
||||
{
|
||||
_emergencyAccessRepository = emergencyAccessRepository;
|
||||
_userService = userService;
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<EmergencyAccess>> ValidateAsync(User user,
|
||||
IEnumerable<EmergencyAccessWithIdRequestModel> emergencyAccessKeys)
|
||||
{
|
||||
var result = new List<EmergencyAccess>();
|
||||
|
||||
var existing = await _emergencyAccessRepository.GetManyDetailsByGrantorIdAsync(user.Id);
|
||||
if (existing == null || existing.Count == 0)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
// Exclude any emergency access that has not been confirmed yet.
|
||||
existing = existing.Where(ea => ea.KeyEncrypted != null).ToList();
|
||||
|
||||
foreach (var ea in existing)
|
||||
{
|
||||
var emergencyAccess = emergencyAccessKeys.FirstOrDefault(c => c.Id == ea.Id);
|
||||
if (emergencyAccess == null)
|
||||
{
|
||||
throw new BadRequestException("All existing emergency access keys must be included in the rotation.");
|
||||
}
|
||||
|
||||
if (emergencyAccess.KeyEncrypted == null)
|
||||
{
|
||||
throw new BadRequestException("Emergency access keys cannot be set to null during rotation.");
|
||||
}
|
||||
|
||||
result.Add(emergencyAccess.ToEmergencyAccess(ea));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
39
src/Api/KeyManagement/Validators/FolderRotationValidator.cs
Normal file
39
src/Api/KeyManagement/Validators/FolderRotationValidator.cs
Normal file
@@ -0,0 +1,39 @@
|
||||
using Bit.Api.Vault.Models.Request;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Vault.Entities;
|
||||
using Bit.Core.Vault.Repositories;
|
||||
|
||||
namespace Bit.Api.KeyManagement.Validators;
|
||||
|
||||
public class FolderRotationValidator : IRotationValidator<IEnumerable<FolderWithIdRequestModel>, IEnumerable<Folder>>
|
||||
{
|
||||
private readonly IFolderRepository _folderRepository;
|
||||
|
||||
public FolderRotationValidator(IFolderRepository folderRepository)
|
||||
{
|
||||
_folderRepository = folderRepository;
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<Folder>> ValidateAsync(User user, IEnumerable<FolderWithIdRequestModel> folders)
|
||||
{
|
||||
var result = new List<Folder>();
|
||||
|
||||
var existingFolders = await _folderRepository.GetManyByUserIdAsync(user.Id);
|
||||
if (existingFolders == null || existingFolders.Count == 0)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
foreach (var existing in existingFolders)
|
||||
{
|
||||
var folder = folders.FirstOrDefault(c => c.Id == existing.Id);
|
||||
if (folder == null)
|
||||
{
|
||||
throw new BadRequestException("All existing folders must be included in the rotation.");
|
||||
}
|
||||
result.Add(folder.ToFolder(existing));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
22
src/Api/KeyManagement/Validators/IRotationValidator.cs
Normal file
22
src/Api/KeyManagement/Validators/IRotationValidator.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Exceptions;
|
||||
|
||||
namespace Bit.Api.KeyManagement.Validators;
|
||||
|
||||
/// <summary>
|
||||
/// A consistent interface for domains to validate re-encrypted data before saved to database. Some examples are:<br/>
|
||||
/// - All available encrypted data is accounted for<br/>
|
||||
/// - All provided encrypted data belongs to the user
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Request model</typeparam>
|
||||
/// <typeparam name="R">Domain model</typeparam>
|
||||
public interface IRotationValidator<T, R>
|
||||
{
|
||||
/// <summary>
|
||||
/// Validates re-encrypted data before being saved to database.
|
||||
/// </summary>
|
||||
/// <param name="user">Request model</param>
|
||||
/// <param name="data">Domain model</param>
|
||||
/// <exception cref="BadRequestException">Throws if data fails validation</exception>
|
||||
Task<R> ValidateAsync(User user, T data);
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
using Bit.Api.AdminConsole.Models.Request.Organizations;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
|
||||
namespace Bit.Api.KeyManagement.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>();
|
||||
|
||||
var existing = await _organizationUserRepository.GetManyByUserAsync(user.Id);
|
||||
if (existing == null || existing.Count == 0)
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
52
src/Api/KeyManagement/Validators/SendRotationValidator.cs
Normal file
52
src/Api/KeyManagement/Validators/SendRotationValidator.cs
Normal file
@@ -0,0 +1,52 @@
|
||||
using Bit.Api.Tools.Models.Request;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Tools.Entities;
|
||||
using Bit.Core.Tools.Repositories;
|
||||
using Bit.Core.Tools.Services;
|
||||
|
||||
namespace Bit.Api.KeyManagement.Validators;
|
||||
|
||||
/// <summary>
|
||||
/// Send implementation for <see cref="IRotationValidator{T,R}"/>
|
||||
/// </summary>
|
||||
public class SendRotationValidator : IRotationValidator<IEnumerable<SendWithIdRequestModel>, IReadOnlyList<Send>>
|
||||
{
|
||||
private readonly ISendService _sendService;
|
||||
private readonly ISendRepository _sendRepository;
|
||||
|
||||
/// <summary>
|
||||
/// Instantiates a new <see cref="SendRotationValidator"/>
|
||||
/// </summary>
|
||||
/// <param name="sendService">Enables conversion of <see cref="SendWithIdRequestModel"/> to <see cref="Send"/></param>
|
||||
/// <param name="sendRepository">Retrieves all user <see cref="Send"/>s</param>
|
||||
public SendRotationValidator(ISendService sendService, ISendRepository sendRepository)
|
||||
{
|
||||
_sendService = sendService;
|
||||
_sendRepository = sendRepository;
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<Send>> ValidateAsync(User user, IEnumerable<SendWithIdRequestModel> sends)
|
||||
{
|
||||
var result = new List<Send>();
|
||||
|
||||
var existingSends = await _sendRepository.GetManyByUserIdAsync(user.Id);
|
||||
if (existingSends == null || existingSends.Count == 0)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
foreach (var existing in existingSends)
|
||||
{
|
||||
var send = sends.FirstOrDefault(c => c.Id == existing.Id);
|
||||
if (send == null)
|
||||
{
|
||||
throw new BadRequestException("All existing sends must be included in the rotation.");
|
||||
}
|
||||
|
||||
result.Add(send.ToSend(existing, _sendService));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
using Bit.Api.Auth.Models.Request.WebAuthn;
|
||||
using Bit.Core.Auth.Models.Data;
|
||||
using Bit.Core.Auth.Repositories;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Exceptions;
|
||||
|
||||
namespace Bit.Api.KeyManagement.Validators;
|
||||
|
||||
public class WebAuthnLoginKeyRotationValidator : IRotationValidator<IEnumerable<WebAuthnLoginRotateKeyRequestModel>, IEnumerable<WebAuthnLoginRotateKeyData>>
|
||||
{
|
||||
private readonly IWebAuthnCredentialRepository _webAuthnCredentialRepository;
|
||||
|
||||
public WebAuthnLoginKeyRotationValidator(IWebAuthnCredentialRepository webAuthnCredentialRepository)
|
||||
{
|
||||
_webAuthnCredentialRepository = webAuthnCredentialRepository;
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<WebAuthnLoginRotateKeyData>> ValidateAsync(User user, IEnumerable<WebAuthnLoginRotateKeyRequestModel> keysToRotate)
|
||||
{
|
||||
// 2024-06: Remove after 3 releases, for backward compatibility
|
||||
if (keysToRotate == null)
|
||||
{
|
||||
return new List<WebAuthnLoginRotateKeyData>();
|
||||
}
|
||||
|
||||
var result = new List<WebAuthnLoginRotateKeyData>();
|
||||
var existing = await _webAuthnCredentialRepository.GetManyByUserIdAsync(user.Id);
|
||||
if (existing == null || !existing.Any())
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
foreach (var ea in existing)
|
||||
{
|
||||
var keyToRotate = keysToRotate.FirstOrDefault(c => c.Id == ea.Id);
|
||||
if (keyToRotate == null)
|
||||
{
|
||||
throw new BadRequestException("All existing webauthn prf keys must be included in the rotation.");
|
||||
}
|
||||
|
||||
if (keyToRotate.EncryptedUserKey == null)
|
||||
{
|
||||
throw new BadRequestException("WebAuthn prf keys must have user-key during rotation.");
|
||||
}
|
||||
if (keyToRotate.EncryptedPublicKey == null)
|
||||
{
|
||||
throw new BadRequestException("WebAuthn prf keys must have public-key during rotation.");
|
||||
}
|
||||
|
||||
result.Add(keyToRotate.ToWebAuthnRotateKeyData());
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user