mirror of
https://github.com/bitwarden/server.git
synced 2024-11-25 12:45:18 +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:
parent
dbf8907bfc
commit
4b2bd6cee6
@ -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>(),
|
||||
|
@ -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);
|
||||
|
56
src/Api/Vault/Validators/CipherRotationValidator.cs
Normal file
56
src/Api/Vault/Validators/CipherRotationValidator.cs
Normal 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;
|
||||
}
|
||||
}
|
44
src/Api/Vault/Validators/FolderRotationValidator.cs
Normal file
44
src/Api/Vault/Validators/FolderRotationValidator.cs
Normal 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;
|
||||
}
|
||||
}
|
@ -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(
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
33
src/Infrastructure.Dapper/Vault/Helpers/CipherHelpers.cs
Normal file
33
src/Infrastructure.Dapper/Vault/Helpers/CipherHelpers.cs
Normal 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);
|
||||
}
|
||||
}
|
25
src/Infrastructure.Dapper/Vault/Helpers/FolderHelpers.cs
Normal file
25
src/Infrastructure.Dapper/Vault/Helpers/FolderHelpers.cs
Normal 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);
|
||||
}
|
||||
}
|
@ -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))
|
||||
|
@ -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();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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)
|
||||
{
|
||||
|
@ -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())
|
||||
|
@ -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();
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
);
|
||||
}
|
||||
|
@ -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
Block a user