1
0
mirror of https://github.com/bitwarden/server.git synced 2025-02-16 01:51:21 +01:00

[PM-3797 Part 4] Add Sends to new Key Rotation (#3442)

* add send validation

* add send repo methods

* add send rotation to delegate list

* add success test
This commit is contained in:
Jake Fink 2023-12-12 11:58:34 -05:00 committed by GitHub
parent 6a6a29d881
commit ca8e3f496e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 424 additions and 7 deletions

View File

@ -5,6 +5,7 @@ using Bit.Api.Auth.Validators;
using Bit.Api.Models.Request; using Bit.Api.Models.Request;
using Bit.Api.Models.Request.Accounts; using Bit.Api.Models.Request.Accounts;
using Bit.Api.Models.Response; using Bit.Api.Models.Response;
using Bit.Api.Tools.Models.Request;
using Bit.Api.Utilities; using Bit.Api.Utilities;
using Bit.Api.Vault.Models.Request; using Bit.Api.Vault.Models.Request;
using Bit.Core; using Bit.Core;
@ -68,6 +69,7 @@ public class AccountsController : Controller
private readonly IRotationValidator<IEnumerable<CipherWithIdRequestModel>, IEnumerable<Cipher>> _cipherValidator; private readonly IRotationValidator<IEnumerable<CipherWithIdRequestModel>, IEnumerable<Cipher>> _cipherValidator;
private readonly IRotationValidator<IEnumerable<FolderWithIdRequestModel>, IEnumerable<Folder>> _folderValidator; private readonly IRotationValidator<IEnumerable<FolderWithIdRequestModel>, IEnumerable<Folder>> _folderValidator;
private readonly IRotationValidator<IEnumerable<SendWithIdRequestModel>, IReadOnlyList<Send>> _sendValidator;
private readonly IRotationValidator<IEnumerable<EmergencyAccessWithIdRequestModel>, IEnumerable<EmergencyAccess>> private readonly IRotationValidator<IEnumerable<EmergencyAccessWithIdRequestModel>, IEnumerable<EmergencyAccess>>
_emergencyAccessValidator; _emergencyAccessValidator;
@ -92,6 +94,7 @@ public class AccountsController : Controller
ICurrentContext currentContext, ICurrentContext currentContext,
IRotationValidator<IEnumerable<CipherWithIdRequestModel>, IEnumerable<Cipher>> cipherValidator, IRotationValidator<IEnumerable<CipherWithIdRequestModel>, IEnumerable<Cipher>> cipherValidator,
IRotationValidator<IEnumerable<FolderWithIdRequestModel>, IEnumerable<Folder>> folderValidator, IRotationValidator<IEnumerable<FolderWithIdRequestModel>, IEnumerable<Folder>> folderValidator,
IRotationValidator<IEnumerable<SendWithIdRequestModel>, IReadOnlyList<Send>> sendValidator,
IRotationValidator<IEnumerable<EmergencyAccessWithIdRequestModel>, IEnumerable<EmergencyAccess>> IRotationValidator<IEnumerable<EmergencyAccessWithIdRequestModel>, IEnumerable<EmergencyAccess>>
emergencyAccessValidator emergencyAccessValidator
) )
@ -115,6 +118,7 @@ public class AccountsController : Controller
_currentContext = currentContext; _currentContext = currentContext;
_cipherValidator = cipherValidator; _cipherValidator = cipherValidator;
_folderValidator = folderValidator; _folderValidator = folderValidator;
_sendValidator = sendValidator;
_emergencyAccessValidator = emergencyAccessValidator; _emergencyAccessValidator = emergencyAccessValidator;
} }
@ -423,7 +427,7 @@ public class AccountsController : Controller
PrivateKey = model.PrivateKey, PrivateKey = model.PrivateKey,
Ciphers = await _cipherValidator.ValidateAsync(user, model.Ciphers), Ciphers = await _cipherValidator.ValidateAsync(user, model.Ciphers),
Folders = await _folderValidator.ValidateAsync(user, model.Folders), Folders = await _folderValidator.ValidateAsync(user, model.Folders),
Sends = new List<Send>(), Sends = await _sendValidator.ValidateAsync(user, model.Sends),
EmergencyAccessKeys = await _emergencyAccessValidator.ValidateAsync(user, model.EmergencyAccessKeys), EmergencyAccessKeys = await _emergencyAccessValidator.ValidateAsync(user, model.EmergencyAccessKeys),
ResetPasswordKeys = new List<OrganizationUser>(), ResetPasswordKeys = new List<OrganizationUser>(),
}; };

