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

[PM-3797 Part 3] Add vault domains to key rotation (#3436)

## Type of change

<!-- (mark with an `X`) -->

```
- [ ] Bug fix
- [ ] New feature development
- [x] Tech debt (refactoring, code cleanup, dependency upgrades, etc)
- [ ] Build/deploy pipeline (DevOps)
- [ ] Other
```

## Objective
<!--Describe what the purpose of this PR is. For example: what bug you're fixing or what new feature you're adding-->
Previous PR: #3434
Adds ciphers and folders to the new key rotation.


## Code changes
<!--Explain the changes you've made to each file or major component. This should help the reviewer understand your changes-->
<!--Also refer to any related changes or PRs in other repositories-->

* **file.ext:** Description of what was changed and why

## Before you submit

- Please check for formatting errors (`dotnet format --verify-no-changes`) (required)
- If making database changes - make sure you also update Entity Framework queries and/or migrations
- Please add **unit tests** where it makes sense to do so (encouraged but not required)
- If this change requires a **documentation update** - notify the documentation team
- If this change has particular **deployment requirements** - notify the DevOps team
This commit is contained in:
Jake Fink 2023-12-06 08:46:36 -05:00 committed by GitHub
parent dbf8907bfc
commit 4b2bd6cee6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 485 additions and 8 deletions

View File

@ -6,6 +6,7 @@ using Bit.Api.Models.Request;
using Bit.Api.Models.Request.Accounts;
using Bit.Api.Models.Response;
using Bit.Api.Utilities;
using Bit.Api.Vault.Models.Request;
using Bit.Core;
using Bit.Core.AdminConsole.Enums.Provider;
using Bit.Core.AdminConsole.Repositories;
@ -65,6 +66,8 @@ public class AccountsController : Controller
private bool UseFlexibleCollections =>
_featureService.IsEnabled(FeatureFlagKeys.FlexibleCollections, _currentContext);
private readonly IRotationValidator<IEnumerable<CipherWithIdRequestModel>, IEnumerable<Cipher>> _cipherValidator;
private readonly IRotationValidator<IEnumerable<FolderWithIdRequestModel>, IEnumerable<Folder>> _folderValidator;
private readonly IRotationValidator<IEnumerable<EmergencyAccessWithIdRequestModel>, IEnumerable<EmergencyAccess>>
_emergencyAccessValidator;
@ -87,6 +90,8 @@ public class AccountsController : Controller
IRotateUserKeyCommand rotateUserKeyCommand,
IFeatureService featureService,
ICurrentContext currentContext,
IRotationValidator<IEnumerable<CipherWithIdRequestModel>, IEnumerable<Cipher>> cipherValidator,
IRotationValidator<IEnumerable<FolderWithIdRequestModel>, IEnumerable<Folder>> folderValidator,
IRotationValidator<IEnumerable<EmergencyAccessWithIdRequestModel>, IEnumerable<EmergencyAccess>>
emergencyAccessValidator
)
@ -108,6 +113,8 @@ public class AccountsController : Controller
_rotateUserKeyCommand = rotateUserKeyCommand;
_featureService = featureService;
_currentContext = currentContext;
_cipherValidator = cipherValidator;
_folderValidator = folderValidator;
_emergencyAccessValidator = emergencyAccessValidator;
}
@ -414,8 +421,8 @@ public class AccountsController : Controller
MasterPasswordHash = model.MasterPasswordHash,
Key = model.Key,
PrivateKey = model.PrivateKey,
Ciphers = new List<Cipher>(),
Folders = new List<Folder>(),
Ciphers = await _cipherValidator.ValidateAsync(user, model.Ciphers),
Folders = await _folderValidator.ValidateAsync(user, model.Folders),
Sends = new List<Send>(),
EmergencyAccessKeys = await _emergencyAccessValidator.ValidateAsync(user, model.EmergencyAccessKeys),
ResetPasswordKeys = new List<OrganizationUser>(),

View File

