Browse Source
* layout new key rotation methods - add endpoint with request model - add command with data model - add repository method * layout new key rotation methods - add endpoint with request model - add command with data model - add repository method * formatting * rename account recovery to reset password * fix tests * remove extra endpoint * rename account recovery to reset password * fix tests and formatting * register db calls in command, removing list from user repo * formattingpull/3441/head
12 changed files with 336 additions and 34 deletions
@ -0,0 +1,19 @@
@@ -0,0 +1,19 @@
|
||||
using Bit.Core.Auth.Entities; |
||||
using Bit.Core.Entities; |
||||
using Bit.Core.Tools.Entities; |
||||
using Bit.Core.Vault.Entities; |
||||
|
||||
namespace Bit.Core.Auth.Models.Data; |
||||
|
||||
public class RotateUserKeyData |
||||
{ |
||||
public User User { get; set; } |
||||
public string MasterPasswordHash { get; set; } |
||||
public string Key { get; set; } |
||||
public string PrivateKey { get; set; } |
||||
public IEnumerable<Cipher> Ciphers { get; set; } |
||||
public IEnumerable<Folder> Folders { get; set; } |
||||
public IEnumerable<Send> Sends { get; set; } |
||||
public IEnumerable<EmergencyAccess> EmergencyAccessKeys { get; set; } |
||||
public IEnumerable<OrganizationUser> ResetPasswordKeys { get; set; } |
||||
} |
||||
@ -0,0 +1,18 @@
@@ -0,0 +1,18 @@
|
||||
using Bit.Core.Auth.Models.Data; |
||||
using Microsoft.AspNetCore.Identity; |
||||
using Microsoft.Data.SqlClient; |
||||
|
||||
namespace Bit.Core.Auth.UserFeatures.UserKey; |
||||
|
||||
public interface IRotateUserKeyCommand |
||||
{ |
||||
/// <summary> |
||||
/// Sets a new user key and updates all encrypted data. |
||||
/// </summary> |
||||
/// <param name="model">All necessary information for rotation. Warning: Any encrypted data not included will be lost.</param> |
||||
/// <returns>An IdentityResult for verification of the master password hash</returns> |
||||
/// <exception cref="ArgumentNullException">User must be provided.</exception> |
||||
Task<IdentityResult> RotateUserKeyAsync(RotateUserKeyData model); |
||||
} |
||||
|
||||
public delegate Task UpdateEncryptedDataForKeyRotation(SqlTransaction transaction = null); |
||||
@ -0,0 +1,61 @@
@@ -0,0 +1,61 @@
|
||||
using Bit.Core.Auth.Models.Data; |
||||
using Bit.Core.Repositories; |
||||
using Bit.Core.Services; |
||||
using Microsoft.AspNetCore.Identity; |
||||
|
||||
namespace Bit.Core.Auth.UserFeatures.UserKey.Implementations; |
||||
|
||||
public class RotateUserKeyCommand : IRotateUserKeyCommand |
||||
{ |
||||
private readonly IUserService _userService; |
||||
private readonly IUserRepository _userRepository; |
||||
private readonly IPushNotificationService _pushService; |
||||
private readonly IdentityErrorDescriber _identityErrorDescriber; |
||||
|
||||
public RotateUserKeyCommand(IUserService userService, IUserRepository userRepository, |
||||
IPushNotificationService pushService, IdentityErrorDescriber errors) |
||||
{ |
||||
_userService = userService; |
||||
_userRepository = userRepository; |
||||
_pushService = pushService; |
||||
_identityErrorDescriber = errors; |
||||
} |
||||
|
||||
/// <inheritdoc /> |
||||
public async Task<IdentityResult> RotateUserKeyAsync(RotateUserKeyData model) |
||||
{ |
||||
if (model.User == null) |
||||
{ |
||||
throw new ArgumentNullException(nameof(model.User)); |
||||
} |
||||
|
||||
if (!await _userService.CheckPasswordAsync(model.User, model.MasterPasswordHash)) |
||||
{ |
||||
return IdentityResult.Failed(_identityErrorDescriber.PasswordMismatch()); |
||||
} |
||||
|
||||
var now = DateTime.UtcNow; |
||||
model.User.RevisionDate = model.User.AccountRevisionDate = now; |
||||
model.User.LastKeyRotationDate = now; |
||||
model.User.SecurityStamp = Guid.NewGuid().ToString(); |
||||
model.User.Key = model.Key; |
||||
model.User.PrivateKey = model.PrivateKey; |
||||
if (model.Ciphers.Any() || model.Folders.Any() || model.Sends.Any() || model.EmergencyAccessKeys.Any() || |
||||
model.ResetPasswordKeys.Any()) |
||||
{ |
||||
List<UpdateEncryptedDataForKeyRotation> saveEncryptedDataActions = new(); |
||||
// if (model.Ciphers.Any()) |
||||
// { |
||||
// saveEncryptedDataActions.Add(_cipherRepository.SaveRotatedData); |
||||
// } |
||||
await _userRepository.UpdateUserKeyAndEncryptedDataAsync(model.User, saveEncryptedDataActions); |
||||
} |
||||
else |
||||
{ |
||||
await _userRepository.ReplaceAsync(model.User); |
||||
} |
||||
|
||||
await _pushService.PushLogOutAsync(model.User.Id, excludeCurrentContextFromPush: true); |
||||
return IdentityResult.Success; |
||||
} |
||||
} |
||||
@ -0,0 +1,50 @@
@@ -0,0 +1,50 @@
|
||||
using Bit.Core.Auth.Models.Data; |
||||
using Bit.Core.Auth.UserFeatures.UserKey.Implementations; |
||||
using Bit.Core.Services; |
||||
using Bit.Test.Common.AutoFixture; |
||||
using Bit.Test.Common.AutoFixture.Attributes; |
||||
using Microsoft.AspNetCore.Identity; |
||||
using NSubstitute; |
||||
using Xunit; |
||||
|
||||
namespace Bit.Core.Test.Auth.UserFeatures.UserKey; |
||||
|
||||
[SutProviderCustomize] |
||||
public class RotateUserKeyCommandTests |
||||
{ |
||||
[Theory, BitAutoData] |
||||
public async Task RotateUserKeyAsync_Success(SutProvider<RotateUserKeyCommand> sutProvider, RotateUserKeyData model) |
||||
{ |
||||
sutProvider.GetDependency<IUserService>().CheckPasswordAsync(model.User, model.MasterPasswordHash) |
||||
.Returns(true); |
||||
|
||||
var result = await sutProvider.Sut.RotateUserKeyAsync(model); |
||||
|
||||
Assert.Equal(IdentityResult.Success, result); |
||||
} |
||||
|
||||
[Theory, BitAutoData] |
||||
public async Task RotateUserKeyAsync_InvalidMasterPasswordHash_ReturnsFailedIdentityResult( |
||||
SutProvider<RotateUserKeyCommand> sutProvider, RotateUserKeyData model) |
||||
{ |
||||
sutProvider.GetDependency<IUserService>().CheckPasswordAsync(model.User, model.MasterPasswordHash) |
||||
.Returns(false); |
||||
|
||||
var result = await sutProvider.Sut.RotateUserKeyAsync(model); |
||||
|
||||
Assert.False(result.Succeeded); |
||||
} |
||||
|
||||
[Theory, BitAutoData] |
||||
public async Task RotateUserKeyAsync_LogsOutUser( |
||||
SutProvider<RotateUserKeyCommand> sutProvider, RotateUserKeyData model) |
||||
{ |
||||
sutProvider.GetDependency<IUserService>().CheckPasswordAsync(model.User, model.MasterPasswordHash) |
||||
.Returns(true); |
||||
|
||||
await sutProvider.Sut.RotateUserKeyAsync(model); |
||||
|
||||
await sutProvider.GetDependency<IPushNotificationService>().ReceivedWithAnyArgs() |
||||
.PushLogOutAsync(default, default); |
||||
} |
||||
} |
||||
Loading…
Reference in new issue