View File

@ -1,4 +1,5 @@
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.Exceptions;
namespace Bit.Api.Auth.Validators; namespace Bit.Api.Auth.Validators;
@ -11,5 +12,11 @@ namespace Bit.Api.Auth.Validators;
/// <typeparam name="R">Domain model</typeparam> /// <typeparam name="R">Domain model</typeparam>
public interface IRotationValidator<T, R> public interface IRotationValidator<T, R>
{ {
/// <summary>
/// Validates re-encrypted data before being saved to database.
/// </summary>
/// <param name="user">Request model</param>
/// <param name="data">Domain model</param>
/// <exception cref="BadRequestException">Throws if data fails validation</exception>
Task<R> ValidateAsync(User user, T data); Task<R> ValidateAsync(User user, T data);
} }

View File

@ -9,6 +9,8 @@ using IdentityModel;
using System.Globalization; using System.Globalization;
using Bit.Api.Auth.Models.Request; using Bit.Api.Auth.Models.Request;
using Bit.Api.Auth.Validators; using Bit.Api.Auth.Validators;
using Bit.Api.Tools.Models.Request;
using Bit.Api.Tools.Validators;
using Bit.Api.Vault.Models.Request; using Bit.Api.Vault.Models.Request;
using Bit.Api.Vault.Validators; using Bit.Api.Vault.Validators;
using Bit.Core.Auth.Entities; using Bit.Core.Auth.Entities;
@ -23,6 +25,7 @@ using Bit.Core.Auth.Identity;
using Bit.Core.Auth.UserFeatures.UserKey; using Bit.Core.Auth.UserFeatures.UserKey;
using Bit.Core.Auth.UserFeatures.UserKey.Implementations; using Bit.Core.Auth.UserFeatures.UserKey.Implementations;
using Bit.Core.OrganizationFeatures.OrganizationSubscriptions; using Bit.Core.OrganizationFeatures.OrganizationSubscriptions;
using Bit.Core.Tools.Entities;
using Bit.Core.Vault.Entities; using Bit.Core.Vault.Entities;
#if !OSS #if !OSS
@ -141,15 +144,18 @@ public class Startup
// Key Rotation // Key Rotation
services.AddScoped<IRotateUserKeyCommand, RotateUserKeyCommand>(); services.AddScoped<IRotateUserKeyCommand, RotateUserKeyCommand>();
services
.AddScoped<IRotationValidator<IEnumerable<EmergencyAccessWithIdRequestModel>, IEnumerable<EmergencyAccess>>,
EmergencyAccessRotationValidator>();
services services
.AddScoped<IRotationValidator<IEnumerable<CipherWithIdRequestModel>, IEnumerable<Cipher>>, .AddScoped<IRotationValidator<IEnumerable<CipherWithIdRequestModel>, IEnumerable<Cipher>>,
CipherRotationValidator>(); CipherRotationValidator>();
services services
.AddScoped<IRotationValidator<IEnumerable<FolderWithIdRequestModel>, IEnumerable<Folder>>, .AddScoped<IRotationValidator<IEnumerable<FolderWithIdRequestModel>, IEnumerable<Folder>>,
FolderRotationValidator>(); FolderRotationValidator>();
services
.AddScoped<IRotationValidator<IEnumerable<SendWithIdRequestModel>, IReadOnlyList<Send>>,
SendRotationValidator>();
services
.AddScoped<IRotationValidator<IEnumerable<EmergencyAccessWithIdRequestModel>, IEnumerable<EmergencyAccess>>,
EmergencyAccessRotationValidator>();
// Services // Services
services.AddBaseServices(globalSettings); services.AddBaseServices(globalSettings);