@ -9,6 +9,8 @@ using IdentityModel;
using System.Globalization;
using Bit.Api.Auth.Models.Request;
using Bit.Api.Auth.Validators;
using Bit.Api.Vault.Models.Request;
using Bit.Api.Vault.Validators;
using Bit.Core.Auth.Entities;
using Bit.Core.IdentityServer;
using Bit.SharedWeb.Health;
@ -21,6 +23,7 @@ using Bit.Core.Auth.Identity;
using Bit.Core.Auth.UserFeatures.UserKey;
using Bit.Core.Auth.UserFeatures.UserKey.Implementations;
using Bit.Core.OrganizationFeatures.OrganizationSubscriptions;
using Bit.Core.Vault.Entities;
#if !OSS
using Bit.Commercial.Core.SecretsManager;
@ -141,6 +144,12 @@ public class Startup
services
.AddScoped<IRotationValidator<IEnumerable<EmergencyAccessWithIdRequestModel>, IEnumerable<EmergencyAccess>>,
EmergencyAccessRotationValidator>();
services
.AddScoped<IRotationValidator<IEnumerable<CipherWithIdRequestModel>, IEnumerable<Cipher>>,
CipherRotationValidator>();
services
.AddScoped<IRotationValidator<IEnumerable<FolderWithIdRequestModel>, IEnumerable<Folder>>,
FolderRotationValidator>();
// Services
services.AddBaseServices(globalSettings);

View File

@ -0,0 +1,56 @@
using Bit.Api.Auth.Validators;
using Bit.Api.Vault.Models.Request;
using Bit.Core;
using Bit.Core.Context;
using Bit.Core.Entities;
using Bit.Core.Exceptions;
using Bit.Core.Services;
using Bit.Core.Vault.Entities;
using Bit.Core.Vault.Repositories;
namespace Bit.Api.Vault.Validators;
public class CipherRotationValidator : IRotationValidator<IEnumerable<CipherWithIdRequestModel>, IEnumerable<Cipher>>
{
private readonly ICipherRepository _cipherRepository;
private readonly ICurrentContext _currentContext;
private readonly IFeatureService _featureService;
private bool UseFlexibleCollections =>
_featureService.IsEnabled(FeatureFlagKeys.FlexibleCollections, _currentContext);
public CipherRotationValidator(ICipherRepository cipherRepository, ICurrentContext currentContext,
IFeatureService featureService)
{
_cipherRepository = cipherRepository;
_currentContext = currentContext;
_featureService = featureService;
}
public async Task<IEnumerable<Cipher>> ValidateAsync(User user, IEnumerable<CipherWithIdRequestModel> ciphers)
{
var result = new List<Cipher>();
if (ciphers == null || !ciphers.Any())
{
return result;
}
var existingCiphers = await _cipherRepository.GetManyByUserIdAsync(user.Id, UseFlexibleCollections);
if (existingCiphers == null || !existingCiphers.Any())
{
return result;
}
foreach (var existing in existingCiphers)
{
var cipher = ciphers.FirstOrDefault(c => c.Id == existing.Id);
if (cipher == null)
{
throw new BadRequestException("All existing ciphers must be included in the rotation.");
}
result.Add(cipher.ToCipher(existing));
}
return result;
}
}

View File

@ -0,0 +1,44 @@
using Bit.Api.Auth.Validators;
using Bit.Api.Vault.Models.Request;
using Bit.Core.Entities;
using Bit.Core.Exceptions;
using Bit.Core.Vault.Entities;
using Bit.Core.Vault.Repositories;
namespace Bit.Api.Vault.Validators;
public class FolderRotationValidator : IRotationValidator<IEnumerable<FolderWithIdRequestModel>, IEnumerable<Folder>>
{
private readonly IFolderRepository _folderRepository;
public FolderRotationValidator(IFolderRepository folderRepository)
{
_folderRepository = folderRepository;
}
public async Task<IEnumerable<Folder>> ValidateAsync(User user, IEnumerable<FolderWithIdRequestModel> folders)
{
var result = new List<Folder>();
if (folders == null || !folders.Any())
{
return result;
}
var existingFolders = await _folderRepository.GetManyByUserIdAsync(user.Id);
if (existingFolders == null || !existingFolders.Any())
{
return result;
}
foreach (var existing in existingFolders)
{
var folder = folders.FirstOrDefault(c => c.Id == existing.Id);
if (folder == null)
{
throw new BadRequestException("All existing folders must be included in the rotation.");
}
result.Add(folder.ToFolder(existing));
}
return result;
}
}

View File

