Browse Source
## 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 teampull/3529/head
17 changed files with 485 additions and 8 deletions
@ -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; |
||||||
|
} |
||||||
|
} |
||||||
@ -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; |
||||||
|
} |
||||||
|
} |
||||||
@ -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); |
||||||
|
} |
||||||
|
} |
||||||
@ -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); |
||||||
|
} |
||||||
|
} |
||||||
@ -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); |
||||||
|
} |
||||||
|
} |
||||||
@ -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); |
||||||
|
} |
||||||
|
} |
||||||
Loading…
Reference in new issue