View File

@ -0,0 +1,57 @@
using Bit.Api.Auth.Validators;
using Bit.Api.Tools.Models.Request;
using Bit.Core.Entities;
using Bit.Core.Exceptions;
using Bit.Core.Tools.Entities;
using Bit.Core.Tools.Repositories;
using Bit.Core.Tools.Services;
namespace Bit.Api.Tools.Validators;
/// <summary>
/// Send implementation for <see cref="IRotationValidator{T,R}"/>
/// </summary>
public class SendRotationValidator : IRotationValidator<IEnumerable<SendWithIdRequestModel>, IReadOnlyList<Send>>
{
private readonly ISendService _sendService;
private readonly ISendRepository _sendRepository;
/// <summary>
/// Instantiates a new <see cref="SendRotationValidator"/>
/// </summary>
/// <param name="sendService">Enables conversion of <see cref="SendWithIdRequestModel"/> to <see cref="Send"/></param>
/// <param name="sendRepository">Retrieves all user <see cref="Send"/>s</param>
public SendRotationValidator(ISendService sendService, ISendRepository sendRepository)
{
_sendService = sendService;
_sendRepository = sendRepository;
}
public async Task<IReadOnlyList<Send>> ValidateAsync(User user, IEnumerable<SendWithIdRequestModel> sends)
{
var result = new List<Send>();
if (sends == null || !sends.Any())
{
return result;
}
var existingSends = await _sendRepository.GetManyByUserIdAsync(user.Id);
if (existingSends == null || !existingSends.Any())
{
return result;
}
foreach (var existing in existingSends)
{
var send = sends.FirstOrDefault(c => c.Id == existing.Id);
if (send == null)
{
throw new BadRequestException("All existing folders must be included in the rotation.");
}
result.Add(send.ToSend(existing, _sendService));
}
return result;
}
}

View File

@ -12,7 +12,7 @@ public class RotateUserKeyData
public string PrivateKey { get; set; } public string PrivateKey { get; set; }
public IEnumerable<Cipher> Ciphers { get; set; } public IEnumerable<Cipher> Ciphers { get; set; }
public IEnumerable<Folder> Folders { get; set; } public IEnumerable<Folder> Folders { get; set; }
public IEnumerable<Send> Sends { get; set; } public IReadOnlyList<Send> Sends { get; set; }
public IEnumerable<EmergencyAccess> EmergencyAccessKeys { get; set; } public IEnumerable<EmergencyAccess> EmergencyAccessKeys { get; set; }
public IEnumerable<OrganizationUser> ResetPasswordKeys { get; set; } public IEnumerable<OrganizationUser> ResetPasswordKeys { get; set; }
} }

View File