@ -2,6 +2,7 @@
using Bit.Core.Entities;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Vault.Repositories;
using Microsoft.AspNetCore.Identity;
namespace Bit.Core.Auth.UserFeatures.UserKey.Implementations;
@ -10,16 +11,21 @@ public class RotateUserKeyCommand : IRotateUserKeyCommand
{
private readonly IUserService _userService;
private readonly IUserRepository _userRepository;
private readonly ICipherRepository _cipherRepository;
private readonly IFolderRepository _folderRepository;
private readonly IEmergencyAccessRepository _emergencyAccessRepository;
private readonly IPushNotificationService _pushService;
private readonly IdentityErrorDescriber _identityErrorDescriber;
public RotateUserKeyCommand(IUserService userService, IUserRepository userRepository,
ICipherRepository cipherRepository, IFolderRepository folderRepository,
IEmergencyAccessRepository emergencyAccessRepository,
IPushNotificationService pushService, IdentityErrorDescriber errors)
{
_userService = userService;
_userRepository = userRepository;
_cipherRepository = cipherRepository;
_folderRepository = folderRepository;
_emergencyAccessRepository = emergencyAccessRepository;
_pushService = pushService;
_identityErrorDescriber = errors;
@ -48,10 +54,16 @@ public class RotateUserKeyCommand : IRotateUserKeyCommand
model.ResetPasswordKeys.Any())
{
List<UpdateEncryptedDataForKeyRotation> saveEncryptedDataActions = new();
// if (model.Ciphers.Any())
// {
// saveEncryptedDataActions.Add(_cipherRepository.SaveRotatedData);
// }
if (model.Ciphers.Any())
{
saveEncryptedDataActions.Add(_cipherRepository.UpdateForKeyRotation(user.Id, model.Ciphers));
}
if (model.Folders.Any())
{
saveEncryptedDataActions.Add(_folderRepository.UpdateForKeyRotation(user.Id, model.Folders));
}
if (model.EmergencyAccessKeys.Any())
{
saveEncryptedDataActions.Add(

View File

@ -1,4 +1,5 @@
using Bit.Core.Entities;
using Bit.Core.Auth.UserFeatures.UserKey;
using Bit.Core.Entities;
using Bit.Core.Repositories;
using Bit.Core.Tools.Entities;
using Bit.Core.Vault.Entities;
@ -38,4 +39,12 @@ public interface ICipherRepository : IRepository<Cipher, Guid>
Task<DateTime> RestoreAsync(IEnumerable<Guid> ids, Guid userId, bool useFlexibleCollections);
Task<DateTime> RestoreByIdsOrganizationIdAsync(IEnumerable<Guid> ids, Guid organizationId);
Task DeleteDeletedAsync(DateTime deletedDateBefore);
/// <summary>
/// Updates encrypted data for ciphers during a key rotation
/// </summary>
/// <param name="userId">The user that initiated the key rotation</param>
/// <param name="ciphers">A list of ciphers with updated data</param>
UpdateEncryptedDataForKeyRotation UpdateForKeyRotation(Guid userId,
IEnumerable<Cipher> ciphers);
}

View File

@ -1,4 +1,5 @@
using Bit.Core.Repositories;
using Bit.Core.Auth.UserFeatures.UserKey;
using Bit.Core.Repositories;
using Bit.Core.Vault.Entities;
namespace Bit.Core.Vault.Repositories;
@ -7,4 +8,12 @@ public interface IFolderRepository : IRepository<Folder, Guid>
{
Task<Folder> GetByIdAsync(Guid id, Guid userId);
Task<ICollection<Folder>> GetManyByUserIdAsync(Guid userId);
/// <summary>
/// Updates encrypted data for folders during a key rotation
/// </summary>
/// <param name="userId">The user that initiated the key rotation</param>
/// <param name="folders">A list of folders with updated data</param>
UpdateEncryptedDataForKeyRotation UpdateForKeyRotation(Guid userId,
IEnumerable<Folder> folders);
}

View File

@ -0,0 +1,33 @@
using System.Data;
using Bit.Core.Vault.Entities;
using Dapper;
namespace Bit.Infrastructure.Dapper.Vault.Helpers;
public static class CipherHelpers
{
public static DataTable ToDataTable(this IEnumerable<Cipher> ciphers)
{
var ciphersTable = new DataTable();
ciphersTable.SetTypeName("[dbo].[Cipher]");
var columnData = new List<(string name, Type type, Func<Cipher, object> getter)>
{
(nameof(Cipher.Id), typeof(Guid), c => c.Id),
(nameof(Cipher.UserId), typeof(Guid), c => c.UserId),
(nameof(Cipher.OrganizationId), typeof(Guid), c => c.OrganizationId),
(nameof(Cipher.Type), typeof(short), c => c.Type),
(nameof(Cipher.Data), typeof(string), c => c.Data),
(nameof(Cipher.Favorites), typeof(string), c => c.Favorites),
(nameof(Cipher.Folders), typeof(string), c => c.Folders),
(nameof(Cipher.Attachments), typeof(string), c => c.Attachments),
(nameof(Cipher.CreationDate), typeof(DateTime), c => c.CreationDate),
(nameof(Cipher.RevisionDate), typeof(DateTime), c => c.RevisionDate),
(nameof(Cipher.DeletedDate), typeof(DateTime), c => c.DeletedDate),
(nameof(Cipher.Reprompt), typeof(short), c => c.Reprompt),
(nameof(Cipher.Key), typeof(string), c => c.Key),
};
return ciphers.BuildTable(ciphersTable, columnData);
}
}

View File

@ -0,0 +1,25 @@
using System.Data;
using Bit.Core.Vault.Entities;
using Dapper;
namespace Bit.Infrastructure.Dapper.Vault.Helpers;
public static class FolderHelpers
{
public static DataTable ToDataTable(this IEnumerable<Folder> folders)
{
var foldersTable = new DataTable();
foldersTable.SetTypeName("[dbo].[Folder]");
var columnData = new List<(string name, Type type, Func<Folder, object> getter)>
{
(nameof(Folder.Id), typeof(Guid), c => c.Id),
(nameof(Folder.UserId), typeof(Guid), c => c.UserId),
(nameof(Folder.Name), typeof(string), c => c.Name),
(nameof(Folder.CreationDate), typeof(DateTime), c => c.CreationDate),
(nameof(Folder.RevisionDate), typeof(DateTime), c => c.RevisionDate),
};
return folders.BuildTable(foldersTable, columnData);
}
}

View File

@ -1,5 +1,6 @@
using System.Data;
using System.Text.Json;
using Bit.Core.Auth.UserFeatures.UserKey;
using Bit.Core.Entities;
using Bit.Core.Settings;
using Bit.Core.Tools.Entities;
@ -7,6 +8,7 @@ using Bit.Core.Vault.Entities;
using Bit.Core.Vault.Models.Data;
using Bit.Core.Vault.Repositories;
using Bit.Infrastructure.Dapper.Repositories;
using Bit.Infrastructure.Dapper.Vault.Helpers;
using Dapper;
using Microsoft.Data.SqlClient;
@ -308,6 +310,63 @@ public class CipherRepository : Repository<Cipher, Guid>, ICipherRepository
}
}
/// <inheritdoc />
public UpdateEncryptedDataForKeyRotation UpdateForKeyRotation(
Guid userId, IEnumerable<Cipher> ciphers)
{
return async (SqlConnection connection, SqlTransaction transaction) =>
{
// Create temp table
var sqlCreateTemp = @"
SELECT TOP 0 *
INTO #TempCipher
FROM [dbo].[Cipher]";
await using (var cmd = new SqlCommand(sqlCreateTemp, connection, transaction))
{
cmd.ExecuteNonQuery();
}
// Bulk copy data into temp table
using (var bulkCopy = new SqlBulkCopy(connection, SqlBulkCopyOptions.KeepIdentity, transaction))
{
bulkCopy.DestinationTableName = "#TempCipher";
var ciphersTable = ciphers.ToDataTable();
foreach (DataColumn col in ciphersTable.Columns)
{
bulkCopy.ColumnMappings.Add(col.ColumnName, col.ColumnName);
}
ciphersTable.PrimaryKey = new DataColumn[] { ciphersTable.Columns[0] };
await bulkCopy.WriteToServerAsync(ciphersTable);
}
// Update cipher table from temp table
var sql = @"
UPDATE
[dbo].[Cipher]
SET
[Data] = TC.[Data],
[Attachments] = TC.[Attachments],
[RevisionDate] = TC.[RevisionDate],
[Key] = TC.[Key]
FROM
[dbo].[Cipher] C
INNER JOIN
#TempCipher TC ON C.Id = TC.Id
WHERE
C.[UserId] = @UserId
DROP TABLE #TempCipher";
await using (var cmd = new SqlCommand(sql, connection, transaction))
{
cmd.Parameters.Add("@UserId", SqlDbType.UniqueIdentifier).Value = userId;
cmd.ExecuteNonQuery();
}
};
}
public Task UpdateUserKeysAndCiphersAsync(User user, IEnumerable<Cipher> ciphers, IEnumerable<Folder> folders, IEnumerable<Send> sends)
{
using (var connection = new SqlConnection(ConnectionString))

View File

@ -1,8 +1,10 @@
using System.Data;
using Bit.Core.Auth.UserFeatures.UserKey;
using Bit.Core.Settings;
using Bit.Core.Vault.Entities;
using Bit.Core.Vault.Repositories;
using Bit.Infrastructure.Dapper.Repositories;
using Bit.Infrastructure.Dapper.Vault.Helpers;
using Dapper;
using Microsoft.Data.SqlClient;
@ -41,4 +43,60 @@ public class FolderRepository : Repository<Folder, Guid>, IFolderRepository
return results.ToList();
}
}
/// <inheritdoc />
public UpdateEncryptedDataForKeyRotation UpdateForKeyRotation(
Guid userId, IEnumerable<Folder> folders)
{
return async (SqlConnection connection, SqlTransaction transaction) =>
{
// Create temp table
var sqlCreateTemp = @"
SELECT TOP 0 *
INTO #TempFolder
FROM [dbo].[Folder]";
await using (var cmd = new SqlCommand(sqlCreateTemp, connection, transaction))
{
cmd.ExecuteNonQuery();
}
// Bulk copy data into temp table
using (var bulkCopy = new SqlBulkCopy(connection, SqlBulkCopyOptions.KeepIdentity, transaction))
{
bulkCopy.DestinationTableName = "#TempFolder";
var foldersTable = folders.ToDataTable();
foreach (DataColumn col in foldersTable.Columns)
{
bulkCopy.ColumnMappings.Add(col.ColumnName, col.ColumnName);
}
foldersTable.PrimaryKey = new DataColumn[] { foldersTable.Columns[0] };
await bulkCopy.WriteToServerAsync(foldersTable);
}
// Update folder table from temp table
var sql = @"
UPDATE
[dbo].[Folder]
SET
[Name] = TF.[Name],
[RevisionDate] = TF.[RevisionDate]
FROM
[dbo].[Folder] F
INNER JOIN
#TempFolder TF ON F.Id = TF.Id
WHERE
F.[UserId] = @UserId;
DROP TABLE #TempFolder";
await using (var cmd = new SqlCommand(sql, connection, transaction))
{
cmd.Parameters.Add("@UserId", SqlDbType.UniqueIdentifier).Value = userId;
cmd.ExecuteNonQuery();
}
};
}
}

