mirror of
https://github.com/bitwarden/server.git
synced 2025-02-24 03:11:22 +01:00
[PM-13362] Add private key regeneration endpoint (#4929)
* Add new RegenerateUserAsymmetricKeysCommand * add new command tests * Add regen controller * Add regen controller tests * add feature flag * Add push notification to sync new asymmetric keys to other devices
This commit is contained in:
parent
d88a103fbc
commit
7637cbe12a
src
test
Api.IntegrationTest
Api.Test/KeyManagement/Controllers
Core.Test/KeyManagement/Commands
@ -0,0 +1,50 @@
|
|||||||
|
#nullable enable
|
||||||
|
using Bit.Api.KeyManagement.Models.Requests;
|
||||||
|
using Bit.Core;
|
||||||
|
using Bit.Core.Exceptions;
|
||||||
|
using Bit.Core.KeyManagement.Commands.Interfaces;
|
||||||
|
using Bit.Core.Repositories;
|
||||||
|
using Bit.Core.Services;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace Bit.Api.KeyManagement.Controllers;
|
||||||
|
|
||||||
|
[Route("accounts/key-management")]
|
||||||
|
[Authorize("Application")]
|
||||||
|
public class AccountsKeyManagementController : Controller
|
||||||
|
{
|
||||||
|
private readonly IEmergencyAccessRepository _emergencyAccessRepository;
|
||||||
|
private readonly IFeatureService _featureService;
|
||||||
|
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||||
|
private readonly IRegenerateUserAsymmetricKeysCommand _regenerateUserAsymmetricKeysCommand;
|
||||||
|
private readonly IUserService _userService;
|
||||||
|
|
||||||
|
public AccountsKeyManagementController(IUserService userService,
|
||||||
|
IFeatureService featureService,
|
||||||
|
IOrganizationUserRepository organizationUserRepository,
|
||||||
|
IEmergencyAccessRepository emergencyAccessRepository,
|
||||||
|
IRegenerateUserAsymmetricKeysCommand regenerateUserAsymmetricKeysCommand)
|
||||||
|
{
|
||||||
|
_userService = userService;
|
||||||
|
_featureService = featureService;
|
||||||
|
_regenerateUserAsymmetricKeysCommand = regenerateUserAsymmetricKeysCommand;
|
||||||
|
_organizationUserRepository = organizationUserRepository;
|
||||||
|
_emergencyAccessRepository = emergencyAccessRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("regenerate-keys")]
|
||||||
|
public async Task RegenerateKeysAsync([FromBody] KeyRegenerationRequestModel request)
|
||||||
|
{
|
||||||
|
if (!_featureService.IsEnabled(FeatureFlagKeys.PrivateKeyRegeneration))
|
||||||
|
{
|
||||||
|
throw new NotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
var user = await _userService.GetUserByPrincipalAsync(User) ?? throw new UnauthorizedAccessException();
|
||||||
|
var usersOrganizationAccounts = await _organizationUserRepository.GetManyByUserAsync(user.Id);
|
||||||
|
var designatedEmergencyAccess = await _emergencyAccessRepository.GetManyDetailsByGranteeIdAsync(user.Id);
|
||||||
|
await _regenerateUserAsymmetricKeysCommand.RegenerateKeysAsync(request.ToUserAsymmetricKeys(user.Id),
|
||||||
|
usersOrganizationAccounts, designatedEmergencyAccess);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,23 @@
|
|||||||
|
#nullable enable
|
||||||
|
using Bit.Core.KeyManagement.Models.Data;
|
||||||
|
using Bit.Core.Utilities;
|
||||||
|
|
||||||
|
namespace Bit.Api.KeyManagement.Models.Requests;
|
||||||
|
|
||||||
|
public class KeyRegenerationRequestModel
|
||||||
|
{
|
||||||
|
public required string UserPublicKey { get; set; }
|
||||||
|
|
||||||
|
[EncryptedString]
|
||||||
|
public required string UserKeyEncryptedUserPrivateKey { get; set; }
|
||||||
|
|
||||||
|
public UserAsymmetricKeys ToUserAsymmetricKeys(Guid userId)
|
||||||
|
{
|
||||||
|
return new UserAsymmetricKeys
|
||||||
|
{
|
||||||
|
UserId = userId,
|
||||||
|
PublicKey = UserPublicKey,
|
||||||
|
UserKeyEncryptedPrivateKey = UserKeyEncryptedUserPrivateKey,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
@ -160,6 +160,7 @@ public static class FeatureFlagKeys
|
|||||||
public const string PM12443RemovePagingLogic = "pm-12443-remove-paging-logic";
|
public const string PM12443RemovePagingLogic = "pm-12443-remove-paging-logic";
|
||||||
public const string SelfHostLicenseRefactor = "pm-11516-self-host-license-refactor";
|
public const string SelfHostLicenseRefactor = "pm-11516-self-host-license-refactor";
|
||||||
public const string PromoteProviderServiceUserTool = "pm-15128-promote-provider-service-user-tool";
|
public const string PromoteProviderServiceUserTool = "pm-15128-promote-provider-service-user-tool";
|
||||||
|
public const string PrivateKeyRegeneration = "pm-12241-private-key-regeneration";
|
||||||
|
|
||||||
public static List<string> GetAllKeys()
|
public static List<string> GetAllKeys()
|
||||||
{
|
{
|
||||||
|
@ -0,0 +1,13 @@
|
|||||||
|
#nullable enable
|
||||||
|
using Bit.Core.Auth.Models.Data;
|
||||||
|
using Bit.Core.Entities;
|
||||||
|
using Bit.Core.KeyManagement.Models.Data;
|
||||||
|
|
||||||
|
namespace Bit.Core.KeyManagement.Commands.Interfaces;
|
||||||
|
|
||||||
|
public interface IRegenerateUserAsymmetricKeysCommand
|
||||||
|
{
|
||||||
|
Task RegenerateKeysAsync(UserAsymmetricKeys userAsymmetricKeys,
|
||||||
|
ICollection<OrganizationUser> usersOrganizationAccounts,
|
||||||
|
ICollection<EmergencyAccessDetails> designatedEmergencyAccess);
|
||||||
|
}
|
@ -0,0 +1,71 @@
|
|||||||
|
#nullable enable
|
||||||
|
using Bit.Core.Auth.Enums;
|
||||||
|
using Bit.Core.Auth.Models.Data;
|
||||||
|
using Bit.Core.Context;
|
||||||
|
using Bit.Core.Entities;
|
||||||
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Exceptions;
|
||||||
|
using Bit.Core.KeyManagement.Commands.Interfaces;
|
||||||
|
using Bit.Core.KeyManagement.Models.Data;
|
||||||
|
using Bit.Core.KeyManagement.Repositories;
|
||||||
|
using Bit.Core.Services;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace Bit.Core.KeyManagement.Commands;
|
||||||
|
|
||||||
|
public class RegenerateUserAsymmetricKeysCommand : IRegenerateUserAsymmetricKeysCommand
|
||||||
|
{
|
||||||
|
private readonly ICurrentContext _currentContext;
|
||||||
|
private readonly ILogger<RegenerateUserAsymmetricKeysCommand> _logger;
|
||||||
|
private readonly IUserAsymmetricKeysRepository _userAsymmetricKeysRepository;
|
||||||
|
private readonly IPushNotificationService _pushService;
|
||||||
|
|
||||||
|
public RegenerateUserAsymmetricKeysCommand(
|
||||||
|
ICurrentContext currentContext,
|
||||||
|
IUserAsymmetricKeysRepository userAsymmetricKeysRepository,
|
||||||
|
IPushNotificationService pushService,
|
||||||
|
ILogger<RegenerateUserAsymmetricKeysCommand> logger)
|
||||||
|
{
|
||||||
|
_currentContext = currentContext;
|
||||||
|
_logger = logger;
|
||||||
|
_userAsymmetricKeysRepository = userAsymmetricKeysRepository;
|
||||||
|
_pushService = pushService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task RegenerateKeysAsync(UserAsymmetricKeys userAsymmetricKeys,
|
||||||
|
ICollection<OrganizationUser> usersOrganizationAccounts,
|
||||||
|
ICollection<EmergencyAccessDetails> designatedEmergencyAccess)
|
||||||
|
{
|
||||||
|
var userId = _currentContext.UserId;
|
||||||
|
if (!userId.HasValue ||
|
||||||
|
userAsymmetricKeys.UserId != userId.Value ||
|
||||||
|
usersOrganizationAccounts.Any(ou => ou.UserId != userId) ||
|
||||||
|
designatedEmergencyAccess.Any(dea => dea.GranteeId != userId))
|
||||||
|
{
|
||||||
|
throw new NotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
var inOrganizations = usersOrganizationAccounts.Any(ou =>
|
||||||
|
ou.Status is OrganizationUserStatusType.Confirmed or OrganizationUserStatusType.Revoked);
|
||||||
|
var hasDesignatedEmergencyAccess = designatedEmergencyAccess.Any(x =>
|
||||||
|
x.Status is EmergencyAccessStatusType.Confirmed or EmergencyAccessStatusType.RecoveryApproved
|
||||||
|
or EmergencyAccessStatusType.RecoveryInitiated);
|
||||||
|
|
||||||
|
_logger.LogInformation(
|
||||||
|
"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)
|
||||||
|
{
|
||||||
|
throw new BadRequestException("Key regeneration not supported for this user.");
|
||||||
|
}
|
||||||
|
|
||||||
|
await _userAsymmetricKeysRepository.RegenerateUserAsymmetricKeysAsync(userAsymmetricKeys);
|
||||||
|
_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);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,18 @@
|
|||||||
|
using Bit.Core.KeyManagement.Commands;
|
||||||
|
using Bit.Core.KeyManagement.Commands.Interfaces;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
|
namespace Bit.Core.KeyManagement;
|
||||||
|
|
||||||
|
public static class KeyManagementServiceCollectionExtensions
|
||||||
|
{
|
||||||
|
public static void AddKeyManagementServices(this IServiceCollection services)
|
||||||
|
{
|
||||||
|
services.AddKeyManagementCommands();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AddKeyManagementCommands(this IServiceCollection services)
|
||||||
|
{
|
||||||
|
services.AddScoped<IRegenerateUserAsymmetricKeysCommand, RegenerateUserAsymmetricKeysCommand>();
|
||||||
|
}
|
||||||
|
}
|
@ -26,6 +26,7 @@ using Bit.Core.Enums;
|
|||||||
using Bit.Core.HostedServices;
|
using Bit.Core.HostedServices;
|
||||||
using Bit.Core.Identity;
|
using Bit.Core.Identity;
|
||||||
using Bit.Core.IdentityServer;
|
using Bit.Core.IdentityServer;
|
||||||
|
using Bit.Core.KeyManagement;
|
||||||
using Bit.Core.NotificationHub;
|
using Bit.Core.NotificationHub;
|
||||||
using Bit.Core.OrganizationFeatures;
|
using Bit.Core.OrganizationFeatures;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
@ -120,6 +121,7 @@ public static class ServiceCollectionExtensions
|
|||||||
services.AddScoped<IOrganizationDomainService, OrganizationDomainService>();
|
services.AddScoped<IOrganizationDomainService, OrganizationDomainService>();
|
||||||
services.AddVaultServices();
|
services.AddVaultServices();
|
||||||
services.AddReportingServices();
|
services.AddReportingServices();
|
||||||
|
services.AddKeyManagementServices();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void AddTokenizers(this IServiceCollection services)
|
public static void AddTokenizers(this IServiceCollection services)
|
||||||
|
@ -16,6 +16,12 @@ public class LoginHelper
|
|||||||
_client = client;
|
_client = client;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task LoginAsync(string email)
|
||||||
|
{
|
||||||
|
var tokens = await _factory.LoginAsync(email);
|
||||||
|
_client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", tokens.Token);
|
||||||
|
}
|
||||||
|
|
||||||
public async Task LoginWithOrganizationApiKeyAsync(Guid organizationId)
|
public async Task LoginWithOrganizationApiKeyAsync(Guid organizationId)
|
||||||
{
|
{
|
||||||
var (clientId, apiKey) = await GetOrganizationApiKey(_factory, organizationId);
|
var (clientId, apiKey) = await GetOrganizationApiKey(_factory, organizationId);
|
||||||
|
@ -0,0 +1,164 @@
|
|||||||
|
using System.Net;
|
||||||
|
using Bit.Api.IntegrationTest.Factories;
|
||||||
|
using Bit.Api.IntegrationTest.Helpers;
|
||||||
|
using Bit.Api.KeyManagement.Models.Requests;
|
||||||
|
using Bit.Core.Auth.Entities;
|
||||||
|
using Bit.Core.Auth.Enums;
|
||||||
|
using Bit.Core.Billing.Enums;
|
||||||
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Repositories;
|
||||||
|
using Bit.Test.Common.AutoFixture.Attributes;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Bit.Api.IntegrationTest.KeyManagement.Controllers;
|
||||||
|
|
||||||
|
public class AccountsKeyManagementControllerTests : IClassFixture<ApiApplicationFactory>, IAsyncLifetime
|
||||||
|
{
|
||||||
|
private static readonly string _mockEncryptedString =
|
||||||
|
"2.AOs41Hd8OQiCPXjyJKCiDA==|O6OHgt2U2hJGBSNGnimJmg==|iD33s8B69C8JhYYhSa4V1tArjvLr8eEaGqOV7BRo5Jk=";
|
||||||
|
|
||||||
|
private readonly HttpClient _client;
|
||||||
|
private readonly IEmergencyAccessRepository _emergencyAccessRepository;
|
||||||
|
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||||
|
private readonly ApiApplicationFactory _factory;
|
||||||
|
private readonly LoginHelper _loginHelper;
|
||||||
|
private readonly IUserRepository _userRepository;
|
||||||
|
private string _ownerEmail = null!;
|
||||||
|
|
||||||
|
public AccountsKeyManagementControllerTests(ApiApplicationFactory factory)
|
||||||
|
{
|
||||||
|
_factory = factory;
|
||||||
|
_factory.UpdateConfiguration("globalSettings:launchDarkly:flagValues:pm-12241-private-key-regeneration",
|
||||||
|
"true");
|
||||||
|
_client = factory.CreateClient();
|
||||||
|
_loginHelper = new LoginHelper(_factory, _client);
|
||||||
|
_userRepository = _factory.GetService<IUserRepository>();
|
||||||
|
_emergencyAccessRepository = _factory.GetService<IEmergencyAccessRepository>();
|
||||||
|
_organizationUserRepository = _factory.GetService<IOrganizationUserRepository>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task InitializeAsync()
|
||||||
|
{
|
||||||
|
_ownerEmail = $"integration-test{Guid.NewGuid()}@bitwarden.com";
|
||||||
|
await _factory.LoginWithNewAccount(_ownerEmail);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task DisposeAsync()
|
||||||
|
{
|
||||||
|
_client.Dispose();
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async Task RegenerateKeysAsync_FeatureFlagTurnedOff_NotFound(KeyRegenerationRequestModel request)
|
||||||
|
{
|
||||||
|
// Localize factory to inject a false value for the feature flag.
|
||||||
|
var localFactory = new ApiApplicationFactory();
|
||||||
|
localFactory.UpdateConfiguration("globalSettings:launchDarkly:flagValues:pm-12241-private-key-regeneration",
|
||||||
|
"false");
|
||||||
|
var localClient = localFactory.CreateClient();
|
||||||
|
var localEmail = $"integration-test{Guid.NewGuid()}@bitwarden.com";
|
||||||
|
var localLoginHelper = new LoginHelper(localFactory, localClient);
|
||||||
|
await localFactory.LoginWithNewAccount(localEmail);
|
||||||
|
await localLoginHelper.LoginAsync(localEmail);
|
||||||
|
|
||||||
|
request.UserKeyEncryptedUserPrivateKey = _mockEncryptedString;
|
||||||
|
|
||||||
|
var response = await localClient.PostAsJsonAsync("/accounts/key-management/regenerate-keys", request);
|
||||||
|
|
||||||
|
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async Task RegenerateKeysAsync_NotLoggedIn_Unauthorized(KeyRegenerationRequestModel request)
|
||||||
|
{
|
||||||
|
request.UserKeyEncryptedUserPrivateKey = _mockEncryptedString;
|
||||||
|
|
||||||
|
var response = await _client.PostAsJsonAsync("/accounts/key-management/regenerate-keys", request);
|
||||||
|
|
||||||
|
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData(OrganizationUserStatusType.Confirmed, EmergencyAccessStatusType.Confirmed)]
|
||||||
|
[BitAutoData(OrganizationUserStatusType.Confirmed, EmergencyAccessStatusType.RecoveryApproved)]
|
||||||
|
[BitAutoData(OrganizationUserStatusType.Confirmed, EmergencyAccessStatusType.RecoveryInitiated)]
|
||||||
|
[BitAutoData(OrganizationUserStatusType.Revoked, EmergencyAccessStatusType.Confirmed)]
|
||||||
|
[BitAutoData(OrganizationUserStatusType.Revoked, EmergencyAccessStatusType.RecoveryApproved)]
|
||||||
|
[BitAutoData(OrganizationUserStatusType.Revoked, EmergencyAccessStatusType.RecoveryInitiated)]
|
||||||
|
[BitAutoData(OrganizationUserStatusType.Confirmed, null)]
|
||||||
|
[BitAutoData(OrganizationUserStatusType.Revoked, null)]
|
||||||
|
[BitAutoData(OrganizationUserStatusType.Invited, EmergencyAccessStatusType.Confirmed)]
|
||||||
|
[BitAutoData(OrganizationUserStatusType.Invited, EmergencyAccessStatusType.RecoveryApproved)]
|
||||||
|
[BitAutoData(OrganizationUserStatusType.Invited, EmergencyAccessStatusType.RecoveryInitiated)]
|
||||||
|
public async Task RegenerateKeysAsync_UserInOrgOrHasDesignatedEmergencyAccess_ThrowsBadRequest(
|
||||||
|
OrganizationUserStatusType organizationUserStatus,
|
||||||
|
EmergencyAccessStatusType? emergencyAccessStatus,
|
||||||
|
KeyRegenerationRequestModel request)
|
||||||
|
{
|
||||||
|
if (organizationUserStatus is OrganizationUserStatusType.Confirmed or OrganizationUserStatusType.Revoked)
|
||||||
|
{
|
||||||
|
await CreateOrganizationUserAsync(organizationUserStatus);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (emergencyAccessStatus != null)
|
||||||
|
{
|
||||||
|
await CreateDesignatedEmergencyAccessAsync(emergencyAccessStatus.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
await _loginHelper.LoginAsync(_ownerEmail);
|
||||||
|
request.UserKeyEncryptedUserPrivateKey = _mockEncryptedString;
|
||||||
|
|
||||||
|
var response = await _client.PostAsJsonAsync("/accounts/key-management/regenerate-keys", request);
|
||||||
|
|
||||||
|
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async Task RegenerateKeysAsync_Success(KeyRegenerationRequestModel request)
|
||||||
|
{
|
||||||
|
await _loginHelper.LoginAsync(_ownerEmail);
|
||||||
|
request.UserKeyEncryptedUserPrivateKey = _mockEncryptedString;
|
||||||
|
|
||||||
|
var response = await _client.PostAsJsonAsync("/accounts/key-management/regenerate-keys", request);
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
|
||||||
|
var user = await _userRepository.GetByEmailAsync(_ownerEmail);
|
||||||
|
Assert.NotNull(user);
|
||||||
|
Assert.Equal(request.UserPublicKey, user.PublicKey);
|
||||||
|
Assert.Equal(request.UserKeyEncryptedUserPrivateKey, user.PrivateKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task CreateOrganizationUserAsync(OrganizationUserStatusType organizationUserStatus)
|
||||||
|
{
|
||||||
|
var (_, organizationUser) = await OrganizationTestHelpers.SignUpAsync(_factory,
|
||||||
|
PlanType.EnterpriseAnnually, _ownerEmail, passwordManagerSeats: 10,
|
||||||
|
paymentMethod: PaymentMethodType.Card);
|
||||||
|
organizationUser.Status = organizationUserStatus;
|
||||||
|
await _organizationUserRepository.ReplaceAsync(organizationUser);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task CreateDesignatedEmergencyAccessAsync(EmergencyAccessStatusType emergencyAccessStatus)
|
||||||
|
{
|
||||||
|
var tempEmail = $"integration-test{Guid.NewGuid()}@bitwarden.com";
|
||||||
|
await _factory.LoginWithNewAccount(tempEmail);
|
||||||
|
|
||||||
|
var tempUser = await _userRepository.GetByEmailAsync(tempEmail);
|
||||||
|
var user = await _userRepository.GetByEmailAsync(_ownerEmail);
|
||||||
|
var emergencyAccess = new EmergencyAccess
|
||||||
|
{
|
||||||
|
GrantorId = tempUser!.Id,
|
||||||
|
GranteeId = user!.Id,
|
||||||
|
KeyEncrypted = _mockEncryptedString,
|
||||||
|
Status = emergencyAccessStatus,
|
||||||
|
Type = EmergencyAccessType.View,
|
||||||
|
WaitTimeDays = 10,
|
||||||
|
CreationDate = DateTime.UtcNow,
|
||||||
|
RevisionDate = DateTime.UtcNow
|
||||||
|
};
|
||||||
|
await _emergencyAccessRepository.CreateAsync(emergencyAccess);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,96 @@
|
|||||||
|
#nullable enable
|
||||||
|
using System.Security.Claims;
|
||||||
|
using Bit.Api.KeyManagement.Controllers;
|
||||||
|
using Bit.Api.KeyManagement.Models.Requests;
|
||||||
|
using Bit.Core;
|
||||||
|
using Bit.Core.Auth.Models.Data;
|
||||||
|
using Bit.Core.Entities;
|
||||||
|
using Bit.Core.Exceptions;
|
||||||
|
using Bit.Core.KeyManagement.Commands.Interfaces;
|
||||||
|
using Bit.Core.KeyManagement.Models.Data;
|
||||||
|
using Bit.Core.Repositories;
|
||||||
|
using Bit.Core.Services;
|
||||||
|
using Bit.Test.Common.AutoFixture;
|
||||||
|
using Bit.Test.Common.AutoFixture.Attributes;
|
||||||
|
using NSubstitute;
|
||||||
|
using NSubstitute.ReturnsExtensions;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Bit.Api.Test.KeyManagement.Controllers;
|
||||||
|
|
||||||
|
[ControllerCustomize(typeof(AccountsKeyManagementController))]
|
||||||
|
[SutProviderCustomize]
|
||||||
|
[JsonDocumentCustomize]
|
||||||
|
public class AccountsKeyManagementControllerTests
|
||||||
|
{
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async Task RegenerateKeysAsync_FeatureFlagOff_Throws(
|
||||||
|
SutProvider<AccountsKeyManagementController> sutProvider,
|
||||||
|
KeyRegenerationRequestModel data)
|
||||||
|
{
|
||||||
|
sutProvider.GetDependency<IFeatureService>().IsEnabled(Arg.Is(FeatureFlagKeys.PrivateKeyRegeneration))
|
||||||
|
.Returns(false);
|
||||||
|
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).ReturnsNull();
|
||||||
|
|
||||||
|
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.RegenerateKeysAsync(data));
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<IOrganizationUserRepository>().ReceivedWithAnyArgs(0)
|
||||||
|
.GetManyByUserAsync(Arg.Any<Guid>());
|
||||||
|
await sutProvider.GetDependency<IEmergencyAccessRepository>().ReceivedWithAnyArgs(0)
|
||||||
|
.GetManyDetailsByGranteeIdAsync(Arg.Any<Guid>());
|
||||||
|
await sutProvider.GetDependency<IRegenerateUserAsymmetricKeysCommand>().ReceivedWithAnyArgs(0)
|
||||||
|
.RegenerateKeysAsync(Arg.Any<UserAsymmetricKeys>(),
|
||||||
|
Arg.Any<ICollection<OrganizationUser>>(),
|
||||||
|
Arg.Any<ICollection<EmergencyAccessDetails>>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async Task RegenerateKeysAsync_UserNull_Throws(SutProvider<AccountsKeyManagementController> sutProvider,
|
||||||
|
KeyRegenerationRequestModel data)
|
||||||
|
{
|
||||||
|
sutProvider.GetDependency<IFeatureService>().IsEnabled(Arg.Is(FeatureFlagKeys.PrivateKeyRegeneration))
|
||||||
|
.Returns(true);
|
||||||
|
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).ReturnsNull();
|
||||||
|
|
||||||
|
await Assert.ThrowsAsync<UnauthorizedAccessException>(() => sutProvider.Sut.RegenerateKeysAsync(data));
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<IOrganizationUserRepository>().ReceivedWithAnyArgs(0)
|
||||||
|
.GetManyByUserAsync(Arg.Any<Guid>());
|
||||||
|
await sutProvider.GetDependency<IEmergencyAccessRepository>().ReceivedWithAnyArgs(0)
|
||||||
|
.GetManyDetailsByGranteeIdAsync(Arg.Any<Guid>());
|
||||||
|
await sutProvider.GetDependency<IRegenerateUserAsymmetricKeysCommand>().ReceivedWithAnyArgs(0)
|
||||||
|
.RegenerateKeysAsync(Arg.Any<UserAsymmetricKeys>(),
|
||||||
|
Arg.Any<ICollection<OrganizationUser>>(),
|
||||||
|
Arg.Any<ICollection<EmergencyAccessDetails>>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async Task RegenerateKeysAsync_Success(SutProvider<AccountsKeyManagementController> sutProvider,
|
||||||
|
KeyRegenerationRequestModel data, User user, ICollection<OrganizationUser> orgUsers,
|
||||||
|
ICollection<EmergencyAccessDetails> accessDetails)
|
||||||
|
{
|
||||||
|
sutProvider.GetDependency<IFeatureService>().IsEnabled(Arg.Is(FeatureFlagKeys.PrivateKeyRegeneration))
|
||||||
|
.Returns(true);
|
||||||
|
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user);
|
||||||
|
sutProvider.GetDependency<IOrganizationUserRepository>().GetManyByUserAsync(Arg.Is(user.Id)).Returns(orgUsers);
|
||||||
|
sutProvider.GetDependency<IEmergencyAccessRepository>().GetManyDetailsByGranteeIdAsync(Arg.Is(user.Id))
|
||||||
|
.Returns(accessDetails);
|
||||||
|
|
||||||
|
await sutProvider.Sut.RegenerateKeysAsync(data);
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<IOrganizationUserRepository>().Received(1)
|
||||||
|
.GetManyByUserAsync(Arg.Is(user.Id));
|
||||||
|
await sutProvider.GetDependency<IEmergencyAccessRepository>().Received(1)
|
||||||
|
.GetManyDetailsByGranteeIdAsync(Arg.Is(user.Id));
|
||||||
|
await sutProvider.GetDependency<IRegenerateUserAsymmetricKeysCommand>().Received(1)
|
||||||
|
.RegenerateKeysAsync(
|
||||||
|
Arg.Is<UserAsymmetricKeys>(u =>
|
||||||
|
u.UserId == user.Id && u.PublicKey == data.UserPublicKey &&
|
||||||
|
u.UserKeyEncryptedPrivateKey == data.UserKeyEncryptedUserPrivateKey),
|
||||||
|
Arg.Is(orgUsers),
|
||||||
|
Arg.Is(accessDetails));
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,197 @@
|
|||||||
|
#nullable enable
|
||||||
|
using Bit.Core.Auth.Enums;
|
||||||
|
using Bit.Core.Auth.Models.Data;
|
||||||
|
using Bit.Core.Context;
|
||||||
|
using Bit.Core.Entities;
|
||||||
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Exceptions;
|
||||||
|
using Bit.Core.KeyManagement.Commands;
|
||||||
|
using Bit.Core.KeyManagement.Models.Data;
|
||||||
|
using Bit.Core.KeyManagement.Repositories;
|
||||||
|
using Bit.Core.Services;
|
||||||
|
using Bit.Test.Common.AutoFixture;
|
||||||
|
using Bit.Test.Common.AutoFixture.Attributes;
|
||||||
|
using NSubstitute;
|
||||||
|
using NSubstitute.ReturnsExtensions;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Bit.Core.Test.KeyManagement.Commands;
|
||||||
|
|
||||||
|
[SutProviderCustomize]
|
||||||
|
public class RegenerateUserAsymmetricKeysCommandTests
|
||||||
|
{
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async Task RegenerateKeysAsync_NoCurrentContext_NotFoundException(
|
||||||
|
SutProvider<RegenerateUserAsymmetricKeysCommand> sutProvider,
|
||||||
|
UserAsymmetricKeys userAsymmetricKeys)
|
||||||
|
{
|
||||||
|
sutProvider.GetDependency<ICurrentContext>().UserId.ReturnsNullForAnyArgs();
|
||||||
|
var usersOrganizationAccounts = new List<OrganizationUser>();
|
||||||
|
var designatedEmergencyAccess = new List<EmergencyAccessDetails>();
|
||||||
|
|
||||||
|
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.RegenerateKeysAsync(userAsymmetricKeys,
|
||||||
|
usersOrganizationAccounts, designatedEmergencyAccess));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async Task RegenerateKeysAsync_UserHasNoSharedAccess_Success(
|
||||||
|
SutProvider<RegenerateUserAsymmetricKeysCommand> sutProvider,
|
||||||
|
UserAsymmetricKeys userAsymmetricKeys)
|
||||||
|
{
|
||||||
|
sutProvider.GetDependency<ICurrentContext>().UserId.ReturnsForAnyArgs(userAsymmetricKeys.UserId);
|
||||||
|
var usersOrganizationAccounts = new List<OrganizationUser>();
|
||||||
|
var designatedEmergencyAccess = new List<EmergencyAccessDetails>();
|
||||||
|
|
||||||
|
await sutProvider.Sut.RegenerateKeysAsync(userAsymmetricKeys,
|
||||||
|
usersOrganizationAccounts, designatedEmergencyAccess);
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<IUserAsymmetricKeysRepository>()
|
||||||
|
.Received(1)
|
||||||
|
.RegenerateUserAsymmetricKeysAsync(Arg.Is(userAsymmetricKeys));
|
||||||
|
await sutProvider.GetDependency<IPushNotificationService>()
|
||||||
|
.Received(1)
|
||||||
|
.PushSyncSettingsAsync(Arg.Is(userAsymmetricKeys.UserId));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData(false, false, true)]
|
||||||
|
[BitAutoData(false, true, false)]
|
||||||
|
[BitAutoData(false, true, true)]
|
||||||
|
[BitAutoData(true, false, false)]
|
||||||
|
[BitAutoData(true, false, true)]
|
||||||
|
[BitAutoData(true, true, false)]
|
||||||
|
[BitAutoData(true, true, true)]
|
||||||
|
public async Task RegenerateKeysAsync_UserIdMisMatch_NotFoundException(
|
||||||
|
bool userAsymmetricKeysMismatch,
|
||||||
|
bool orgMismatch,
|
||||||
|
bool emergencyAccessMismatch,
|
||||||
|
SutProvider<RegenerateUserAsymmetricKeysCommand> sutProvider,
|
||||||
|
UserAsymmetricKeys userAsymmetricKeys,
|
||||||
|
ICollection<OrganizationUser> usersOrganizationAccounts,
|
||||||
|
ICollection<EmergencyAccessDetails> designatedEmergencyAccess)
|
||||||
|
{
|
||||||
|
sutProvider.GetDependency<ICurrentContext>().UserId
|
||||||
|
.ReturnsForAnyArgs(userAsymmetricKeysMismatch ? new Guid() : userAsymmetricKeys.UserId);
|
||||||
|
|
||||||
|
if (!orgMismatch)
|
||||||
|
{
|
||||||
|
usersOrganizationAccounts =
|
||||||
|
SetupOrganizationUserAccounts(userAsymmetricKeys.UserId, usersOrganizationAccounts);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!emergencyAccessMismatch)
|
||||||
|
{
|
||||||
|
designatedEmergencyAccess = SetupEmergencyAccess(userAsymmetricKeys.UserId, designatedEmergencyAccess);
|
||||||
|
}
|
||||||
|
|
||||||
|
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.RegenerateKeysAsync(userAsymmetricKeys,
|
||||||
|
usersOrganizationAccounts, designatedEmergencyAccess));
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<IUserAsymmetricKeysRepository>()
|
||||||
|
.ReceivedWithAnyArgs(0)
|
||||||
|
.RegenerateUserAsymmetricKeysAsync(Arg.Any<UserAsymmetricKeys>());
|
||||||
|
await sutProvider.GetDependency<IPushNotificationService>()
|
||||||
|
.ReceivedWithAnyArgs(0)
|
||||||
|
.PushSyncSettingsAsync(Arg.Any<Guid>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData(OrganizationUserStatusType.Confirmed)]
|
||||||
|
[BitAutoData(OrganizationUserStatusType.Revoked)]
|
||||||
|
public async Task RegenerateKeysAsync_UserInOrganizations_BadRequestException(
|
||||||
|
OrganizationUserStatusType organizationUserStatus,
|
||||||
|
SutProvider<RegenerateUserAsymmetricKeysCommand> sutProvider,
|
||||||
|
UserAsymmetricKeys userAsymmetricKeys,
|
||||||
|
ICollection<OrganizationUser> usersOrganizationAccounts)
|
||||||
|
{
|
||||||
|
sutProvider.GetDependency<ICurrentContext>().UserId.ReturnsForAnyArgs(userAsymmetricKeys.UserId);
|
||||||
|
usersOrganizationAccounts = CreateInOrganizationAccounts(userAsymmetricKeys.UserId, organizationUserStatus,
|
||||||
|
usersOrganizationAccounts);
|
||||||
|
var designatedEmergencyAccess = new List<EmergencyAccessDetails>();
|
||||||
|
|
||||||
|
await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.RegenerateKeysAsync(userAsymmetricKeys,
|
||||||
|
usersOrganizationAccounts, designatedEmergencyAccess));
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<IUserAsymmetricKeysRepository>()
|
||||||
|
.ReceivedWithAnyArgs(0)
|
||||||
|
.RegenerateUserAsymmetricKeysAsync(Arg.Any<UserAsymmetricKeys>());
|
||||||
|
await sutProvider.GetDependency<IPushNotificationService>()
|
||||||
|
.ReceivedWithAnyArgs(0)
|
||||||
|
.PushSyncSettingsAsync(Arg.Any<Guid>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData(EmergencyAccessStatusType.Confirmed)]
|
||||||
|
[BitAutoData(EmergencyAccessStatusType.RecoveryApproved)]
|
||||||
|
[BitAutoData(EmergencyAccessStatusType.RecoveryInitiated)]
|
||||||
|
public async Task RegenerateKeysAsync_UserHasDesignatedEmergencyAccess_BadRequestException(
|
||||||
|
EmergencyAccessStatusType statusType,
|
||||||
|
SutProvider<RegenerateUserAsymmetricKeysCommand> sutProvider,
|
||||||
|
UserAsymmetricKeys userAsymmetricKeys,
|
||||||
|
ICollection<EmergencyAccessDetails> designatedEmergencyAccess)
|
||||||
|
{
|
||||||
|
sutProvider.GetDependency<ICurrentContext>().UserId.ReturnsForAnyArgs(userAsymmetricKeys.UserId);
|
||||||
|
designatedEmergencyAccess =
|
||||||
|
CreateDesignatedEmergencyAccess(userAsymmetricKeys.UserId, statusType, designatedEmergencyAccess);
|
||||||
|
var usersOrganizationAccounts = new List<OrganizationUser>();
|
||||||
|
|
||||||
|
|
||||||
|
await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.RegenerateKeysAsync(userAsymmetricKeys,
|
||||||
|
usersOrganizationAccounts, designatedEmergencyAccess));
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<IUserAsymmetricKeysRepository>()
|
||||||
|
.ReceivedWithAnyArgs(0)
|
||||||
|
.RegenerateUserAsymmetricKeysAsync(Arg.Any<UserAsymmetricKeys>());
|
||||||
|
await sutProvider.GetDependency<IPushNotificationService>()
|
||||||
|
.ReceivedWithAnyArgs(0)
|
||||||
|
.PushSyncSettingsAsync(Arg.Any<Guid>());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ICollection<OrganizationUser> CreateInOrganizationAccounts(Guid userId,
|
||||||
|
OrganizationUserStatusType organizationUserStatus, ICollection<OrganizationUser> organizationUserAccounts)
|
||||||
|
{
|
||||||
|
foreach (var organizationUserAccount in organizationUserAccounts)
|
||||||
|
{
|
||||||
|
organizationUserAccount.UserId = userId;
|
||||||
|
organizationUserAccount.Status = organizationUserStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
return organizationUserAccounts;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ICollection<EmergencyAccessDetails> CreateDesignatedEmergencyAccess(Guid userId,
|
||||||
|
EmergencyAccessStatusType status, ICollection<EmergencyAccessDetails> designatedEmergencyAccess)
|
||||||
|
{
|
||||||
|
foreach (var designated in designatedEmergencyAccess)
|
||||||
|
{
|
||||||
|
designated.GranteeId = userId;
|
||||||
|
designated.Status = status;
|
||||||
|
}
|
||||||
|
|
||||||
|
return designatedEmergencyAccess;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ICollection<OrganizationUser> SetupOrganizationUserAccounts(Guid userId,
|
||||||
|
ICollection<OrganizationUser> organizationUserAccounts)
|
||||||
|
{
|
||||||
|
foreach (var organizationUserAccount in organizationUserAccounts)
|
||||||
|
{
|
||||||
|
organizationUserAccount.UserId = userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
return organizationUserAccounts;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ICollection<EmergencyAccessDetails> SetupEmergencyAccess(Guid userId,
|
||||||
|
ICollection<EmergencyAccessDetails> emergencyAccessDetails)
|
||||||
|
{
|
||||||
|
foreach (var emergencyAccessDetail in emergencyAccessDetails)
|
||||||
|
{
|
||||||
|
emergencyAccessDetail.GranteeId = userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
return emergencyAccessDetails;
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user