@ -5,6 +5,9 @@ using Microsoft.Data.SqlClient;
namespace Bit.Core.Auth.UserFeatures.UserKey; namespace Bit.Core.Auth.UserFeatures.UserKey;
/// <summary>
/// Responsible for rotation of a user key and updating database with re-encrypted data
/// </summary>
public interface IRotateUserKeyCommand public interface IRotateUserKeyCommand
{ {
/// <summary> /// <summary>

View File

@ -2,23 +2,37 @@
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Services; using Bit.Core.Services;
using Bit.Core.Tools.Repositories;
using Bit.Core.Vault.Repositories; using Bit.Core.Vault.Repositories;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
namespace Bit.Core.Auth.UserFeatures.UserKey.Implementations; namespace Bit.Core.Auth.UserFeatures.UserKey.Implementations;
/// <inheritdoc />
public class RotateUserKeyCommand : IRotateUserKeyCommand public class RotateUserKeyCommand : IRotateUserKeyCommand
{ {
private readonly IUserService _userService; private readonly IUserService _userService;
private readonly IUserRepository _userRepository; private readonly IUserRepository _userRepository;
private readonly ICipherRepository _cipherRepository; private readonly ICipherRepository _cipherRepository;
private readonly IFolderRepository _folderRepository; private readonly IFolderRepository _folderRepository;
private readonly ISendRepository _sendRepository;
private readonly IEmergencyAccessRepository _emergencyAccessRepository; private readonly IEmergencyAccessRepository _emergencyAccessRepository;
private readonly IPushNotificationService _pushService; private readonly IPushNotificationService _pushService;
private readonly IdentityErrorDescriber _identityErrorDescriber; private readonly IdentityErrorDescriber _identityErrorDescriber;
/// <summary>
/// Instantiates a new <see cref="RotateUserKeyCommand"/>
/// </summary>
/// <param name="userService">Master password hash validation</param>
/// <param name="userRepository">Updates user keys and re-encrypted data if needed</param>
/// <param name="cipherRepository">Provides a method to update re-encrypted cipher data</param>
/// <param name="folderRepository">Provides a method to update re-encrypted folder data</param>
/// <param name="sendRepository">Provides a method to update re-encrypted send data</param>
/// <param name="emergencyAccessRepository">Provides a method to update re-encrypted emergency access data</param>
/// <param name="pushService">Logs out user from other devices after successful rotation</param>
/// <param name="errors">Provides a password mismatch error if master password hash validation fails</param>
public RotateUserKeyCommand(IUserService userService, IUserRepository userRepository, public RotateUserKeyCommand(IUserService userService, IUserRepository userRepository,
ICipherRepository cipherRepository, IFolderRepository folderRepository, ICipherRepository cipherRepository, IFolderRepository folderRepository, ISendRepository sendRepository,
IEmergencyAccessRepository emergencyAccessRepository, IEmergencyAccessRepository emergencyAccessRepository,
IPushNotificationService pushService, IdentityErrorDescriber errors) IPushNotificationService pushService, IdentityErrorDescriber errors)
{ {
@ -26,6 +40,7 @@ public class RotateUserKeyCommand : IRotateUserKeyCommand
_userRepository = userRepository; _userRepository = userRepository;
_cipherRepository = cipherRepository; _cipherRepository = cipherRepository;
_folderRepository = folderRepository; _folderRepository = folderRepository;
_sendRepository = sendRepository;
_emergencyAccessRepository = emergencyAccessRepository; _emergencyAccessRepository = emergencyAccessRepository;
_pushService = pushService; _pushService = pushService;
_identityErrorDescriber = errors; _identityErrorDescriber = errors;
@ -64,6 +79,12 @@ public class RotateUserKeyCommand : IRotateUserKeyCommand
{ {
saveEncryptedDataActions.Add(_folderRepository.UpdateForKeyRotation(user.Id, model.Folders)); saveEncryptedDataActions.Add(_folderRepository.UpdateForKeyRotation(user.Id, model.Folders));
} }
if (model.Sends.Any())
{
saveEncryptedDataActions.Add(_sendRepository.UpdateForKeyRotation(user.Id, model.Sends));
}
if (model.EmergencyAccessKeys.Any()) if (model.EmergencyAccessKeys.Any())
{ {
saveEncryptedDataActions.Add( saveEncryptedDataActions.Add(

View File

@ -1,5 +1,6 @@
#nullable enable #nullable enable
using Bit.Core.Auth.UserFeatures.UserKey;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Tools.Entities; using Bit.Core.Tools.Entities;
@ -33,4 +34,12 @@ public interface ISendRepository : IRepository<Send, Guid>
/// The task's result contains the loaded <see cref="Send"/>s. /// The task's result contains the loaded <see cref="Send"/>s.
/// </returns> /// </returns>
Task<ICollection<Send>> GetManyByDeletionDateAsync(DateTime deletionDateBefore); Task<ICollection<Send>> GetManyByDeletionDateAsync(DateTime deletionDateBefore);
/// <summary>
/// Updates encrypted data for sends during a key rotation
/// </summary>
/// <param name="userId">The user that initiated the key rotation</param>
/// <param name="sends">A list of sends with updated data</param>
UpdateEncryptedDataForKeyRotation UpdateForKeyRotation(Guid userId,
IEnumerable<Send> sends);
} }

View File

@ -0,0 +1,42 @@
using System.Data;
using Bit.Core.Tools.Entities;
namespace Bit.Infrastructure.Dapper.Tools.Helpers;
/// <summary>
/// Dapper helper methods for Sends
/// </summary>
public static class SendHelpers
{
/// <summary>
/// Converts an IEnumerable of Sends to a DataTable
/// </summary>
/// <remarks>Contains a hardcoded list of properties and must be updated with model</remarks>
/// <param name="sends">List of sends</param>
/// <returns>A data table matching the schema of dbo.Send containing one row mapped from the items in <see cref="Send"/>s</returns>
public static DataTable ToDataTable(this IEnumerable<Send> sends)
{
var sendsTable = new DataTable();
var columnData = new List<(string name, Type type, Func<Send, object> getter)>
{
(nameof(Send.Id), typeof(Guid), c => c.Id),
(nameof(Send.UserId), typeof(Guid), c => c.UserId),
(nameof(Send.OrganizationId), typeof(Guid), c => c.OrganizationId),
(nameof(Send.Type), typeof(short), c => c.Type),
(nameof(Send.Data), typeof(string), c => c.Data),
(nameof(Send.Key), typeof(string), c => c.Key),
(nameof(Send.Password), typeof(string), c => c.Password),
(nameof(Send.MaxAccessCount), typeof(int), c => c.MaxAccessCount),
(nameof(Send.AccessCount), typeof(int), c => c.AccessCount),
(nameof(Send.CreationDate), typeof(DateTime), c => c.CreationDate),
(nameof(Send.RevisionDate), typeof(DateTime), c => c.RevisionDate),
(nameof(Send.ExpirationDate), typeof(DateTime), c => c.ExpirationDate),
(nameof(Send.DeletionDate), typeof(DateTime), c => c.DeletionDate),
(nameof(Send.Disabled), typeof(bool), c => c.Disabled),
(nameof(Send.HideEmail), typeof(bool), c => c.HideEmail),
};
return sends.BuildTable(sendsTable, columnData);
}
}

View File

@ -1,10 +1,12 @@
#nullable enable #nullable enable
using System.Data; using System.Data;
using Bit.Core.Auth.UserFeatures.UserKey;
using Bit.Core.Settings; using Bit.Core.Settings;
using Bit.Core.Tools.Entities; using Bit.Core.Tools.Entities;
using Bit.Core.Tools.Repositories; using Bit.Core.Tools.Repositories;
using Bit.Infrastructure.Dapper.Repositories; using Bit.Infrastructure.Dapper.Repositories;
using Bit.Infrastructure.Dapper.Tools.Helpers;
using Dapper; using Dapper;
using Microsoft.Data.SqlClient; using Microsoft.Data.SqlClient;
@ -48,4 +50,57 @@ public class SendRepository : Repository<Send, Guid>, ISendRepository
return results.ToList(); return results.ToList();
} }
} }
/// <inheritdoc />
public UpdateEncryptedDataForKeyRotation UpdateForKeyRotation(Guid userId, IEnumerable<Send> sends)
{
return async (connection, transaction) =>
{
// Create temp table
var sqlCreateTemp = @"
SELECT TOP 0 *
INTO #TempSend
FROM [dbo].[Send]";
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 = "#TempSend";
var sendsTable = sends.ToDataTable();
foreach (DataColumn col in sendsTable.Columns)
{
bulkCopy.ColumnMappings.Add(col.ColumnName, col.ColumnName);
}
sendsTable.PrimaryKey = new DataColumn[] { sendsTable.Columns[0] };
await bulkCopy.WriteToServerAsync(sendsTable);
}
// Update send table from temp table
var sql = @"
UPDATE
[dbo].[Send]
SET
[Key] = TS.[Key],
[RevisionDate] = TS.[RevisionDate]
FROM
[dbo].[Send] S
INNER JOIN
#TempSend TS ON S.Id = TS.Id
WHERE
S.[UserId] = @UserId
DROP TABLE #TempSend";
await using (var cmd = new SqlCommand(sql, connection, transaction))
{
cmd.Parameters.Add("@UserId", SqlDbType.UniqueIdentifier).Value = userId;
cmd.ExecuteNonQuery();
}
};
}
} }

View File

@ -1,6 +1,7 @@
#nullable enable #nullable enable
using AutoMapper; using AutoMapper;
using Bit.Core.Auth.UserFeatures.UserKey;
using Bit.Core.Tools.Repositories; using Bit.Core.Tools.Repositories;
using Bit.Infrastructure.EntityFramework.Models; using Bit.Infrastructure.EntityFramework.Models;
using Bit.Infrastructure.EntityFramework.Repositories; using Bit.Infrastructure.EntityFramework.Repositories;
@ -69,4 +70,28 @@ public class SendRepository : Repository<Core.Tools.Entities.Send, Send, Guid>,
return Mapper.Map<List<Core.Tools.Entities.Send>>(results); return Mapper.Map<List<Core.Tools.Entities.Send>>(results);
} }
} }
/// <inheritdoc />
public UpdateEncryptedDataForKeyRotation UpdateForKeyRotation(Guid userId,
IEnumerable<Core.Tools.Entities.Send> sends)
{
return async (_, _) =>
{
var newSends = sends.ToDictionary(s => s.Id);
using var scope = ServiceScopeFactory.CreateScope();
var dbContext = GetDatabaseContext(scope);
var userSends = await GetDbSet(dbContext)
.Where(s => s.UserId == userId)
.ToListAsync();
var validSends = userSends
.Where(send => newSends.ContainsKey(send.Id));
foreach (var send in validSends)
{
send.Key = newSends[send.Id].Key;
}
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;
using Bit.Api.Auth.Models.Request.Accounts; using Bit.Api.Auth.Models.Request.Accounts;
using Bit.Api.Auth.Validators; using Bit.Api.Auth.Validators;
using Bit.Api.Tools.Models.Request;
using Bit.Api.Vault.Models.Request; using Bit.Api.Vault.Models.Request;
using Bit.Core; using Bit.Core;
using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Repositories;
@ -20,6 +21,7 @@ using Bit.Core.Models.Data;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Services; using Bit.Core.Services;
using Bit.Core.Settings; using Bit.Core.Settings;
using Bit.Core.Tools.Entities;
using Bit.Core.Tools.Repositories; using Bit.Core.Tools.Repositories;
using Bit.Core.Tools.Services; using Bit.Core.Tools.Services;
using Bit.Core.Vault.Entities; using Bit.Core.Vault.Entities;
@ -53,9 +55,9 @@ public class AccountsControllerTests : IDisposable
private readonly IFeatureService _featureService; private readonly IFeatureService _featureService;
private readonly ICurrentContext _currentContext; private readonly ICurrentContext _currentContext;
private readonly IRotationValidator<IEnumerable<CipherWithIdRequestModel>, IEnumerable<Cipher>> _cipherValidator; private readonly IRotationValidator<IEnumerable<CipherWithIdRequestModel>, IEnumerable<Cipher>> _cipherValidator;
private readonly IRotationValidator<IEnumerable<FolderWithIdRequestModel>, IEnumerable<Folder>> _folderValidator; private readonly IRotationValidator<IEnumerable<FolderWithIdRequestModel>, IEnumerable<Folder>> _folderValidator;
private readonly IRotationValidator<IEnumerable<SendWithIdRequestModel>, IReadOnlyList<Send>> _sendValidator;
private readonly IRotationValidator<IEnumerable<EmergencyAccessWithIdRequestModel>, IEnumerable<EmergencyAccess>> private readonly IRotationValidator<IEnumerable<EmergencyAccessWithIdRequestModel>, IEnumerable<EmergencyAccess>>
_emergencyAccessValidator; _emergencyAccessValidator;
@ -83,6 +85,7 @@ public class AccountsControllerTests : IDisposable
Substitute.For<IRotationValidator<IEnumerable<CipherWithIdRequestModel>, IEnumerable<Cipher>>>(); Substitute.For<IRotationValidator<IEnumerable<CipherWithIdRequestModel>, IEnumerable<Cipher>>>();
_folderValidator = _folderValidator =
Substitute.For<IRotationValidator<IEnumerable<FolderWithIdRequestModel>, IEnumerable<Folder>>>(); Substitute.For<IRotationValidator<IEnumerable<FolderWithIdRequestModel>, IEnumerable<Folder>>>();
_sendValidator = Substitute.For<IRotationValidator<IEnumerable<SendWithIdRequestModel>, IReadOnlyList<Send>>>();
_emergencyAccessValidator = Substitute.For<IRotationValidator<IEnumerable<EmergencyAccessWithIdRequestModel>, _emergencyAccessValidator = Substitute.For<IRotationValidator<IEnumerable<EmergencyAccessWithIdRequestModel>,
IEnumerable<EmergencyAccess>>>(); IEnumerable<EmergencyAccess>>>();
@ -106,6 +109,7 @@ public class AccountsControllerTests : IDisposable
_currentContext, _currentContext,
_cipherValidator, _cipherValidator,
_folderValidator, _folderValidator,
_sendValidator,
_emergencyAccessValidator _emergencyAccessValidator
); );
} }