View File

@ -161,6 +161,8 @@ public class UserRepository : Repository<Core.Entities.User, User, Guid>, IUserR
entity.AccountRevisionDate = user.AccountRevisionDate;
entity.RevisionDate = user.RevisionDate;
await dbContext.SaveChangesAsync();
// Update re-encrypted data
foreach (var action in updateDataActions)
{

View File

@ -1,6 +1,7 @@
using System.Text.Json;
using System.Text.Json.Nodes;
using AutoMapper;
using Bit.Core.Auth.UserFeatures.UserKey;
using Bit.Core.Enums;
using Bit.Core.Utilities;
using Bit.Core.Vault.Enums;
@ -13,6 +14,7 @@ using Bit.Infrastructure.EntityFramework.Repositories.Vault.Queries;
using Bit.Infrastructure.EntityFramework.Vault.Models;
using Bit.Infrastructure.EntityFramework.Vault.Repositories.Queries;
using LinqToDB.EntityFrameworkCore;
using Microsoft.Data.SqlClient;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using NS = Newtonsoft.Json;
@ -825,6 +827,34 @@ public class CipherRepository : Repository<Core.Vault.Entities.Cipher, Cipher, G
}
}
/// <inheritdoc />
public UpdateEncryptedDataForKeyRotation UpdateForKeyRotation(
Guid userId, IEnumerable<Core.Vault.Entities.Cipher> ciphers)
{
return async (SqlConnection _, SqlTransaction _) =>
{
var newCiphers = ciphers.ToList();
using var scope = ServiceScopeFactory.CreateScope();
var dbContext = GetDatabaseContext(scope);
var userCiphers = await GetDbSet(dbContext)
.Where(c => c.UserId == userId)
.ToListAsync();
var validCiphers = userCiphers
.Where(cipher => newCiphers.Any(newCipher => newCipher.Id == cipher.Id));
foreach (var cipher in validCiphers)
{
var updateCipher = newCiphers.First(newCipher => newCipher.Id == cipher.Id);
cipher.Data = updateCipher.Data;
cipher.Attachments = updateCipher.Attachments;
cipher.RevisionDate = updateCipher.RevisionDate;
cipher.Key = updateCipher.Key;
}
await dbContext.SaveChangesAsync();
};
}
public async Task UpdateUserKeysAndCiphersAsync(User user, IEnumerable<Core.Vault.Entities.Cipher> ciphers, IEnumerable<Core.Vault.Entities.Folder> folders, IEnumerable<Core.Tools.Entities.Send> sends)
{
using (var scope = ServiceScopeFactory.CreateScope())

View File

@ -1,7 +1,9 @@
using AutoMapper;
using Bit.Core.Auth.UserFeatures.UserKey;
using Bit.Core.Vault.Repositories;
using Bit.Infrastructure.EntityFramework.Repositories;
using Bit.Infrastructure.EntityFramework.Vault.Models;
using Microsoft.Data.SqlClient;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
@ -36,4 +38,28 @@ public class FolderRepository : Repository<Core.Vault.Entities.Folder, Folder, G
return Mapper.Map<List<Core.Vault.Entities.Folder>>(folders);
}
}
/// <inheritdoc />
public UpdateEncryptedDataForKeyRotation UpdateForKeyRotation(
Guid userId, IEnumerable<Core.Vault.Entities.Folder> folders)
{
return async (SqlConnection _, SqlTransaction _) =>
{
var newFolders = folders.ToList();
using var scope = ServiceScopeFactory.CreateScope();
var dbContext = GetDatabaseContext(scope);
var userFolders = await GetDbSet(dbContext)
.Where(f => f.UserId == userId)
.ToListAsync();
var validFolders = userFolders
.Where(folder => newFolders.Any(newFolder => newFolder.Id == folder.Id));
foreach (var folder in validFolders)
{
var updateFolder = newFolders.First(newFolder => newFolder.Id == folder.Id);
folder.Name = updateFolder.Name;
}
await dbContext.SaveChangesAsync();
};
}
}

