mirror of
https://github.com/bitwarden/server
synced 2025-12-30 23:23:37 +00:00
[PM-27280] Support v2 encryption on key-connector signups (#6712)
* account v2 registration for key connector * use new user repository functions * test coverage * integration test coverage * documentation * code review * missing test coverage * fix failing test * failing test * incorrect ticket number * moved back request model to Api, created dedicated data class in Core * sql stored procedure type mismatch, simplification * key connector authorization handler
This commit is contained in:
@@ -47,6 +47,7 @@ public class AccountsKeyManagementController : Controller
|
||||
_webauthnKeyValidator;
|
||||
private readonly IRotationValidator<IEnumerable<OtherDeviceKeysUpdateRequestModel>, IEnumerable<Device>> _deviceValidator;
|
||||
private readonly IKeyConnectorConfirmationDetailsQuery _keyConnectorConfirmationDetailsQuery;
|
||||
private readonly ISetKeyConnectorKeyCommand _setKeyConnectorKeyCommand;
|
||||
|
||||
public AccountsKeyManagementController(IUserService userService,
|
||||
IFeatureService featureService,
|
||||
@@ -62,8 +63,10 @@ public class AccountsKeyManagementController : Controller
|
||||
emergencyAccessValidator,
|
||||
IRotationValidator<IEnumerable<ResetPasswordWithOrgIdRequestModel>, IReadOnlyList<OrganizationUser>>
|
||||
organizationUserValidator,
|
||||
IRotationValidator<IEnumerable<WebAuthnLoginRotateKeyRequestModel>, IEnumerable<WebAuthnLoginRotateKeyData>> webAuthnKeyValidator,
|
||||
IRotationValidator<IEnumerable<OtherDeviceKeysUpdateRequestModel>, IEnumerable<Device>> deviceValidator)
|
||||
IRotationValidator<IEnumerable<WebAuthnLoginRotateKeyRequestModel>, IEnumerable<WebAuthnLoginRotateKeyData>>
|
||||
webAuthnKeyValidator,
|
||||
IRotationValidator<IEnumerable<OtherDeviceKeysUpdateRequestModel>, IEnumerable<Device>> deviceValidator,
|
||||
ISetKeyConnectorKeyCommand setKeyConnectorKeyCommand)
|
||||
{
|
||||
_userService = userService;
|
||||
_featureService = featureService;
|
||||
@@ -79,6 +82,7 @@ public class AccountsKeyManagementController : Controller
|
||||
_webauthnKeyValidator = webAuthnKeyValidator;
|
||||
_deviceValidator = deviceValidator;
|
||||
_keyConnectorConfirmationDetailsQuery = keyConnectorConfirmationDetailsQuery;
|
||||
_setKeyConnectorKeyCommand = setKeyConnectorKeyCommand;
|
||||
}
|
||||
|
||||
[HttpPost("key-management/regenerate-keys")]
|
||||
@@ -146,18 +150,28 @@ public class AccountsKeyManagementController : Controller
|
||||
throw new UnauthorizedAccessException();
|
||||
}
|
||||
|
||||
var result = await _userService.SetKeyConnectorKeyAsync(model.ToUser(user), model.Key, model.OrgIdentifier);
|
||||
if (result.Succeeded)
|
||||
if (model.IsV2Request())
|
||||
{
|
||||
return;
|
||||
// V2 account registration
|
||||
await _setKeyConnectorKeyCommand.SetKeyConnectorKeyForUserAsync(user, model.ToKeyConnectorKeysData());
|
||||
}
|
||||
|
||||
foreach (var error in result.Errors)
|
||||
else
|
||||
{
|
||||
ModelState.AddModelError(string.Empty, error.Description);
|
||||
}
|
||||
// V1 account registration
|
||||
// TODO removed with https://bitwarden.atlassian.net/browse/PM-27328
|
||||
var result = await _userService.SetKeyConnectorKeyAsync(model.ToUser(user), model.Key, model.OrgIdentifier);
|
||||
if (result.Succeeded)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
throw new BadRequestException(ModelState);
|
||||
foreach (var error in result.Errors)
|
||||
{
|
||||
ModelState.AddModelError(string.Empty, error.Description);
|
||||
}
|
||||
|
||||
throw new BadRequestException(ModelState);
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost("convert-to-key-connector")]
|
||||
|
||||
@@ -1,36 +1,112 @@
|
||||
// FIXME: Update this file to be null safe and then delete the line below
|
||||
#nullable disable
|
||||
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Bit.Core.Auth.Models.Api.Request.Accounts;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.KeyManagement.Models.Api.Request;
|
||||
using Bit.Core.KeyManagement.Models.Data;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
namespace Bit.Api.KeyManagement.Models.Requests;
|
||||
|
||||
public class SetKeyConnectorKeyRequestModel
|
||||
public class SetKeyConnectorKeyRequestModel : IValidatableObject
|
||||
{
|
||||
[Required]
|
||||
public string Key { get; set; }
|
||||
[Required]
|
||||
public KeysRequestModel Keys { get; set; }
|
||||
[Required]
|
||||
public KdfType Kdf { get; set; }
|
||||
[Required]
|
||||
public int KdfIterations { get; set; }
|
||||
public int? KdfMemory { get; set; }
|
||||
public int? KdfParallelism { get; set; }
|
||||
[Required]
|
||||
public string OrgIdentifier { get; set; }
|
||||
// TODO will be removed with https://bitwarden.atlassian.net/browse/PM-27328
|
||||
[Obsolete("Use KeyConnectorKeyWrappedUserKey instead")]
|
||||
public string? Key { get; set; }
|
||||
|
||||
[Obsolete("Use AccountKeys instead")]
|
||||
public KeysRequestModel? Keys { get; set; }
|
||||
[Obsolete("Not used anymore")]
|
||||
public KdfType? Kdf { get; set; }
|
||||
[Obsolete("Not used anymore")]
|
||||
public int? KdfIterations { get; set; }
|
||||
[Obsolete("Not used anymore")]
|
||||
public int? KdfMemory { get; set; }
|
||||
[Obsolete("Not used anymore")]
|
||||
public int? KdfParallelism { get; set; }
|
||||
|
||||
[EncryptedString]
|
||||
public string? KeyConnectorKeyWrappedUserKey { get; set; }
|
||||
public AccountKeysRequestModel? AccountKeys { get; set; }
|
||||
|
||||
[Required]
|
||||
public required string OrgIdentifier { get; init; }
|
||||
|
||||
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
|
||||
{
|
||||
if (IsV2Request())
|
||||
{
|
||||
// V2 registration
|
||||
yield break;
|
||||
}
|
||||
|
||||
// V1 registration
|
||||
// TODO removed with https://bitwarden.atlassian.net/browse/PM-27328
|
||||
if (string.IsNullOrEmpty(Key))
|
||||
{
|
||||
yield return new ValidationResult("Key must be supplied.");
|
||||
}
|
||||
|
||||
if (Keys == null)
|
||||
{
|
||||
yield return new ValidationResult("Keys must be supplied.");
|
||||
}
|
||||
|
||||
if (Kdf == null)
|
||||
{
|
||||
yield return new ValidationResult("Kdf must be supplied.");
|
||||
}
|
||||
|
||||
if (KdfIterations == null)
|
||||
{
|
||||
yield return new ValidationResult("KdfIterations must be supplied.");
|
||||
}
|
||||
|
||||
if (Kdf == KdfType.Argon2id)
|
||||
{
|
||||
if (KdfMemory == null)
|
||||
{
|
||||
yield return new ValidationResult("KdfMemory must be supplied when Kdf is Argon2id.");
|
||||
}
|
||||
|
||||
if (KdfParallelism == null)
|
||||
{
|
||||
yield return new ValidationResult("KdfParallelism must be supplied when Kdf is Argon2id.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsV2Request()
|
||||
{
|
||||
return !string.IsNullOrEmpty(KeyConnectorKeyWrappedUserKey) && AccountKeys != null;
|
||||
}
|
||||
|
||||
// TODO removed with https://bitwarden.atlassian.net/browse/PM-27328
|
||||
public User ToUser(User existingUser)
|
||||
{
|
||||
existingUser.Kdf = Kdf;
|
||||
existingUser.KdfIterations = KdfIterations;
|
||||
existingUser.Kdf = Kdf!.Value;
|
||||
existingUser.KdfIterations = KdfIterations!.Value;
|
||||
existingUser.KdfMemory = KdfMemory;
|
||||
existingUser.KdfParallelism = KdfParallelism;
|
||||
existingUser.Key = Key;
|
||||
Keys.ToUser(existingUser);
|
||||
Keys!.ToUser(existingUser);
|
||||
return existingUser;
|
||||
}
|
||||
|
||||
public KeyConnectorKeysData ToKeyConnectorKeysData()
|
||||
{
|
||||
// TODO remove validation with https://bitwarden.atlassian.net/browse/PM-27328
|
||||
if (string.IsNullOrEmpty(KeyConnectorKeyWrappedUserKey) || AccountKeys == null)
|
||||
{
|
||||
throw new BadRequestException("KeyConnectorKeyWrappedUserKey and AccountKeys must be supplied.");
|
||||
}
|
||||
|
||||
return new KeyConnectorKeysData
|
||||
{
|
||||
KeyConnectorKeyWrappedUserKey = KeyConnectorKeyWrappedUserKey,
|
||||
AccountKeys = AccountKeys,
|
||||
OrgIdentifier = OrgIdentifier
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -212,6 +212,7 @@ public static class FeatureFlagKeys
|
||||
public const string ConsolidatedSessionTimeoutComponent = "pm-26056-consolidated-session-timeout-component";
|
||||
public const string V2RegistrationTDEJIT = "pm-27279-v2-registration-tde-jit";
|
||||
public const string DataRecoveryTool = "pm-28813-data-recovery-tool";
|
||||
public const string EnableAccountEncryptionV2KeyConnectorRegistration = "enable-account-encryption-v2-key-connector-registration";
|
||||
|
||||
/* Mobile Team */
|
||||
public const string AndroidImportLoginsFlow = "import-logins-flow";
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
|
||||
namespace Bit.Core.KeyManagement.Authorization;
|
||||
|
||||
public class KeyConnectorAuthorizationHandler : AuthorizationHandler<KeyConnectorOperationsRequirement, User>
|
||||
{
|
||||
private readonly ICurrentContext _currentContext;
|
||||
|
||||
public KeyConnectorAuthorizationHandler(ICurrentContext currentContext)
|
||||
{
|
||||
_currentContext = currentContext;
|
||||
}
|
||||
|
||||
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context,
|
||||
KeyConnectorOperationsRequirement requirement,
|
||||
User user)
|
||||
{
|
||||
var authorized = requirement switch
|
||||
{
|
||||
not null when requirement == KeyConnectorOperations.Use => CanUse(user),
|
||||
_ => throw new ArgumentException("Unsupported operation requirement type provided.", nameof(requirement))
|
||||
};
|
||||
|
||||
if (authorized)
|
||||
{
|
||||
context.Succeed(requirement);
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private bool CanUse(User user)
|
||||
{
|
||||
// User cannot use Key Connector if they already use it
|
||||
if (user.UsesKeyConnector)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// User cannot use Key Connector if they are an owner or admin of any organization
|
||||
if (_currentContext.Organizations.Any(u =>
|
||||
u.Type is OrganizationUserType.Owner or OrganizationUserType.Admin))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
using Microsoft.AspNetCore.Authorization.Infrastructure;
|
||||
|
||||
namespace Bit.Core.KeyManagement.Authorization;
|
||||
|
||||
public class KeyConnectorOperationsRequirement : OperationAuthorizationRequirement
|
||||
{
|
||||
public KeyConnectorOperationsRequirement(string name)
|
||||
{
|
||||
Name = name;
|
||||
}
|
||||
}
|
||||
|
||||
public static class KeyConnectorOperations
|
||||
{
|
||||
public static readonly KeyConnectorOperationsRequirement Use = new(nameof(Use));
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.KeyManagement.Models.Data;
|
||||
|
||||
namespace Bit.Core.KeyManagement.Commands.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// Creates the user key and account cryptographic state for a new user registering
|
||||
/// with Key Connector SSO configuration.
|
||||
/// </summary>
|
||||
public interface ISetKeyConnectorKeyCommand
|
||||
{
|
||||
Task SetKeyConnectorKeyForUserAsync(User user, KeyConnectorKeysData keyConnectorKeysData);
|
||||
}
|
||||
60
src/Core/KeyManagement/Commands/SetKeyConnectorKeyCommand.cs
Normal file
60
src/Core/KeyManagement/Commands/SetKeyConnectorKeyCommand.cs
Normal file
@@ -0,0 +1,60 @@
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.KeyManagement.Authorization;
|
||||
using Bit.Core.KeyManagement.Commands.Interfaces;
|
||||
using Bit.Core.KeyManagement.Models.Data;
|
||||
using Bit.Core.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
|
||||
namespace Bit.Core.KeyManagement.Commands;
|
||||
|
||||
public class SetKeyConnectorKeyCommand : ISetKeyConnectorKeyCommand
|
||||
{
|
||||
private readonly IAuthorizationService _authorizationService;
|
||||
private readonly ICurrentContext _currentContext;
|
||||
private readonly IEventService _eventService;
|
||||
private readonly IAcceptOrgUserCommand _acceptOrgUserCommand;
|
||||
private readonly IUserService _userService;
|
||||
private readonly IUserRepository _userRepository;
|
||||
|
||||
public SetKeyConnectorKeyCommand(
|
||||
IAuthorizationService authorizationService,
|
||||
ICurrentContext currentContext,
|
||||
IEventService eventService,
|
||||
IAcceptOrgUserCommand acceptOrgUserCommand,
|
||||
IUserService userService,
|
||||
IUserRepository userRepository)
|
||||
{
|
||||
_authorizationService = authorizationService;
|
||||
_currentContext = currentContext;
|
||||
_eventService = eventService;
|
||||
_acceptOrgUserCommand = acceptOrgUserCommand;
|
||||
_userService = userService;
|
||||
_userRepository = userRepository;
|
||||
}
|
||||
|
||||
public async Task SetKeyConnectorKeyForUserAsync(User user, KeyConnectorKeysData keyConnectorKeysData)
|
||||
{
|
||||
var authorizationResult = await _authorizationService.AuthorizeAsync(_currentContext.HttpContext.User, user,
|
||||
KeyConnectorOperations.Use);
|
||||
if (!authorizationResult.Succeeded)
|
||||
{
|
||||
throw new BadRequestException("Cannot use Key Connector");
|
||||
}
|
||||
|
||||
var setKeyConnectorUserKeyTask =
|
||||
_userRepository.SetKeyConnectorUserKey(user.Id, keyConnectorKeysData.KeyConnectorKeyWrappedUserKey);
|
||||
|
||||
await _userRepository.SetV2AccountCryptographicStateAsync(user.Id,
|
||||
keyConnectorKeysData.AccountKeys.ToAccountKeysData(), [setKeyConnectorUserKeyTask]);
|
||||
|
||||
await _eventService.LogUserEventAsync(user.Id, EventType.User_MigratedKeyToKeyConnector);
|
||||
|
||||
await _acceptOrgUserCommand.AcceptOrgUserByOrgSsoIdAsync(keyConnectorKeysData.OrgIdentifier, user,
|
||||
_userService);
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,11 @@
|
||||
using Bit.Core.KeyManagement.Commands;
|
||||
using Bit.Core.KeyManagement.Authorization;
|
||||
using Bit.Core.KeyManagement.Commands;
|
||||
using Bit.Core.KeyManagement.Commands.Interfaces;
|
||||
using Bit.Core.KeyManagement.Kdf;
|
||||
using Bit.Core.KeyManagement.Kdf.Implementations;
|
||||
using Bit.Core.KeyManagement.Queries;
|
||||
using Bit.Core.KeyManagement.Queries.Interfaces;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Bit.Core.KeyManagement;
|
||||
@@ -12,15 +14,22 @@ public static class KeyManagementServiceCollectionExtensions
|
||||
{
|
||||
public static void AddKeyManagementServices(this IServiceCollection services)
|
||||
{
|
||||
services.AddKeyManagementAuthorizationHandlers();
|
||||
services.AddKeyManagementCommands();
|
||||
services.AddKeyManagementQueries();
|
||||
services.AddSendPasswordServices();
|
||||
}
|
||||
|
||||
private static void AddKeyManagementAuthorizationHandlers(this IServiceCollection services)
|
||||
{
|
||||
services.AddScoped<IAuthorizationHandler, KeyConnectorAuthorizationHandler>();
|
||||
}
|
||||
|
||||
private static void AddKeyManagementCommands(this IServiceCollection services)
|
||||
{
|
||||
services.AddScoped<IRegenerateUserAsymmetricKeysCommand, RegenerateUserAsymmetricKeysCommand>();
|
||||
services.AddScoped<IChangeKdfCommand, ChangeKdfCommand>();
|
||||
services.AddScoped<ISetKeyConnectorKeyCommand, SetKeyConnectorKeyCommand>();
|
||||
}
|
||||
|
||||
private static void AddKeyManagementQueries(this IServiceCollection services)
|
||||
|
||||
12
src/Core/KeyManagement/Models/Data/KeyConnectorKeysData.cs
Normal file
12
src/Core/KeyManagement/Models/Data/KeyConnectorKeysData.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using Bit.Core.KeyManagement.Models.Api.Request;
|
||||
|
||||
namespace Bit.Core.KeyManagement.Models.Data;
|
||||
|
||||
public class KeyConnectorKeysData
|
||||
{
|
||||
public required string KeyConnectorKeyWrappedUserKey { get; set; }
|
||||
|
||||
public required AccountKeysRequestModel AccountKeys { get; set; }
|
||||
|
||||
public required string OrgIdentifier { get; init; }
|
||||
}
|
||||
@@ -72,6 +72,8 @@ public interface IUserRepository : IRepository<User, Guid>
|
||||
UserAccountKeysData accountKeysData,
|
||||
IEnumerable<UpdateUserData>? updateUserDataActions = null);
|
||||
Task DeleteManyAsync(IEnumerable<User> users);
|
||||
|
||||
UpdateUserData SetKeyConnectorUserKey(Guid userId, string keyConnectorWrappedUserKey);
|
||||
}
|
||||
|
||||
public delegate Task UpdateUserData(Microsoft.Data.SqlClient.SqlConnection? connection = null,
|
||||
|
||||
@@ -33,6 +33,8 @@ public interface IUserService
|
||||
Task<IdentityResult> ChangeEmailAsync(User user, string masterPassword, string newEmail, string newMasterPassword,
|
||||
string token, string key);
|
||||
Task<IdentityResult> ChangePasswordAsync(User user, string masterPassword, string newMasterPassword, string passwordHint, string key);
|
||||
// TODO removed with https://bitwarden.atlassian.net/browse/PM-27328
|
||||
[Obsolete("Use ISetKeyConnectorKeyCommand instead. This method will be removed in a future version.")]
|
||||
Task<IdentityResult> SetKeyConnectorKeyAsync(User user, string key, string orgIdentifier);
|
||||
Task<IdentityResult> ConvertToKeyConnectorAsync(User user);
|
||||
Task<IdentityResult> AdminResetPasswordAsync(OrganizationUserType type, Guid orgId, Guid id, string newMasterPassword, string key);
|
||||
|
||||
@@ -621,6 +621,7 @@ public class UserService : UserManager<User>, IUserService
|
||||
return IdentityResult.Failed(_identityErrorDescriber.PasswordMismatch());
|
||||
}
|
||||
|
||||
// TODO removed with https://bitwarden.atlassian.net/browse/PM-27328
|
||||
public async Task<IdentityResult> SetKeyConnectorKeyAsync(User user, string key, string orgIdentifier)
|
||||
{
|
||||
var identityResult = CheckCanUseKeyConnector(user);
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.Text.Json;
|
||||
using Bit.Core;
|
||||
using Bit.Core.Billing.Premium.Models;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.KeyManagement.Models.Data;
|
||||
using Bit.Core.KeyManagement.UserKey;
|
||||
using Bit.Core.Models.Data;
|
||||
@@ -401,6 +402,32 @@ public class UserRepository : Repository<User, Guid>, IUserRepository
|
||||
return result.SingleOrDefault();
|
||||
}
|
||||
|
||||
public UpdateUserData SetKeyConnectorUserKey(Guid userId, string keyConnectorWrappedUserKey)
|
||||
{
|
||||
return async (connection, transaction) =>
|
||||
{
|
||||
var timestamp = DateTime.UtcNow;
|
||||
|
||||
await connection!.ExecuteAsync(
|
||||
"[dbo].[User_UpdateKeyConnectorUserKey]",
|
||||
new
|
||||
{
|
||||
Id = userId,
|
||||
Key = keyConnectorWrappedUserKey,
|
||||
// Key Connector does not use KDF, so we set some defaults
|
||||
Kdf = KdfType.Argon2id,
|
||||
KdfIterations = AuthConstants.ARGON2_ITERATIONS.Default,
|
||||
KdfMemory = AuthConstants.ARGON2_MEMORY.Default,
|
||||
KdfParallelism = AuthConstants.ARGON2_PARALLELISM.Default,
|
||||
UsesKeyConnector = true,
|
||||
RevisionDate = timestamp,
|
||||
AccountRevisionDate = timestamp
|
||||
},
|
||||
transaction: transaction,
|
||||
commandType: CommandType.StoredProcedure);
|
||||
};
|
||||
}
|
||||
|
||||
private async Task ProtectDataAndSaveAsync(User user, Func<Task> saveTask)
|
||||
{
|
||||
if (user == null)
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
using AutoMapper;
|
||||
using Bit.Core;
|
||||
using Bit.Core.Billing.Premium.Models;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.KeyManagement.Models.Data;
|
||||
using Bit.Core.KeyManagement.UserKey;
|
||||
using Bit.Core.Models.Data;
|
||||
@@ -479,6 +481,35 @@ public class UserRepository : Repository<Core.Entities.User, User, Guid>, IUserR
|
||||
}
|
||||
}
|
||||
|
||||
public UpdateUserData SetKeyConnectorUserKey(Guid userId, string keyConnectorWrappedUserKey)
|
||||
{
|
||||
return async (_, _) =>
|
||||
{
|
||||
using var scope = ServiceScopeFactory.CreateScope();
|
||||
var dbContext = GetDatabaseContext(scope);
|
||||
|
||||
var userEntity = await dbContext.Users.FindAsync(userId);
|
||||
if (userEntity == null)
|
||||
{
|
||||
throw new ArgumentException("User not found", nameof(userId));
|
||||
}
|
||||
|
||||
var timestamp = DateTime.UtcNow;
|
||||
|
||||
userEntity.Key = keyConnectorWrappedUserKey;
|
||||
// Key Connector does not use KDF, so we set some defaults
|
||||
userEntity.Kdf = KdfType.Argon2id;
|
||||
userEntity.KdfIterations = AuthConstants.ARGON2_ITERATIONS.Default;
|
||||
userEntity.KdfMemory = AuthConstants.ARGON2_MEMORY.Default;
|
||||
userEntity.KdfParallelism = AuthConstants.ARGON2_PARALLELISM.Default;
|
||||
userEntity.UsesKeyConnector = true;
|
||||
userEntity.RevisionDate = timestamp;
|
||||
userEntity.AccountRevisionDate = timestamp;
|
||||
|
||||
await dbContext.SaveChangesAsync();
|
||||
};
|
||||
}
|
||||
|
||||
private static void MigrateDefaultUserCollectionsToShared(DatabaseContext dbContext, IEnumerable<Guid> userIds)
|
||||
{
|
||||
var defaultCollections = (from c in dbContext.Collections
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
CREATE PROCEDURE [dbo].[User_UpdateKeyConnectorUserKey]
|
||||
@Id UNIQUEIDENTIFIER,
|
||||
@Key VARCHAR(MAX),
|
||||
@Kdf TINYINT,
|
||||
@KdfIterations INT,
|
||||
@KdfMemory INT,
|
||||
@KdfParallelism INT,
|
||||
@UsesKeyConnector BIT,
|
||||
@RevisionDate DATETIME2(7),
|
||||
@AccountRevisionDate DATETIME2(7)
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
|
||||
UPDATE
|
||||
[dbo].[User]
|
||||
SET
|
||||
[Key] = @Key,
|
||||
[Kdf] = @Kdf,
|
||||
[KdfIterations] = @KdfIterations,
|
||||
[KdfMemory] = @KdfMemory,
|
||||
[KdfParallelism] = @KdfParallelism,
|
||||
[UsesKeyConnector] = @UsesKeyConnector,
|
||||
[RevisionDate] = @RevisionDate,
|
||||
[AccountRevisionDate] = @AccountRevisionDate
|
||||
WHERE
|
||||
[Id] = @Id
|
||||
END
|
||||
@@ -7,6 +7,7 @@ using Bit.Api.KeyManagement.Models.Responses;
|
||||
using Bit.Api.Tools.Models.Request;
|
||||
using Bit.Api.Vault.Models;
|
||||
using Bit.Api.Vault.Models.Request;
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Auth.Entities;
|
||||
using Bit.Core.Auth.Enums;
|
||||
@@ -19,9 +20,11 @@ using Bit.Core.KeyManagement.Enums;
|
||||
using Bit.Core.KeyManagement.Models.Api.Request;
|
||||
using Bit.Core.KeyManagement.Repositories;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Vault.Enums;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Api.IntegrationTest.KeyManagement.Controllers;
|
||||
@@ -31,6 +34,7 @@ public class AccountsKeyManagementControllerTests : IClassFixture<ApiApplication
|
||||
private static readonly string _mockEncryptedString =
|
||||
"2.AOs41Hd8OQiCPXjyJKCiDA==|O6OHgt2U2hJGBSNGnimJmg==|iD33s8B69C8JhYYhSa4V1tArjvLr8eEaGqOV7BRo5Jk=";
|
||||
private static readonly string _mockEncryptedType7String = "7.AOs41Hd8OQiCPXjyJKCiDA==";
|
||||
private static readonly string _mockEncryptedType7WrappedSigningKey = "7.DRv74Kg1RSlFSam1MNFlGD==";
|
||||
|
||||
private readonly HttpClient _client;
|
||||
private readonly IEmergencyAccessRepository _emergencyAccessRepository;
|
||||
@@ -47,8 +51,11 @@ public class AccountsKeyManagementControllerTests : IClassFixture<ApiApplication
|
||||
public AccountsKeyManagementControllerTests(ApiApplicationFactory factory)
|
||||
{
|
||||
_factory = factory;
|
||||
_factory.UpdateConfiguration("globalSettings:launchDarkly:flagValues:pm-12241-private-key-regeneration",
|
||||
"true");
|
||||
_factory.SubstituteService<IFeatureService>(featureService =>
|
||||
{
|
||||
featureService.IsEnabled(FeatureFlagKeys.PrivateKeyRegeneration, Arg.Any<bool>())
|
||||
.Returns(true);
|
||||
});
|
||||
_client = factory.CreateClient();
|
||||
_loginHelper = new LoginHelper(_factory, _client);
|
||||
_userRepository = _factory.GetService<IUserRepository>();
|
||||
@@ -78,8 +85,11 @@ public class AccountsKeyManagementControllerTests : IClassFixture<ApiApplication
|
||||
{
|
||||
// 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");
|
||||
localFactory.SubstituteService<IFeatureService>(featureService =>
|
||||
{
|
||||
featureService.IsEnabled(FeatureFlagKeys.PrivateKeyRegeneration, Arg.Any<bool>())
|
||||
.Returns(false);
|
||||
});
|
||||
var localClient = localFactory.CreateClient();
|
||||
var localEmail = $"integration-test{Guid.NewGuid()}@bitwarden.com";
|
||||
var localLoginHelper = new LoginHelper(localFactory, localClient);
|
||||
@@ -285,21 +295,21 @@ public class AccountsKeyManagementControllerTests : IClassFixture<ApiApplication
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task PostSetKeyConnectorKeyAsync_Success(string organizationSsoIdentifier,
|
||||
SetKeyConnectorKeyRequestModel request)
|
||||
public async Task PostSetKeyConnectorKeyAsync_Success(string organizationSsoIdentifier)
|
||||
{
|
||||
var (ssoUserEmail, organization) = await SetupKeyConnectorTestAsync(OrganizationUserStatusType.Invited, organizationSsoIdentifier);
|
||||
|
||||
var ssoUser = await _userRepository.GetByEmailAsync(ssoUserEmail);
|
||||
Assert.NotNull(ssoUser);
|
||||
|
||||
request.Keys = new KeysRequestModel
|
||||
var request = new SetKeyConnectorKeyRequestModel
|
||||
{
|
||||
PublicKey = ssoUser.PublicKey,
|
||||
EncryptedPrivateKey = ssoUser.PrivateKey
|
||||
Key = _mockEncryptedString,
|
||||
Keys = new KeysRequestModel { PublicKey = ssoUser.PublicKey, EncryptedPrivateKey = ssoUser.PrivateKey },
|
||||
Kdf = KdfType.PBKDF2_SHA256,
|
||||
KdfIterations = AuthConstants.PBKDF2_ITERATIONS.Default,
|
||||
OrgIdentifier = organizationSsoIdentifier
|
||||
};
|
||||
request.Key = _mockEncryptedString;
|
||||
request.OrgIdentifier = organizationSsoIdentifier;
|
||||
|
||||
var response = await _client.PostAsJsonAsync("/accounts/set-key-connector-key", request);
|
||||
response.EnsureSuccessStatusCode();
|
||||
@@ -310,12 +320,95 @@ public class AccountsKeyManagementControllerTests : IClassFixture<ApiApplication
|
||||
Assert.True(user.UsesKeyConnector);
|
||||
Assert.Equal(DateTime.UtcNow, user.RevisionDate, TimeSpan.FromMinutes(1));
|
||||
Assert.Equal(DateTime.UtcNow, user.AccountRevisionDate, TimeSpan.FromMinutes(1));
|
||||
var ssoOrganizationUser = await _organizationUserRepository.GetByOrganizationAsync(organization.Id, user.Id);
|
||||
Assert.NotNull(ssoOrganizationUser);
|
||||
Assert.Equal(OrganizationUserStatusType.Accepted, ssoOrganizationUser.Status);
|
||||
Assert.Equal(user.Id, ssoOrganizationUser.UserId);
|
||||
Assert.Null(ssoOrganizationUser.Email);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PostSetKeyConnectorKeyAsync_V2_NotLoggedIn_Unauthorized()
|
||||
{
|
||||
var request = new SetKeyConnectorKeyRequestModel
|
||||
{
|
||||
KeyConnectorKeyWrappedUserKey = _mockEncryptedString,
|
||||
AccountKeys = new AccountKeysRequestModel
|
||||
{
|
||||
AccountPublicKey = "publicKey",
|
||||
UserKeyEncryptedAccountPrivateKey = _mockEncryptedType7String
|
||||
},
|
||||
OrgIdentifier = "test-org"
|
||||
};
|
||||
|
||||
var response = await _client.PostAsJsonAsync("/accounts/set-key-connector-key", request);
|
||||
|
||||
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task PostSetKeyConnectorKeyAsync_V2_Success(string organizationSsoIdentifier)
|
||||
{
|
||||
var (ssoUserEmail, organization) = await SetupKeyConnectorTestAsync(OrganizationUserStatusType.Invited, organizationSsoIdentifier);
|
||||
|
||||
var request = new SetKeyConnectorKeyRequestModel
|
||||
{
|
||||
KeyConnectorKeyWrappedUserKey = _mockEncryptedString,
|
||||
AccountKeys = new AccountKeysRequestModel
|
||||
{
|
||||
AccountPublicKey = "publicKey",
|
||||
UserKeyEncryptedAccountPrivateKey = _mockEncryptedType7String,
|
||||
PublicKeyEncryptionKeyPair = new PublicKeyEncryptionKeyPairRequestModel
|
||||
{
|
||||
PublicKey = "publicKey",
|
||||
WrappedPrivateKey = _mockEncryptedType7String,
|
||||
SignedPublicKey = "signedPublicKey"
|
||||
},
|
||||
SignatureKeyPair = new SignatureKeyPairRequestModel
|
||||
{
|
||||
SignatureAlgorithm = "ed25519",
|
||||
WrappedSigningKey = _mockEncryptedType7WrappedSigningKey,
|
||||
VerifyingKey = "verifyingKey"
|
||||
},
|
||||
SecurityState = new SecurityStateModel
|
||||
{
|
||||
SecurityVersion = 2,
|
||||
SecurityState = "v2"
|
||||
}
|
||||
},
|
||||
OrgIdentifier = organizationSsoIdentifier
|
||||
};
|
||||
|
||||
var response = await _client.PostAsJsonAsync("/accounts/set-key-connector-key", request);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
var user = await _userRepository.GetByEmailAsync(ssoUserEmail);
|
||||
Assert.NotNull(user);
|
||||
Assert.Equal(request.KeyConnectorKeyWrappedUserKey, user.Key);
|
||||
Assert.True(user.UsesKeyConnector);
|
||||
Assert.Equal(KdfType.Argon2id, user.Kdf);
|
||||
Assert.Equal(AuthConstants.ARGON2_ITERATIONS.Default, user.KdfIterations);
|
||||
Assert.Equal(AuthConstants.ARGON2_MEMORY.Default, user.KdfMemory);
|
||||
Assert.Equal(AuthConstants.ARGON2_PARALLELISM.Default, user.KdfParallelism);
|
||||
Assert.Equal(request.AccountKeys.PublicKeyEncryptionKeyPair!.SignedPublicKey, user.SignedPublicKey);
|
||||
Assert.Equal(request.AccountKeys.SecurityState!.SecurityState, user.SecurityState);
|
||||
Assert.Equal(request.AccountKeys.SecurityState.SecurityVersion, user.SecurityVersion);
|
||||
Assert.Equal(DateTime.UtcNow, user.RevisionDate, TimeSpan.FromMinutes(1));
|
||||
Assert.Equal(DateTime.UtcNow, user.AccountRevisionDate, TimeSpan.FromMinutes(1));
|
||||
|
||||
var ssoOrganizationUser =
|
||||
await _organizationUserRepository.GetByOrganizationAsync(organization.Id, user.Id);
|
||||
Assert.NotNull(ssoOrganizationUser);
|
||||
Assert.Equal(OrganizationUserStatusType.Accepted, ssoOrganizationUser.Status);
|
||||
Assert.Equal(user.Id, ssoOrganizationUser.UserId);
|
||||
Assert.Null(ssoOrganizationUser.Email);
|
||||
|
||||
var signatureKeyPair = await _userSignatureKeyPairRepository.GetByUserIdAsync(user.Id);
|
||||
Assert.NotNull(signatureKeyPair);
|
||||
Assert.Equal(SignatureAlgorithm.Ed25519, signatureKeyPair.SignatureAlgorithm);
|
||||
Assert.Equal(_mockEncryptedType7WrappedSigningKey, signatureKeyPair.WrappedSigningKey);
|
||||
Assert.Equal("verifyingKey", signatureKeyPair.VerifyingKey);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -238,10 +238,13 @@ public class AccountsKeyManagementControllerTests
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task PostSetKeyConnectorKeyAsync_UserNull_Throws(
|
||||
public async Task PostSetKeyConnectorKeyAsync_V1_UserNull_Throws(
|
||||
SutProvider<AccountsKeyManagementController> sutProvider,
|
||||
SetKeyConnectorKeyRequestModel data)
|
||||
{
|
||||
data.KeyConnectorKeyWrappedUserKey = null;
|
||||
data.AccountKeys = null;
|
||||
|
||||
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).ReturnsNull();
|
||||
|
||||
await Assert.ThrowsAsync<UnauthorizedAccessException>(() => sutProvider.Sut.PostSetKeyConnectorKeyAsync(data));
|
||||
@@ -252,10 +255,13 @@ public class AccountsKeyManagementControllerTests
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task PostSetKeyConnectorKeyAsync_SetKeyConnectorKeyFails_ThrowsBadRequestWithErrorResponse(
|
||||
public async Task PostSetKeyConnectorKeyAsync_V1_SetKeyConnectorKeyFails_ThrowsBadRequestWithErrorResponse(
|
||||
SutProvider<AccountsKeyManagementController> sutProvider,
|
||||
SetKeyConnectorKeyRequestModel data, User expectedUser)
|
||||
{
|
||||
data.KeyConnectorKeyWrappedUserKey = null;
|
||||
data.AccountKeys = null;
|
||||
|
||||
expectedUser.PublicKey = null;
|
||||
expectedUser.PrivateKey = null;
|
||||
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>())
|
||||
@@ -278,17 +284,20 @@ public class AccountsKeyManagementControllerTests
|
||||
Assert.Equal(data.KdfIterations, user.KdfIterations);
|
||||
Assert.Equal(data.KdfMemory, user.KdfMemory);
|
||||
Assert.Equal(data.KdfParallelism, user.KdfParallelism);
|
||||
Assert.Equal(data.Keys.PublicKey, user.PublicKey);
|
||||
Assert.Equal(data.Keys.EncryptedPrivateKey, user.PrivateKey);
|
||||
Assert.Equal(data.Keys!.PublicKey, user.PublicKey);
|
||||
Assert.Equal(data.Keys!.EncryptedPrivateKey, user.PrivateKey);
|
||||
}), Arg.Is(data.Key), Arg.Is(data.OrgIdentifier));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task PostSetKeyConnectorKeyAsync_SetKeyConnectorKeySucceeds_OkResponse(
|
||||
public async Task PostSetKeyConnectorKeyAsync_V1_SetKeyConnectorKeySucceeds_OkResponse(
|
||||
SutProvider<AccountsKeyManagementController> sutProvider,
|
||||
SetKeyConnectorKeyRequestModel data, User expectedUser)
|
||||
{
|
||||
data.KeyConnectorKeyWrappedUserKey = null;
|
||||
data.AccountKeys = null;
|
||||
|
||||
expectedUser.PublicKey = null;
|
||||
expectedUser.PrivateKey = null;
|
||||
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>())
|
||||
@@ -308,11 +317,108 @@ public class AccountsKeyManagementControllerTests
|
||||
Assert.Equal(data.KdfIterations, user.KdfIterations);
|
||||
Assert.Equal(data.KdfMemory, user.KdfMemory);
|
||||
Assert.Equal(data.KdfParallelism, user.KdfParallelism);
|
||||
Assert.Equal(data.Keys.PublicKey, user.PublicKey);
|
||||
Assert.Equal(data.Keys.EncryptedPrivateKey, user.PrivateKey);
|
||||
Assert.Equal(data.Keys!.PublicKey, user.PublicKey);
|
||||
Assert.Equal(data.Keys!.EncryptedPrivateKey, user.PrivateKey);
|
||||
}), Arg.Is(data.Key), Arg.Is(data.OrgIdentifier));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task PostSetKeyConnectorKeyAsync_V2_UserNull_Throws(
|
||||
SutProvider<AccountsKeyManagementController> sutProvider)
|
||||
{
|
||||
var request = new SetKeyConnectorKeyRequestModel
|
||||
{
|
||||
KeyConnectorKeyWrappedUserKey = "wrapped-user-key",
|
||||
AccountKeys = new AccountKeysRequestModel
|
||||
{
|
||||
AccountPublicKey = "public-key",
|
||||
UserKeyEncryptedAccountPrivateKey = "encrypted-private-key"
|
||||
},
|
||||
OrgIdentifier = "test-org"
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).ReturnsNull();
|
||||
|
||||
await Assert.ThrowsAsync<UnauthorizedAccessException>(() => sutProvider.Sut.PostSetKeyConnectorKeyAsync(request));
|
||||
|
||||
await sutProvider.GetDependency<ISetKeyConnectorKeyCommand>().DidNotReceive()
|
||||
.SetKeyConnectorKeyForUserAsync(Arg.Any<User>(), Arg.Any<KeyConnectorKeysData>());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task PostSetKeyConnectorKeyAsync_V2_Success(
|
||||
SutProvider<AccountsKeyManagementController> sutProvider,
|
||||
User expectedUser)
|
||||
{
|
||||
var request = new SetKeyConnectorKeyRequestModel
|
||||
{
|
||||
KeyConnectorKeyWrappedUserKey = "wrapped-user-key",
|
||||
AccountKeys = new AccountKeysRequestModel
|
||||
{
|
||||
AccountPublicKey = "public-key",
|
||||
UserKeyEncryptedAccountPrivateKey = "encrypted-private-key"
|
||||
},
|
||||
OrgIdentifier = "test-org"
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>())
|
||||
.Returns(expectedUser);
|
||||
|
||||
await sutProvider.Sut.PostSetKeyConnectorKeyAsync(request);
|
||||
|
||||
await sutProvider.GetDependency<ISetKeyConnectorKeyCommand>().Received(1)
|
||||
.SetKeyConnectorKeyForUserAsync(Arg.Is(expectedUser),
|
||||
Arg.Do<KeyConnectorKeysData>(data =>
|
||||
{
|
||||
Assert.Equal(request.KeyConnectorKeyWrappedUserKey, data.KeyConnectorKeyWrappedUserKey);
|
||||
Assert.Equal(request.AccountKeys.AccountPublicKey, data.AccountKeys.AccountPublicKey);
|
||||
Assert.Equal(request.AccountKeys.UserKeyEncryptedAccountPrivateKey,
|
||||
data.AccountKeys.UserKeyEncryptedAccountPrivateKey);
|
||||
Assert.Equal(request.OrgIdentifier, data.OrgIdentifier);
|
||||
}));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task PostSetKeyConnectorKeyAsync_V2_CommandThrows_PropagatesException(
|
||||
SutProvider<AccountsKeyManagementController> sutProvider,
|
||||
User expectedUser)
|
||||
{
|
||||
var request = new SetKeyConnectorKeyRequestModel
|
||||
{
|
||||
KeyConnectorKeyWrappedUserKey = "wrapped-user-key",
|
||||
AccountKeys = new AccountKeysRequestModel
|
||||
{
|
||||
AccountPublicKey = "public-key",
|
||||
UserKeyEncryptedAccountPrivateKey = "encrypted-private-key"
|
||||
},
|
||||
OrgIdentifier = "test-org"
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>())
|
||||
.Returns(expectedUser);
|
||||
sutProvider.GetDependency<ISetKeyConnectorKeyCommand>()
|
||||
.When(x => x.SetKeyConnectorKeyForUserAsync(Arg.Any<User>(), Arg.Any<KeyConnectorKeysData>()))
|
||||
.Do(_ => throw new BadRequestException("Command failed"));
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.PostSetKeyConnectorKeyAsync(request));
|
||||
|
||||
Assert.Equal("Command failed", exception.Message);
|
||||
await sutProvider.GetDependency<ISetKeyConnectorKeyCommand>().Received(1)
|
||||
.SetKeyConnectorKeyForUserAsync(Arg.Is(expectedUser),
|
||||
Arg.Do<KeyConnectorKeysData>(data =>
|
||||
{
|
||||
Assert.Equal(request.KeyConnectorKeyWrappedUserKey, data.KeyConnectorKeyWrappedUserKey);
|
||||
Assert.Equal(request.AccountKeys.AccountPublicKey, data.AccountKeys.AccountPublicKey);
|
||||
Assert.Equal(request.AccountKeys.UserKeyEncryptedAccountPrivateKey,
|
||||
data.AccountKeys.UserKeyEncryptedAccountPrivateKey);
|
||||
Assert.Equal(request.OrgIdentifier, data.OrgIdentifier);
|
||||
}));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task PostConvertToKeyConnectorAsync_UserNull_Throws(
|
||||
|
||||
@@ -0,0 +1,333 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Bit.Api.KeyManagement.Models.Requests;
|
||||
using Bit.Core;
|
||||
using Bit.Core.Auth.Models.Api.Request.Accounts;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.KeyManagement.Models.Api.Request;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Api.Test.KeyManagement.Models.Request;
|
||||
|
||||
public class SetKeyConnectorKeyRequestModelTests
|
||||
{
|
||||
private const string _wrappedUserKey = "2.AOs41Hd8OQiCPXjyJKCiDA==|O6OHgt2U2hJGBSNGnimJmg==|iD33s8B69C8JhYYhSa4V1tArjvLr8eEaGqOV7BRo5Jk=";
|
||||
private const string _publicKey = "public-key";
|
||||
private const string _privateKey = "private-key";
|
||||
private const string _userKey = "user-key";
|
||||
private const string _orgIdentifier = "org-identifier";
|
||||
|
||||
[Fact]
|
||||
public void Validate_V2Registration_Valid()
|
||||
{
|
||||
// Arrange
|
||||
var model = new SetKeyConnectorKeyRequestModel
|
||||
{
|
||||
KeyConnectorKeyWrappedUserKey = _wrappedUserKey,
|
||||
AccountKeys = new AccountKeysRequestModel
|
||||
{
|
||||
AccountPublicKey = _publicKey,
|
||||
UserKeyEncryptedAccountPrivateKey = _privateKey
|
||||
},
|
||||
OrgIdentifier = _orgIdentifier
|
||||
};
|
||||
|
||||
// Act
|
||||
var results = Validate(model);
|
||||
|
||||
// Assert
|
||||
Assert.Empty(results);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_V2Registration_WrappedUserKeyNotEncryptedString_Invalid()
|
||||
{
|
||||
// Arrange
|
||||
var model = new SetKeyConnectorKeyRequestModel
|
||||
{
|
||||
KeyConnectorKeyWrappedUserKey = "not-encrypted-string",
|
||||
AccountKeys = new AccountKeysRequestModel
|
||||
{
|
||||
AccountPublicKey = _publicKey,
|
||||
UserKeyEncryptedAccountPrivateKey = _privateKey
|
||||
},
|
||||
OrgIdentifier = _orgIdentifier
|
||||
};
|
||||
|
||||
// Act
|
||||
var results = Validate(model);
|
||||
|
||||
// Assert
|
||||
Assert.Single(results);
|
||||
Assert.Contains(results,
|
||||
r => r.ErrorMessage == "KeyConnectorKeyWrappedUserKey is not a valid encrypted string.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_V1Registration_Valid()
|
||||
{
|
||||
// Arrange
|
||||
var model = new SetKeyConnectorKeyRequestModel
|
||||
{
|
||||
Key = _userKey,
|
||||
Keys = new KeysRequestModel
|
||||
{
|
||||
PublicKey = _publicKey,
|
||||
EncryptedPrivateKey = _privateKey
|
||||
},
|
||||
Kdf = KdfType.PBKDF2_SHA256,
|
||||
KdfIterations = AuthConstants.PBKDF2_ITERATIONS.Default,
|
||||
OrgIdentifier = _orgIdentifier
|
||||
};
|
||||
|
||||
// Act
|
||||
var results = Validate(model);
|
||||
|
||||
// Assert
|
||||
Assert.Empty(results);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_V1Registration_MissingKey_Invalid()
|
||||
{
|
||||
// Arrange
|
||||
var model = new SetKeyConnectorKeyRequestModel
|
||||
{
|
||||
Key = null,
|
||||
Keys = new KeysRequestModel
|
||||
{
|
||||
PublicKey = _publicKey,
|
||||
EncryptedPrivateKey = _privateKey
|
||||
},
|
||||
Kdf = KdfType.PBKDF2_SHA256,
|
||||
KdfIterations = AuthConstants.PBKDF2_ITERATIONS.Default,
|
||||
OrgIdentifier = _orgIdentifier
|
||||
};
|
||||
|
||||
// Act
|
||||
var results = Validate(model);
|
||||
|
||||
// Assert
|
||||
Assert.Single(results);
|
||||
Assert.Contains(results, r => r.ErrorMessage == "Key must be supplied.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_V1Registration_MissingKeys_Invalid()
|
||||
{
|
||||
// Arrange
|
||||
var model = new SetKeyConnectorKeyRequestModel
|
||||
{
|
||||
Key = _userKey,
|
||||
Keys = null,
|
||||
Kdf = KdfType.PBKDF2_SHA256,
|
||||
KdfIterations = AuthConstants.PBKDF2_ITERATIONS.Default,
|
||||
OrgIdentifier = _orgIdentifier
|
||||
};
|
||||
|
||||
// Act
|
||||
var results = Validate(model);
|
||||
|
||||
// Assert
|
||||
Assert.Single(results);
|
||||
Assert.Contains(results, r => r.ErrorMessage == "Keys must be supplied.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_V1Registration_MissingKdf_Invalid()
|
||||
{
|
||||
// Arrange
|
||||
var model = new SetKeyConnectorKeyRequestModel
|
||||
{
|
||||
Key = _userKey,
|
||||
Keys = new KeysRequestModel
|
||||
{
|
||||
PublicKey = _publicKey,
|
||||
EncryptedPrivateKey = _privateKey
|
||||
},
|
||||
Kdf = null,
|
||||
KdfIterations = AuthConstants.PBKDF2_ITERATIONS.Default,
|
||||
OrgIdentifier = _orgIdentifier
|
||||
};
|
||||
|
||||
// Act
|
||||
var results = Validate(model);
|
||||
|
||||
// Assert
|
||||
Assert.Single(results);
|
||||
Assert.Contains(results, r => r.ErrorMessage == "Kdf must be supplied.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_V1Registration_MissingKdfIterations_Invalid()
|
||||
{
|
||||
// Arrange
|
||||
var model = new SetKeyConnectorKeyRequestModel
|
||||
{
|
||||
Key = _userKey,
|
||||
Keys = new KeysRequestModel
|
||||
{
|
||||
PublicKey = _publicKey,
|
||||
EncryptedPrivateKey = _privateKey
|
||||
},
|
||||
Kdf = KdfType.PBKDF2_SHA256,
|
||||
KdfIterations = null,
|
||||
OrgIdentifier = _orgIdentifier
|
||||
};
|
||||
|
||||
// Act
|
||||
var results = Validate(model);
|
||||
|
||||
// Assert
|
||||
Assert.Single(results);
|
||||
Assert.Contains(results, r => r.ErrorMessage == "KdfIterations must be supplied.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_V1Registration_Argon2id_MissingKdfMemory_Invalid()
|
||||
{
|
||||
// Arrange
|
||||
var model = new SetKeyConnectorKeyRequestModel
|
||||
{
|
||||
Key = _userKey,
|
||||
Keys = new KeysRequestModel
|
||||
{
|
||||
PublicKey = _publicKey,
|
||||
EncryptedPrivateKey = _privateKey
|
||||
},
|
||||
Kdf = KdfType.Argon2id,
|
||||
KdfIterations = AuthConstants.ARGON2_ITERATIONS.Default,
|
||||
KdfMemory = null,
|
||||
KdfParallelism = AuthConstants.ARGON2_PARALLELISM.Default,
|
||||
OrgIdentifier = _orgIdentifier
|
||||
};
|
||||
|
||||
// Act
|
||||
var results = Validate(model);
|
||||
|
||||
// Assert
|
||||
Assert.Single(results);
|
||||
Assert.Contains(results, r => r.ErrorMessage == "KdfMemory must be supplied when Kdf is Argon2id.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_V1Registration_Argon2id_MissingKdfParallelism_Invalid()
|
||||
{
|
||||
// Arrange
|
||||
var model = new SetKeyConnectorKeyRequestModel
|
||||
{
|
||||
Key = _userKey,
|
||||
Keys = new KeysRequestModel
|
||||
{
|
||||
PublicKey = _publicKey,
|
||||
EncryptedPrivateKey = _privateKey
|
||||
},
|
||||
Kdf = KdfType.Argon2id,
|
||||
KdfIterations = AuthConstants.ARGON2_ITERATIONS.Default,
|
||||
KdfMemory = AuthConstants.ARGON2_MEMORY.Default,
|
||||
KdfParallelism = null,
|
||||
OrgIdentifier = _orgIdentifier
|
||||
};
|
||||
|
||||
// Act
|
||||
var results = Validate(model);
|
||||
|
||||
// Assert
|
||||
Assert.Single(results);
|
||||
Assert.Contains(results, r => r.ErrorMessage == "KdfParallelism must be supplied when Kdf is Argon2id.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToKeyConnectorKeysData_EmptyKeyConnectorKeyWrappedUserKey_ThrowsException()
|
||||
{
|
||||
// Arrange
|
||||
var model = new SetKeyConnectorKeyRequestModel
|
||||
{
|
||||
KeyConnectorKeyWrappedUserKey = "",
|
||||
AccountKeys = new AccountKeysRequestModel
|
||||
{
|
||||
AccountPublicKey = _publicKey,
|
||||
UserKeyEncryptedAccountPrivateKey = _privateKey
|
||||
},
|
||||
OrgIdentifier = _orgIdentifier
|
||||
};
|
||||
|
||||
// Act
|
||||
var exception = Assert.Throws<BadRequestException>(() => model.ToKeyConnectorKeysData());
|
||||
|
||||
// Assert
|
||||
Assert.Equal("KeyConnectorKeyWrappedUserKey and AccountKeys must be supplied.", exception.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToKeyConnectorKeysData_NullKeyConnectorKeyWrappedUserKey_ThrowsException()
|
||||
{
|
||||
// Arrange
|
||||
var model = new SetKeyConnectorKeyRequestModel
|
||||
{
|
||||
KeyConnectorKeyWrappedUserKey = null,
|
||||
AccountKeys = new AccountKeysRequestModel
|
||||
{
|
||||
AccountPublicKey = _publicKey,
|
||||
UserKeyEncryptedAccountPrivateKey = _privateKey
|
||||
},
|
||||
OrgIdentifier = _orgIdentifier
|
||||
};
|
||||
|
||||
// Act
|
||||
var exception = Assert.Throws<BadRequestException>(() => model.ToKeyConnectorKeysData());
|
||||
|
||||
// Assert
|
||||
Assert.Equal("KeyConnectorKeyWrappedUserKey and AccountKeys must be supplied.", exception.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToKeyConnectorKeysData_NullAccountKeys_ThrowsException()
|
||||
{
|
||||
// Arrange
|
||||
var model = new SetKeyConnectorKeyRequestModel
|
||||
{
|
||||
KeyConnectorKeyWrappedUserKey = _wrappedUserKey,
|
||||
AccountKeys = null,
|
||||
OrgIdentifier = _orgIdentifier
|
||||
};
|
||||
|
||||
// Act
|
||||
var exception = Assert.Throws<BadRequestException>(() => model.ToKeyConnectorKeysData());
|
||||
|
||||
// Assert
|
||||
Assert.Equal("KeyConnectorKeyWrappedUserKey and AccountKeys must be supplied.", exception.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToKeyConnectorKeysData_Valid_Success()
|
||||
{
|
||||
// Arrange
|
||||
var model = new SetKeyConnectorKeyRequestModel
|
||||
{
|
||||
KeyConnectorKeyWrappedUserKey = _wrappedUserKey,
|
||||
AccountKeys = new AccountKeysRequestModel
|
||||
{
|
||||
AccountPublicKey = _publicKey,
|
||||
UserKeyEncryptedAccountPrivateKey = _privateKey
|
||||
},
|
||||
OrgIdentifier = _orgIdentifier
|
||||
};
|
||||
|
||||
// Act
|
||||
var data = model.ToKeyConnectorKeysData();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(_wrappedUserKey, data.KeyConnectorKeyWrappedUserKey);
|
||||
Assert.Equal(_publicKey, data.AccountKeys.AccountPublicKey);
|
||||
Assert.Equal(_privateKey, data.AccountKeys.UserKeyEncryptedAccountPrivateKey);
|
||||
Assert.Equal(_orgIdentifier, data.OrgIdentifier);
|
||||
}
|
||||
|
||||
private static List<ValidationResult> Validate(SetKeyConnectorKeyRequestModel model)
|
||||
{
|
||||
var results = new List<ValidationResult>();
|
||||
Validator.TryValidateObject(model, new ValidationContext(model), results, true);
|
||||
return results;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
using System.Security.Claims;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.KeyManagement.Authorization;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.KeyManagement.Authorization;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class KeyConnectorAuthorizationHandlerTests
|
||||
{
|
||||
[Theory, BitAutoData]
|
||||
public async Task HandleRequirementAsync_UserCanUseKeyConnector_Success(
|
||||
User user,
|
||||
ClaimsPrincipal claimsPrincipal,
|
||||
SutProvider<KeyConnectorAuthorizationHandler> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
user.UsesKeyConnector = false;
|
||||
sutProvider.GetDependency<ICurrentContext>().Organizations
|
||||
.Returns(new List<CurrentContextOrganization>());
|
||||
|
||||
var requirement = KeyConnectorOperations.Use;
|
||||
var context = new AuthorizationHandlerContext([requirement], claimsPrincipal, user);
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.HandleAsync(context);
|
||||
|
||||
// Assert
|
||||
Assert.True(context.HasSucceeded);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task HandleRequirementAsync_UserAlreadyUsesKeyConnector_Fails(
|
||||
User user,
|
||||
ClaimsPrincipal claimsPrincipal,
|
||||
SutProvider<KeyConnectorAuthorizationHandler> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
user.UsesKeyConnector = true;
|
||||
sutProvider.GetDependency<ICurrentContext>().Organizations
|
||||
.Returns(new List<CurrentContextOrganization>());
|
||||
|
||||
var requirement = KeyConnectorOperations.Use;
|
||||
var context = new AuthorizationHandlerContext([requirement], claimsPrincipal, user);
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.HandleAsync(context);
|
||||
|
||||
// Assert
|
||||
Assert.False(context.HasSucceeded);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task HandleRequirementAsync_UserIsOwner_Fails(
|
||||
User user,
|
||||
Guid organizationId,
|
||||
ClaimsPrincipal claimsPrincipal,
|
||||
SutProvider<KeyConnectorAuthorizationHandler> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
user.UsesKeyConnector = false;
|
||||
var organizations = new List<CurrentContextOrganization>
|
||||
{
|
||||
new() { Id = organizationId, Type = OrganizationUserType.Owner }
|
||||
};
|
||||
sutProvider.GetDependency<ICurrentContext>().Organizations.Returns(organizations);
|
||||
|
||||
var requirement = KeyConnectorOperations.Use;
|
||||
var context = new AuthorizationHandlerContext([requirement], claimsPrincipal, user);
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.HandleAsync(context);
|
||||
|
||||
// Assert
|
||||
Assert.False(context.HasSucceeded);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task HandleRequirementAsync_UserIsAdmin_Fails(
|
||||
User user,
|
||||
Guid organizationId,
|
||||
ClaimsPrincipal claimsPrincipal,
|
||||
SutProvider<KeyConnectorAuthorizationHandler> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
user.UsesKeyConnector = false;
|
||||
var organizations = new List<CurrentContextOrganization>
|
||||
{
|
||||
new() { Id = organizationId, Type = OrganizationUserType.Admin }
|
||||
};
|
||||
sutProvider.GetDependency<ICurrentContext>().Organizations.Returns(organizations);
|
||||
|
||||
var requirement = KeyConnectorOperations.Use;
|
||||
var context = new AuthorizationHandlerContext([requirement], claimsPrincipal, user);
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.HandleAsync(context);
|
||||
|
||||
// Assert
|
||||
Assert.False(context.HasSucceeded);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task HandleRequirementAsync_UserIsRegularMember_Success(
|
||||
User user,
|
||||
Guid organizationId,
|
||||
ClaimsPrincipal claimsPrincipal,
|
||||
SutProvider<KeyConnectorAuthorizationHandler> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
user.UsesKeyConnector = false;
|
||||
var organizations = new List<CurrentContextOrganization>
|
||||
{
|
||||
new() { Id = organizationId, Type = OrganizationUserType.User }
|
||||
};
|
||||
sutProvider.GetDependency<ICurrentContext>().Organizations.Returns(organizations);
|
||||
|
||||
var requirement = KeyConnectorOperations.Use;
|
||||
var context = new AuthorizationHandlerContext([requirement], claimsPrincipal, user);
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.HandleAsync(context);
|
||||
|
||||
// Assert
|
||||
Assert.True(context.HasSucceeded);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task HandleRequirementAsync_UnsupportedRequirement_ThrowsArgumentException(
|
||||
User user,
|
||||
ClaimsPrincipal claimsPrincipal,
|
||||
SutProvider<KeyConnectorAuthorizationHandler> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
user.UsesKeyConnector = false;
|
||||
sutProvider.GetDependency<ICurrentContext>().Organizations
|
||||
.Returns(new List<CurrentContextOrganization>());
|
||||
|
||||
var unsupportedRequirement = new KeyConnectorOperationsRequirement("UnsupportedOperation");
|
||||
var context = new AuthorizationHandlerContext([unsupportedRequirement], claimsPrincipal, user);
|
||||
|
||||
// Act & Assert
|
||||
await Assert.ThrowsAsync<ArgumentException>(() => sutProvider.Sut.HandleAsync(context));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
using System.Security.Claims;
|
||||
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.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.KeyManagement.Commands;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class SetKeyConnectorKeyCommandTests
|
||||
{
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task SetKeyConnectorKeyForUserAsync_Success_SetsAccountKeys(
|
||||
User user,
|
||||
KeyConnectorKeysData data,
|
||||
SutProvider<SetKeyConnectorKeyCommand> sutProvider)
|
||||
{
|
||||
// Set up valid V2 encryption data
|
||||
if (data.AccountKeys!.SignatureKeyPair != null)
|
||||
{
|
||||
data.AccountKeys.SignatureKeyPair.SignatureAlgorithm = "ed25519";
|
||||
}
|
||||
|
||||
var expectedAccountKeysData = data.AccountKeys.ToAccountKeysData();
|
||||
|
||||
// Arrange
|
||||
user.UsesKeyConnector = false;
|
||||
var currentContext = sutProvider.GetDependency<ICurrentContext>();
|
||||
var httpContext = Substitute.For<HttpContext>();
|
||||
httpContext.User.Returns(new ClaimsPrincipal());
|
||||
currentContext.HttpContext.Returns(httpContext);
|
||||
|
||||
sutProvider.GetDependency<IAuthorizationService>()
|
||||
.AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), user, Arg.Any<IEnumerable<IAuthorizationRequirement>>())
|
||||
.Returns(AuthorizationResult.Success());
|
||||
|
||||
var userRepository = sutProvider.GetDependency<IUserRepository>();
|
||||
var mockUpdateUserData = Substitute.For<UpdateUserData>();
|
||||
userRepository.SetKeyConnectorUserKey(user.Id, data.KeyConnectorKeyWrappedUserKey!)
|
||||
.Returns(mockUpdateUserData);
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.SetKeyConnectorKeyForUserAsync(user, data);
|
||||
|
||||
// Assert
|
||||
|
||||
userRepository
|
||||
.Received(1)
|
||||
.SetKeyConnectorUserKey(user.Id, data.KeyConnectorKeyWrappedUserKey);
|
||||
|
||||
await userRepository
|
||||
.Received(1)
|
||||
.SetV2AccountCryptographicStateAsync(
|
||||
user.Id,
|
||||
Arg.Is<UserAccountKeysData>(data =>
|
||||
data.PublicKeyEncryptionKeyPairData.PublicKey == expectedAccountKeysData.PublicKeyEncryptionKeyPairData.PublicKey &&
|
||||
data.PublicKeyEncryptionKeyPairData.WrappedPrivateKey == expectedAccountKeysData.PublicKeyEncryptionKeyPairData.WrappedPrivateKey &&
|
||||
data.PublicKeyEncryptionKeyPairData.SignedPublicKey == expectedAccountKeysData.PublicKeyEncryptionKeyPairData.SignedPublicKey &&
|
||||
data.SignatureKeyPairData!.SignatureAlgorithm == expectedAccountKeysData.SignatureKeyPairData!.SignatureAlgorithm &&
|
||||
data.SignatureKeyPairData.WrappedSigningKey == expectedAccountKeysData.SignatureKeyPairData.WrappedSigningKey &&
|
||||
data.SignatureKeyPairData.VerifyingKey == expectedAccountKeysData.SignatureKeyPairData.VerifyingKey &&
|
||||
data.SecurityStateData!.SecurityState == expectedAccountKeysData.SecurityStateData!.SecurityState &&
|
||||
data.SecurityStateData.SecurityVersion == expectedAccountKeysData.SecurityStateData.SecurityVersion),
|
||||
Arg.Is<IEnumerable<UpdateUserData>>(actions =>
|
||||
actions.Count() == 1 && actions.First() == mockUpdateUserData));
|
||||
|
||||
await sutProvider.GetDependency<IEventService>()
|
||||
.Received(1)
|
||||
.LogUserEventAsync(user.Id, EventType.User_MigratedKeyToKeyConnector);
|
||||
|
||||
await sutProvider.GetDependency<IAcceptOrgUserCommand>()
|
||||
.Received(1)
|
||||
.AcceptOrgUserByOrgSsoIdAsync(data.OrgIdentifier, user, sutProvider.GetDependency<IUserService>());
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task SetKeyConnectorKeyForUserAsync_UserCantUseKeyConnector_ThrowsException(
|
||||
User user,
|
||||
KeyConnectorKeysData data,
|
||||
SutProvider<SetKeyConnectorKeyCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
user.UsesKeyConnector = true;
|
||||
var currentContext = sutProvider.GetDependency<ICurrentContext>();
|
||||
var httpContext = Substitute.For<HttpContext>();
|
||||
httpContext.User.Returns(new ClaimsPrincipal());
|
||||
currentContext.HttpContext.Returns(httpContext);
|
||||
|
||||
sutProvider.GetDependency<IAuthorizationService>()
|
||||
.AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), user, Arg.Any<IEnumerable<IAuthorizationRequirement>>())
|
||||
.Returns(AuthorizationResult.Failed());
|
||||
|
||||
// Act & Assert
|
||||
await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.SetKeyConnectorKeyForUserAsync(user, data));
|
||||
|
||||
sutProvider.GetDependency<IUserRepository>()
|
||||
.DidNotReceiveWithAnyArgs()
|
||||
.SetKeyConnectorUserKey(Arg.Any<Guid>(), Arg.Any<string>());
|
||||
|
||||
await sutProvider.GetDependency<IUserRepository>()
|
||||
.DidNotReceiveWithAnyArgs()
|
||||
.SetV2AccountCryptographicStateAsync(Arg.Any<Guid>(), Arg.Any<UserAccountKeysData>(), Arg.Any<IEnumerable<UpdateUserData>>());
|
||||
|
||||
await sutProvider.GetDependency<IEventService>()
|
||||
.DidNotReceiveWithAnyArgs()
|
||||
.LogUserEventAsync(Arg.Any<Guid>(), Arg.Any<EventType>());
|
||||
|
||||
await sutProvider.GetDependency<IAcceptOrgUserCommand>()
|
||||
.DidNotReceiveWithAnyArgs()
|
||||
.AcceptOrgUserByOrgSsoIdAsync(Arg.Any<string>(), Arg.Any<User>(), Arg.Any<IUserService>());
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,11 @@
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Infrastructure.IntegrationTest.AdminConsole;
|
||||
using Microsoft.Data.SqlClient;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Infrastructure.IntegrationTest.Repositories;
|
||||
@@ -500,4 +502,54 @@ public class UserRepositoryTests
|
||||
// Assert
|
||||
Assert.Empty(results);
|
||||
}
|
||||
|
||||
[Theory, DatabaseData]
|
||||
public async Task SetKeyConnectorUserKey_UpdatesUserKey(IUserRepository userRepository, Database database)
|
||||
{
|
||||
var user = await userRepository.CreateTestUserAsync();
|
||||
|
||||
const string keyConnectorWrappedKey = "key-connector-wrapped-user-key";
|
||||
|
||||
var setKeyConnectorUserKeyDelegate = userRepository.SetKeyConnectorUserKey(user.Id, keyConnectorWrappedKey);
|
||||
|
||||
await RunUpdateUserDataAsync(setKeyConnectorUserKeyDelegate, database);
|
||||
|
||||
var updatedUser = await userRepository.GetByIdAsync(user.Id);
|
||||
|
||||
Assert.NotNull(updatedUser);
|
||||
Assert.Equal(keyConnectorWrappedKey, updatedUser.Key);
|
||||
Assert.True(updatedUser.UsesKeyConnector);
|
||||
Assert.Equal(KdfType.Argon2id, updatedUser.Kdf);
|
||||
Assert.Equal(AuthConstants.ARGON2_ITERATIONS.Default, updatedUser.KdfIterations);
|
||||
Assert.Equal(AuthConstants.ARGON2_MEMORY.Default, updatedUser.KdfMemory);
|
||||
Assert.Equal(AuthConstants.ARGON2_PARALLELISM.Default, updatedUser.KdfParallelism);
|
||||
Assert.Equal(DateTime.UtcNow, updatedUser.RevisionDate, TimeSpan.FromMinutes(1));
|
||||
Assert.Equal(DateTime.UtcNow, updatedUser.AccountRevisionDate, TimeSpan.FromMinutes(1));
|
||||
}
|
||||
|
||||
private static async Task RunUpdateUserDataAsync(UpdateUserData task, Database database)
|
||||
{
|
||||
if (database.Type == SupportedDatabaseProviders.SqlServer && !database.UseEf)
|
||||
{
|
||||
await using var connection = new SqlConnection(database.ConnectionString);
|
||||
connection.Open();
|
||||
|
||||
await using var transaction = connection.BeginTransaction();
|
||||
try
|
||||
{
|
||||
await task(connection, transaction);
|
||||
|
||||
transaction.Commit();
|
||||
}
|
||||
catch
|
||||
{
|
||||
transaction.Rollback();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
await task();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
CREATE OR ALTER PROCEDURE [dbo].[User_UpdateKeyConnectorUserKey]
|
||||
@Id UNIQUEIDENTIFIER,
|
||||
@Key VARCHAR(MAX),
|
||||
@Kdf TINYINT,
|
||||
@KdfIterations INT,
|
||||
@KdfMemory INT,
|
||||
@KdfParallelism INT,
|
||||
@UsesKeyConnector BIT,
|
||||
@RevisionDate DATETIME2(7),
|
||||
@AccountRevisionDate DATETIME2(7)
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
|
||||
UPDATE
|
||||
[dbo].[User]
|
||||
SET
|
||||
[Key] = @Key,
|
||||
[Kdf] = @Kdf,
|
||||
[KdfIterations] = @KdfIterations,
|
||||
[KdfMemory] = @KdfMemory,
|
||||
[KdfParallelism] = @KdfParallelism,
|
||||
[UsesKeyConnector] = @UsesKeyConnector,
|
||||
[RevisionDate] = @RevisionDate,
|
||||
[AccountRevisionDate] = @AccountRevisionDate
|
||||
WHERE
|
||||
[Id] = @Id
|
||||
END
|
||||
GO
|
||||
Reference in New Issue
Block a user