View File

@ -0,0 +1,184 @@
using System.Text.Json;
using Bit.Api.Tools.Models;
using Bit.Api.Tools.Models.Request;
using Bit.Api.Tools.Validators;
using Bit.Core.Entities;
using Bit.Core.Exceptions;
using Bit.Core.Tools.Entities;
using Bit.Core.Tools.Enums;
using Bit.Core.Tools.Models.Data;
using Bit.Core.Tools.Repositories;
using Bit.Core.Tools.Services;
using Bit.Core.Utilities;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using Xunit;
namespace Bit.Api.Test.Tools.Validators;
[SutProviderCustomize]
public class SendRotationValidatorTests
{
[Fact]
public async Task ValidateAsync_Success()
{
// Arrange
var sendService = Substitute.For<ISendService>();
var sendRepository = Substitute.For<ISendRepository>();
var sut = new SendRotationValidator(
sendService,
sendRepository
);
var user = new User { Id = new Guid() };
var sends = CreateInputSendRequests();
sendRepository.GetManyByUserIdAsync(user.Id).Returns(MockUserSends(user));
// Act
var result = await sut.ValidateAsync(user, sends);
// Assert
var sendIds = new Guid[]
{
new("72e9ac6d-05f4-4227-ae0d-8a5207623a1a"), new("6b55836c-9280-4589-8762-01b0d8172c97"),
new("9a65bbfb-8138-4aa5-a572-e5c0a41b540e"),
};
Assert.All(result, c => Assert.Contains(c.Id, sendIds));
}
[Fact]
public async Task ValidateAsync_SendNotReturnedFromRepository_NotIncludedInOutput()
{
// Arrange
var sendService = Substitute.For<ISendService>();
var sendRepository = Substitute.For<ISendRepository>();
var sut = new SendRotationValidator(
sendService,
sendRepository
);
var user = new User { Id = new Guid() };
var sends = CreateInputSendRequests();
var userSends = MockUserSends(user);
userSends.RemoveAll(c => c.Id == new Guid("72e9ac6d-05f4-4227-ae0d-8a5207623a1a"));
sendRepository.GetManyByUserIdAsync(user.Id).Returns(userSends);
var result = await sut.ValidateAsync(user, sends);
Assert.DoesNotContain(result, c => c.Id == new Guid("72e9ac6d-05f4-4227-ae0d-8a5207623a1a"));
}
[Fact]
public async Task ValidateAsync_InputMissingUserSend_Throws()
{
// Arrange
var sendService = Substitute.For<ISendService>();
var sendRepository = Substitute.For<ISendRepository>();
var sut = new SendRotationValidator(
sendService,
sendRepository
);
var user = new User { Id = new Guid() };
var sends = CreateInputSendRequests();
var userSends = MockUserSends(user);
userSends.Add(new Send { Id = new Guid(), Data = "{}" });
sendRepository.GetManyByUserIdAsync(user.Id).Returns(userSends);
// Act, Assert
await Assert.ThrowsAsync<BadRequestException>(async () =>
await sut.ValidateAsync(user, sends));
}
private IEnumerable<SendWithIdRequestModel> CreateInputSendRequests()
{
return new[]
{
new SendWithIdRequestModel
{
DeletionDate = new DateTime(2080, 12, 31),
Disabled = false,
Id = new Guid("72e9ac6d-05f4-4227-ae0d-8a5207623a1a"),
Key = "Send1Key",
Name = "Send 1",
Type = SendType.Text,
Text = new SendTextModel(new SendTextData("Text name", "Notes", "Encrypted text for Send 1", false))
},
new SendWithIdRequestModel
{
DeletionDate = new DateTime(2080, 12, 31),
Disabled = true,
Id = new Guid("6b55836c-9280-4589-8762-01b0d8172c97"),
Key = "Send2Key",
Name = "Send 2",
Type = SendType.Text,
Text = new SendTextModel(new SendTextData("Text name", "Notes", "Encrypted text for Send 2",
false)),
},
new SendWithIdRequestModel
{
DeletionDate = new DateTime(2080, 12, 31),
Disabled = false,
Id = new Guid("9a65bbfb-8138-4aa5-a572-e5c0a41b540e"),
Key = "Send3Key",
Name = "Send 3",
Type = SendType.File,
File = new SendFileModel(new SendFileData("File name", "Notes", "File name here")),
HideEmail = true
}
};
}
private List<Send> MockUserSends(User user)
{
return new List<Send>(new[]
{
new Send
{
DeletionDate = new DateTime(2080, 12, 31),
Disabled = false,
Id = new Guid("72e9ac6d-05f4-4227-ae0d-8a5207623a1a"),
UserId = user.Id,
Key = "Send1Key",
Type = SendType.Text,
Data = JsonSerializer.Serialize(
new SendTextModel(new SendTextData("Text name", "Notes", "Encrypted text for Send 1", false)),
JsonHelpers.IgnoreWritingNull),
},
new Send
{
DeletionDate = new DateTime(2080, 12, 31),
Disabled = true,
Id = new Guid("6b55836c-9280-4589-8762-01b0d8172c97"),
UserId = user.Id,
Key = "Send2Key",
Type = SendType.Text,
Data = JsonSerializer.Serialize(
new SendTextModel(new SendTextData("Text name", "Notes", "Encrypted text for Send 2",
false)),
JsonHelpers.IgnoreWritingNull),
},
new Send
{
DeletionDate = new DateTime(2080, 12, 31),
Disabled = false,
Id = new Guid("9a65bbfb-8138-4aa5-a572-e5c0a41b540e"),
UserId = user.Id,
Key = "Send3Key",
Type = SendType.File,
Data = JsonSerializer.Serialize(
new SendFileModel(new SendFileData("File name", "Notes", "File name here")),
JsonHelpers.IgnoreWritingNull),
HideEmail = true
}
});
}
}