View File

@ -3,6 +3,7 @@ using Bit.Api.Auth.Controllers;
using Bit.Api.Auth.Models.Request;
using Bit.Api.Auth.Models.Request.Accounts;
using Bit.Api.Auth.Validators;
using Bit.Api.Vault.Models.Request;
using Bit.Core;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.AdminConsole.Services;
@ -21,6 +22,7 @@ using Bit.Core.Services;
using Bit.Core.Settings;
using Bit.Core.Tools.Repositories;
using Bit.Core.Tools.Services;
using Bit.Core.Vault.Entities;
using Bit.Core.Vault.Repositories;
using Bit.Test.Common.AutoFixture.Attributes;
using Microsoft.AspNetCore.Identity;
@ -51,6 +53,9 @@ public class AccountsControllerTests : IDisposable
private readonly IFeatureService _featureService;
private readonly ICurrentContext _currentContext;
private readonly IRotationValidator<IEnumerable<CipherWithIdRequestModel>, IEnumerable<Cipher>> _cipherValidator;
private readonly IRotationValidator<IEnumerable<FolderWithIdRequestModel>, IEnumerable<Folder>> _folderValidator;
private readonly IRotationValidator<IEnumerable<EmergencyAccessWithIdRequestModel>, IEnumerable<EmergencyAccess>>
_emergencyAccessValidator;
@ -74,6 +79,10 @@ public class AccountsControllerTests : IDisposable
_rotateUserKeyCommand = Substitute.For<IRotateUserKeyCommand>();
_featureService = Substitute.For<IFeatureService>();
_currentContext = Substitute.For<ICurrentContext>();
_cipherValidator =
Substitute.For<IRotationValidator<IEnumerable<CipherWithIdRequestModel>, IEnumerable<Cipher>>>();
_folderValidator =
Substitute.For<IRotationValidator<IEnumerable<FolderWithIdRequestModel>, IEnumerable<Folder>>>();
_emergencyAccessValidator = Substitute.For<IRotationValidator<IEnumerable<EmergencyAccessWithIdRequestModel>,
IEnumerable<EmergencyAccess>>>();
@ -95,6 +104,8 @@ public class AccountsControllerTests : IDisposable
_rotateUserKeyCommand,
_featureService,
_currentContext,
_cipherValidator,
_folderValidator,
_emergencyAccessValidator
);
}

View File

@ -0,0 +1,45 @@
using Bit.Api.Vault.Models.Request;
using Bit.Api.Vault.Validators;
using Bit.Core.Entities;
using Bit.Core.Exceptions;
using Bit.Core.Vault.Models.Data;
using Bit.Core.Vault.Repositories;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using Xunit;
namespace Bit.Api.Test.Vault.Validators;
[SutProviderCustomize]
public class CipherRotationValidatorTests
{
[Theory, BitAutoData]
public async Task ValidateAsync_MissingCipher_Throws(SutProvider<CipherRotationValidator> sutProvider, User user,
IEnumerable<CipherWithIdRequestModel> ciphers)
{
var userCiphers = ciphers.Select(c => new CipherDetails { Id = c.Id.GetValueOrDefault(), Type = c.Type })
.ToList();
userCiphers.Add(new CipherDetails { Id = Guid.NewGuid(), Type = Core.Vault.Enums.CipherType.Login });
sutProvider.GetDependency<ICipherRepository>().GetManyByUserIdAsync(user.Id, Arg.Any<bool>())
.Returns(userCiphers);
await Assert.ThrowsAsync<BadRequestException>(async () => await sutProvider.Sut.ValidateAsync(user, ciphers));
}
[Theory, BitAutoData]
public async Task ValidateAsync_CipherDoesNotBelongToUser_NotIncluded(
SutProvider<CipherRotationValidator> sutProvider, User user, IEnumerable<CipherWithIdRequestModel> ciphers)
{
var userCiphers = ciphers.Select(c => new CipherDetails { Id = c.Id.GetValueOrDefault(), Type = c.Type })
.ToList();
userCiphers.RemoveAt(0);
sutProvider.GetDependency<ICipherRepository>().GetManyByUserIdAsync(user.Id, Arg.Any<bool>())
.Returns(userCiphers);
var result = await sutProvider.Sut.ValidateAsync(user, ciphers);
Assert.DoesNotContain(result, c => c.Id == ciphers.First().Id);
}
}

View File

@ -0,0 +1,42 @@
using Bit.Api.Vault.Models.Request;
using Bit.Api.Vault.Validators;
using Bit.Core.Entities;
using Bit.Core.Exceptions;
using Bit.Core.Vault.Entities;
using Bit.Core.Vault.Repositories;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using Xunit;
namespace Bit.Api.Test.Vault.Validators;
[SutProviderCustomize]
public class FolderRotationValidatorTests
{
[Theory]
[BitAutoData]
public async Task ValidateAsync_MissingFolder_Throws(SutProvider<FolderRotationValidator> sutProvider, User user,
IEnumerable<FolderWithIdRequestModel> folders)
{
var userFolders = folders.Select(f => f.ToFolder(new Folder())).ToList();
userFolders.Add(new Folder { Id = Guid.NewGuid(), Name = "Missing Folder" });
sutProvider.GetDependency<IFolderRepository>().GetManyByUserIdAsync(user.Id).Returns(userFolders);
await Assert.ThrowsAsync<BadRequestException>(async () => await sutProvider.Sut.ValidateAsync(user, folders));
}
[Theory]
[BitAutoData]
public async Task ValidateAsync_FolderDoesNotBelongToUser_NotReturned(
SutProvider<FolderRotationValidator> sutProvider, User user, IEnumerable<FolderWithIdRequestModel> folders)
{
var userFolders = folders.Select(f => f.ToFolder(new Folder())).ToList();
userFolders.RemoveAt(0);
sutProvider.GetDependency<IFolderRepository>().GetManyByUserIdAsync(user.Id).Returns(userFolders);
var result = await sutProvider.Sut.ValidateAsync(user, folders);
Assert.DoesNotContain(result, c => c.Id == folders.First().Id);
}
}