diff --git a/src/Admin/Controllers/UsersController.cs b/src/Admin/Controllers/UsersController.cs index 30565ca3d9..e4ff88a68e 100644 --- a/src/Admin/Controllers/UsersController.cs +++ b/src/Admin/Controllers/UsersController.cs @@ -2,6 +2,8 @@ using Bit.Admin.Models; using Bit.Admin.Services; using Bit.Admin.Utilities; +using Bit.Core; +using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Repositories; using Bit.Core.Services; @@ -21,19 +23,28 @@ public class UsersController : Controller private readonly IPaymentService _paymentService; private readonly GlobalSettings _globalSettings; private readonly IAccessControlService _accessControlService; + private readonly ICurrentContext _currentContext; + private readonly IFeatureService _featureService; + + private bool UseFlexibleCollections => + _featureService.IsEnabled(FeatureFlagKeys.FlexibleCollections, _currentContext); public UsersController( IUserRepository userRepository, ICipherRepository cipherRepository, IPaymentService paymentService, GlobalSettings globalSettings, - IAccessControlService accessControlService) + IAccessControlService accessControlService, + ICurrentContext currentContext, + IFeatureService featureService) { _userRepository = userRepository; _cipherRepository = cipherRepository; _paymentService = paymentService; _globalSettings = globalSettings; _accessControlService = accessControlService; + _currentContext = currentContext; + _featureService = featureService; } [RequirePermission(Permission.User_List_View)] @@ -69,7 +80,7 @@ public class UsersController : Controller return RedirectToAction("Index"); } - var ciphers = await _cipherRepository.GetManyByUserIdAsync(id); + var ciphers = await _cipherRepository.GetManyByUserIdAsync(id, useFlexibleCollections: UseFlexibleCollections); return View(new UserViewModel(user, ciphers)); } @@ -82,7 +93,7 @@ public class UsersController : Controller return RedirectToAction("Index"); } - var ciphers = await _cipherRepository.GetManyByUserIdAsync(id); + var ciphers = await _cipherRepository.GetManyByUserIdAsync(id, useFlexibleCollections: UseFlexibleCollections); var billingInfo = await _paymentService.GetBillingAsync(user); return View(new UserEditModel(user, ciphers, billingInfo, _globalSettings)); } diff --git a/src/Api/Controllers/AccountsController.cs b/src/Api/Controllers/AccountsController.cs index d699f09ec6..7217afd0bd 100644 --- a/src/Api/Controllers/AccountsController.cs +++ b/src/Api/Controllers/AccountsController.cs @@ -58,6 +58,8 @@ public class AccountsController : Controller private readonly IFeatureService _featureService; private readonly ICurrentContext _currentContext; + private bool UseFlexibleCollections => + _featureService.IsEnabled(FeatureFlagKeys.FlexibleCollections, _currentContext); public AccountsController( GlobalSettings globalSettings, @@ -415,7 +417,7 @@ public class AccountsController : Controller var ciphers = new List(); if (model.Ciphers.Any()) { - var existingCiphers = await _cipherRepository.GetManyByUserIdAsync(user.Id); + var existingCiphers = await _cipherRepository.GetManyByUserIdAsync(user.Id, useFlexibleCollections: UseFlexibleCollections); ciphers.AddRange(existingCiphers .Join(model.Ciphers, c => c.Id, c => c.Id, (existing, c) => c.ToCipher(existing))); } diff --git a/src/Api/Vault/Controllers/CiphersController.cs b/src/Api/Vault/Controllers/CiphersController.cs index ad0db8f6b3..7ce9709885 100644 --- a/src/Api/Vault/Controllers/CiphersController.cs +++ b/src/Api/Vault/Controllers/CiphersController.cs @@ -40,6 +40,10 @@ public class CiphersController : Controller private readonly ILogger _logger; private readonly GlobalSettings _globalSettings; private readonly Version _cipherKeyEncryptionMinimumVersion = new Version(Constants.CipherKeyEncryptionMinimumVersion); + private readonly IFeatureService _featureService; + + private bool UseFlexibleCollections => + _featureService.IsEnabled(FeatureFlagKeys.FlexibleCollections, _currentContext); public CiphersController( ICipherRepository cipherRepository, @@ -50,7 +54,8 @@ public class CiphersController : Controller IProviderService providerService, ICurrentContext currentContext, ILogger logger, - GlobalSettings globalSettings) + GlobalSettings globalSettings, + IFeatureService featureService) { _cipherRepository = cipherRepository; _collectionCipherRepository = collectionCipherRepository; @@ -61,13 +66,14 @@ public class CiphersController : Controller _currentContext = currentContext; _logger = logger; _globalSettings = globalSettings; + _featureService = featureService; } [HttpGet("{id}")] - public async Task Get(string id) + public async Task Get(Guid id) { var userId = _userService.GetProperUserId(User).Value; - var cipher = await _cipherRepository.GetByIdAsync(new Guid(id), userId); + var cipher = await GetByIdAsync(id, userId); if (cipher == null) { throw new NotFoundException(); @@ -91,17 +97,16 @@ public class CiphersController : Controller [HttpGet("{id}/full-details")] [HttpGet("{id}/details")] - public async Task GetDetails(string id) + public async Task GetDetails(Guid id) { var userId = _userService.GetProperUserId(User).Value; - var cipherId = new Guid(id); - var cipher = await _cipherRepository.GetByIdAsync(cipherId, userId); + var cipher = await GetByIdAsync(id, userId); if (cipher == null) { throw new NotFoundException(); } - var collectionCiphers = await _collectionCipherRepository.GetManyByUserIdCipherIdAsync(userId, cipherId); + var collectionCiphers = await _collectionCipherRepository.GetManyByUserIdCipherIdAsync(userId, id); return new CipherDetailsResponseModel(cipher, _globalSettings, collectionCiphers); } @@ -111,7 +116,7 @@ public class CiphersController : Controller var userId = _userService.GetProperUserId(User).Value; var hasOrgs = _currentContext.Organizations?.Any() ?? false; // TODO: Use hasOrgs proper for cipher listing here? - var ciphers = await _cipherRepository.GetManyByUserIdAsync(userId, true || hasOrgs); + var ciphers = await _cipherRepository.GetManyByUserIdAsync(userId, useFlexibleCollections: UseFlexibleCollections, withOrganizations: true || hasOrgs); Dictionary> collectionCiphersGroupDict = null; if (hasOrgs) { @@ -175,7 +180,7 @@ public class CiphersController : Controller public async Task Put(Guid id, [FromBody] CipherRequestModel model) { var userId = _userService.GetProperUserId(User).Value; - var cipher = await _cipherRepository.GetByIdAsync(id, userId); + var cipher = await GetByIdAsync(id, userId); if (cipher == null) { throw new NotFoundException(); @@ -247,25 +252,23 @@ public class CiphersController : Controller [HttpPut("{id}/partial")] [HttpPost("{id}/partial")] - public async Task PutPartial(string id, [FromBody] CipherPartialRequestModel model) + public async Task PutPartial(Guid id, [FromBody] CipherPartialRequestModel model) { var userId = _userService.GetProperUserId(User).Value; var folderId = string.IsNullOrWhiteSpace(model.FolderId) ? null : (Guid?)new Guid(model.FolderId); - var cipherId = new Guid(id); - await _cipherRepository.UpdatePartialAsync(cipherId, userId, folderId, model.Favorite); + await _cipherRepository.UpdatePartialAsync(id, userId, folderId, model.Favorite); - var cipher = await _cipherRepository.GetByIdAsync(cipherId, userId); + var cipher = await GetByIdAsync(id, userId); var response = new CipherResponseModel(cipher, _globalSettings); return response; } [HttpPut("{id}/share")] [HttpPost("{id}/share")] - public async Task PutShare(string id, [FromBody] CipherShareRequestModel model) + public async Task PutShare(Guid id, [FromBody] CipherShareRequestModel model) { var userId = _userService.GetProperUserId(User).Value; - var cipherId = new Guid(id); - var cipher = await _cipherRepository.GetByIdAsync(cipherId); + var cipher = await _cipherRepository.GetByIdAsync(id); if (cipher == null || cipher.UserId != userId || !await _currentContext.OrganizationUser(new Guid(model.Cipher.OrganizationId))) { @@ -279,17 +282,17 @@ public class CiphersController : Controller await _cipherService.ShareAsync(original, model.Cipher.ToCipher(cipher), new Guid(model.Cipher.OrganizationId), model.CollectionIds.Select(c => new Guid(c)), userId, model.Cipher.LastKnownRevisionDate); - var sharedCipher = await _cipherRepository.GetByIdAsync(cipherId, userId); + var sharedCipher = await GetByIdAsync(id, userId); var response = new CipherResponseModel(sharedCipher, _globalSettings); return response; } [HttpPut("{id}/collections")] [HttpPost("{id}/collections")] - public async Task PutCollections(string id, [FromBody] CipherCollectionsRequestModel model) + public async Task PutCollections(Guid id, [FromBody] CipherCollectionsRequestModel model) { var userId = _userService.GetProperUserId(User).Value; - var cipher = await _cipherRepository.GetByIdAsync(new Guid(id), userId); + var cipher = await GetByIdAsync(id, userId); if (cipher == null || !cipher.OrganizationId.HasValue || !await _currentContext.OrganizationUser(cipher.OrganizationId.Value)) { @@ -318,10 +321,10 @@ public class CiphersController : Controller [HttpDelete("{id}")] [HttpPost("{id}/delete")] - public async Task Delete(string id) + public async Task Delete(Guid id) { var userId = _userService.GetProperUserId(User).Value; - var cipher = await _cipherRepository.GetByIdAsync(new Guid(id), userId); + var cipher = await GetByIdAsync(id, userId); if (cipher == null) { throw new NotFoundException(); @@ -380,10 +383,10 @@ public class CiphersController : Controller } [HttpPut("{id}/delete")] - public async Task PutDelete(string id) + public async Task PutDelete(Guid id) { var userId = _userService.GetProperUserId(User).Value; - var cipher = await _cipherRepository.GetByIdAsync(new Guid(id), userId); + var cipher = await GetByIdAsync(id, userId); if (cipher == null) { throw new NotFoundException(); @@ -436,10 +439,10 @@ public class CiphersController : Controller } [HttpPut("{id}/restore")] - public async Task PutRestore(string id) + public async Task PutRestore(Guid id) { var userId = _userService.GetProperUserId(User).Value; - var cipher = await _cipherRepository.GetByIdAsync(new Guid(id), userId); + var cipher = await GetByIdAsync(id, userId); if (cipher == null) { throw new NotFoundException(); @@ -526,7 +529,7 @@ public class CiphersController : Controller } var userId = _userService.GetProperUserId(User).Value; - var ciphers = await _cipherRepository.GetManyByUserIdAsync(userId, false); + var ciphers = await _cipherRepository.GetManyByUserIdAsync(userId, useFlexibleCollections: UseFlexibleCollections, withOrganizations: false); var ciphersDict = ciphers.ToDictionary(c => c.Id); var shareCiphers = new List<(Cipher, DateTime?)>(); @@ -581,13 +584,12 @@ public class CiphersController : Controller } [HttpPost("{id}/attachment/v2")] - public async Task PostAttachment(string id, [FromBody] AttachmentRequestModel request) + public async Task PostAttachment(Guid id, [FromBody] AttachmentRequestModel request) { - var idGuid = new Guid(id); var userId = _userService.GetProperUserId(User).Value; var cipher = request.AdminRequest ? - await _cipherRepository.GetOrganizationDetailsByIdAsync(idGuid) : - await _cipherRepository.GetByIdAsync(idGuid, userId); + await _cipherRepository.GetOrganizationDetailsByIdAsync(id) : + await GetByIdAsync(id, userId); if (cipher == null || (request.AdminRequest && (!cipher.OrganizationId.HasValue || !await _currentContext.EditAnyCollection(cipher.OrganizationId.Value)))) @@ -615,11 +617,10 @@ public class CiphersController : Controller } [HttpGet("{id}/attachment/{attachmentId}/renew")] - public async Task RenewFileUploadUrl(string id, string attachmentId) + public async Task RenewFileUploadUrl(Guid id, string attachmentId) { var userId = _userService.GetProperUserId(User).Value; - var cipherId = new Guid(id); - var cipher = await _cipherRepository.GetByIdAsync(cipherId, userId); + var cipher = await GetByIdAsync(id, userId); var attachments = cipher?.GetAttachments(); if (attachments == null || !attachments.ContainsKey(attachmentId) || attachments[attachmentId].Validated) @@ -638,7 +639,7 @@ public class CiphersController : Controller [SelfHosted(SelfHostedOnly = true)] [RequestSizeLimit(Constants.FileSize501mb)] [DisableFormValueModelBinding] - public async Task PostFileForExistingAttachment(string id, string attachmentId) + public async Task PostFileForExistingAttachment(Guid id, string attachmentId) { if (!Request?.ContentType.Contains("multipart/") ?? true) { @@ -646,7 +647,7 @@ public class CiphersController : Controller } var userId = _userService.GetProperUserId(User).Value; - var cipher = await _cipherRepository.GetByIdAsync(new Guid(id), userId); + var cipher = await GetByIdAsync(id, userId); var attachments = cipher?.GetAttachments(); if (attachments == null || !attachments.ContainsKey(attachmentId)) { @@ -664,13 +665,12 @@ public class CiphersController : Controller [Obsolete("Deprecated Attachments API", false)] [RequestSizeLimit(Constants.FileSize101mb)] [DisableFormValueModelBinding] - public async Task PostAttachment(string id) + public async Task PostAttachment(Guid id) { ValidateAttachment(); - var idGuid = new Guid(id); var userId = _userService.GetProperUserId(User).Value; - var cipher = await _cipherRepository.GetByIdAsync(idGuid, userId); + var cipher = await GetByIdAsync(id, userId); if (cipher == null) { throw new NotFoundException(); @@ -711,10 +711,10 @@ public class CiphersController : Controller } [HttpGet("{id}/attachment/{attachmentId}")] - public async Task GetAttachmentData(string id, string attachmentId) + public async Task GetAttachmentData(Guid id, string attachmentId) { var userId = _userService.GetProperUserId(User).Value; - var cipher = await _cipherRepository.GetByIdAsync(new Guid(id), userId); + var cipher = await GetByIdAsync(id, userId); var result = await _cipherService.GetAttachmentDownloadDataAsync(cipher, attachmentId); return new AttachmentResponseModel(result); } @@ -742,11 +742,10 @@ public class CiphersController : Controller [HttpDelete("{id}/attachment/{attachmentId}")] [HttpPost("{id}/attachment/{attachmentId}/delete")] - public async Task DeleteAttachment(string id, string attachmentId) + public async Task DeleteAttachment(Guid id, string attachmentId) { - var idGuid = new Guid(id); var userId = _userService.GetProperUserId(User).Value; - var cipher = await _cipherRepository.GetByIdAsync(idGuid, userId); + var cipher = await GetByIdAsync(id, userId); if (cipher == null) { throw new NotFoundException(); @@ -836,4 +835,9 @@ public class CiphersController : Controller } } } + + private async Task GetByIdAsync(Guid cipherId, Guid userId) + { + return await _cipherRepository.GetByIdAsync(cipherId, userId, UseFlexibleCollections); + } } diff --git a/src/Api/Vault/Controllers/SyncController.cs b/src/Api/Vault/Controllers/SyncController.cs index 0d75235111..9d8cc5d576 100644 --- a/src/Api/Vault/Controllers/SyncController.cs +++ b/src/Api/Vault/Controllers/SyncController.cs @@ -1,7 +1,9 @@ using Bit.Api.Vault.Models.Response; +using Bit.Core; using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.AdminConsole.Repositories; +using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Exceptions; @@ -30,6 +32,11 @@ public class SyncController : Controller private readonly IPolicyRepository _policyRepository; private readonly ISendRepository _sendRepository; private readonly GlobalSettings _globalSettings; + private readonly ICurrentContext _currentContext; + private readonly IFeatureService _featureService; + + private bool UseFlexibleCollections => + _featureService.IsEnabled(FeatureFlagKeys.FlexibleCollections, _currentContext); public SyncController( IUserService userService, @@ -41,7 +48,9 @@ public class SyncController : Controller IProviderUserRepository providerUserRepository, IPolicyRepository policyRepository, ISendRepository sendRepository, - GlobalSettings globalSettings) + GlobalSettings globalSettings, + ICurrentContext currentContext, + IFeatureService featureService) { _userService = userService; _folderRepository = folderRepository; @@ -53,6 +62,8 @@ public class SyncController : Controller _policyRepository = policyRepository; _sendRepository = sendRepository; _globalSettings = globalSettings; + _currentContext = currentContext; + _featureService = featureService; } [HttpGet("")] @@ -74,7 +85,7 @@ public class SyncController : Controller var hasEnabledOrgs = organizationUserDetails.Any(o => o.Enabled); var folders = await _folderRepository.GetManyByUserIdAsync(user.Id); - var ciphers = await _cipherRepository.GetManyByUserIdAsync(user.Id, hasEnabledOrgs); + var ciphers = await _cipherRepository.GetManyByUserIdAsync(user.Id, useFlexibleCollections: UseFlexibleCollections, withOrganizations: hasEnabledOrgs); var sends = await _sendRepository.GetManyByUserIdAsync(user.Id); IEnumerable collections = null; diff --git a/src/Core/Auth/Services/Implementations/EmergencyAccessService.cs b/src/Core/Auth/Services/Implementations/EmergencyAccessService.cs index 98db32bbdd..34c0e8e0e3 100644 --- a/src/Core/Auth/Services/Implementations/EmergencyAccessService.cs +++ b/src/Core/Auth/Services/Implementations/EmergencyAccessService.cs @@ -5,6 +5,7 @@ using Bit.Core.Auth.Enums; using Bit.Core.Auth.Models; using Bit.Core.Auth.Models.Business.Tokenables; using Bit.Core.Auth.Models.Data; +using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Exceptions; @@ -33,6 +34,11 @@ public class EmergencyAccessService : IEmergencyAccessService private readonly IPasswordHasher _passwordHasher; private readonly IOrganizationService _organizationService; private readonly IDataProtectorTokenFactory _dataProtectorTokenizer; + private readonly ICurrentContext _currentContext; + private readonly IFeatureService _featureService; + + private bool UseFlexibleCollections => + _featureService.IsEnabled(FeatureFlagKeys.FlexibleCollections, _currentContext); public EmergencyAccessService( IEmergencyAccessRepository emergencyAccessRepository, @@ -46,7 +52,9 @@ public class EmergencyAccessService : IEmergencyAccessService IPasswordHasher passwordHasher, GlobalSettings globalSettings, IOrganizationService organizationService, - IDataProtectorTokenFactory dataProtectorTokenizer) + IDataProtectorTokenFactory dataProtectorTokenizer, + ICurrentContext currentContext, + IFeatureService featureService) { _emergencyAccessRepository = emergencyAccessRepository; _organizationUserRepository = organizationUserRepository; @@ -60,6 +68,8 @@ public class EmergencyAccessService : IEmergencyAccessService _globalSettings = globalSettings; _organizationService = organizationService; _dataProtectorTokenizer = dataProtectorTokenizer; + _currentContext = currentContext; + _featureService = featureService; } public async Task InviteAsync(User invitingUser, string email, EmergencyAccessType type, int waitTime) @@ -387,7 +397,7 @@ public class EmergencyAccessService : IEmergencyAccessService throw new BadRequestException("Emergency Access not valid."); } - var ciphers = await _cipherRepository.GetManyByUserIdAsync(emergencyAccess.GrantorId, false); + var ciphers = await _cipherRepository.GetManyByUserIdAsync(emergencyAccess.GrantorId, useFlexibleCollections: UseFlexibleCollections, withOrganizations: false); return new EmergencyAccessViewData { @@ -405,7 +415,7 @@ public class EmergencyAccessService : IEmergencyAccessService throw new BadRequestException("Emergency Access not valid."); } - var cipher = await _cipherRepository.GetByIdAsync(cipherId, emergencyAccess.GrantorId); + var cipher = await _cipherRepository.GetByIdAsync(cipherId, emergencyAccess.GrantorId, UseFlexibleCollections); return await _cipherService.GetAttachmentDownloadDataAsync(cipher, attachmentId); } diff --git a/src/Core/Vault/Repositories/ICipherRepository.cs b/src/Core/Vault/Repositories/ICipherRepository.cs index 0ba80857d6..811e48a17b 100644 --- a/src/Core/Vault/Repositories/ICipherRepository.cs +++ b/src/Core/Vault/Repositories/ICipherRepository.cs @@ -8,11 +8,11 @@ namespace Bit.Core.Vault.Repositories; public interface ICipherRepository : IRepository { - Task GetByIdAsync(Guid id, Guid userId); + Task GetByIdAsync(Guid id, Guid userId, bool useFlexibleCollections); Task GetOrganizationDetailsByIdAsync(Guid id); Task> GetManyOrganizationDetailsByOrganizationIdAsync(Guid organizationId); Task GetCanEditByIdAsync(Guid userId, Guid cipherId); - Task> GetManyByUserIdAsync(Guid userId, bool withOrganizations = true); + Task> GetManyByUserIdAsync(Guid userId, bool useFlexibleCollections, bool withOrganizations = true); Task> GetManyByOrganizationIdAsync(Guid organizationId); Task CreateAsync(Cipher cipher, IEnumerable collectionIds); Task CreateAsync(CipherDetails cipher); @@ -23,9 +23,9 @@ public interface ICipherRepository : IRepository Task UpdatePartialAsync(Guid id, Guid userId, Guid? folderId, bool favorite); Task UpdateAttachmentAsync(CipherAttachment attachment); Task DeleteAttachmentAsync(Guid cipherId, string attachmentId); - Task DeleteAsync(IEnumerable ids, Guid userId); + Task DeleteAsync(IEnumerable ids, Guid userId, bool useFlexibleCollections); Task DeleteByIdsOrganizationIdAsync(IEnumerable ids, Guid organizationId); - Task MoveAsync(IEnumerable ids, Guid? folderId, Guid userId); + Task MoveAsync(IEnumerable ids, Guid? folderId, Guid userId, bool useFlexibleCollections); Task DeleteByUserIdAsync(Guid userId); Task DeleteByOrganizationIdAsync(Guid organizationId); Task UpdateUserKeysAndCiphersAsync(User user, IEnumerable ciphers, IEnumerable folders, IEnumerable sends); @@ -33,9 +33,9 @@ public interface ICipherRepository : IRepository Task CreateAsync(IEnumerable ciphers, IEnumerable folders); Task CreateAsync(IEnumerable ciphers, IEnumerable collections, IEnumerable collectionCiphers, IEnumerable collectionUsers); - Task SoftDeleteAsync(IEnumerable ids, Guid userId); + Task SoftDeleteAsync(IEnumerable ids, Guid userId, bool useFlexibleCollections); Task SoftDeleteByIdsOrganizationIdAsync(IEnumerable ids, Guid organizationId); - Task RestoreAsync(IEnumerable ids, Guid userId); + Task RestoreAsync(IEnumerable ids, Guid userId, bool useFlexibleCollections); Task RestoreByIdsOrganizationIdAsync(IEnumerable ids, Guid organizationId); Task DeleteDeletedAsync(DateTime deletedDateBefore); } diff --git a/src/Core/Vault/Services/Implementations/CipherService.cs b/src/Core/Vault/Services/Implementations/CipherService.cs index 6e5b15de0d..4218910238 100644 --- a/src/Core/Vault/Services/Implementations/CipherService.cs +++ b/src/Core/Vault/Services/Implementations/CipherService.cs @@ -38,6 +38,10 @@ public class CipherService : ICipherService private const long _fileSizeLeeway = 1024L * 1024L; // 1MB private readonly IReferenceEventService _referenceEventService; private readonly ICurrentContext _currentContext; + private readonly IFeatureService _featureService; + + private bool UseFlexibleCollections => + _featureService.IsEnabled(FeatureFlagKeys.FlexibleCollections, _currentContext); public CipherService( ICipherRepository cipherRepository, @@ -54,7 +58,8 @@ public class CipherService : ICipherService IPolicyService policyService, GlobalSettings globalSettings, IReferenceEventService referenceEventService, - ICurrentContext currentContext) + ICurrentContext currentContext, + IFeatureService featureService) { _cipherRepository = cipherRepository; _folderRepository = folderRepository; @@ -71,6 +76,7 @@ public class CipherService : ICipherService _globalSettings = globalSettings; _referenceEventService = referenceEventService; _currentContext = currentContext; + _featureService = featureService; } public async Task SaveAsync(Cipher cipher, Guid savingUserId, DateTime? lastKnownRevisionDate, @@ -424,9 +430,10 @@ public class CipherService : ICipherService } else { - var ciphers = await _cipherRepository.GetManyByUserIdAsync(deletingUserId); + var ciphers = await _cipherRepository.GetManyByUserIdAsync(deletingUserId, useFlexibleCollections: UseFlexibleCollections); deletingCiphers = ciphers.Where(c => cipherIdsSet.Contains(c.Id) && c.Edit).Select(x => (Cipher)x).ToList(); - await _cipherRepository.DeleteAsync(deletingCiphers.Select(c => c.Id), deletingUserId); + + await _cipherRepository.DeleteAsync(deletingCiphers.Select(c => c.Id), deletingUserId, UseFlexibleCollections); } var events = deletingCiphers.Select(c => @@ -478,7 +485,7 @@ public class CipherService : ICipherService } } - await _cipherRepository.MoveAsync(cipherIds, destinationFolderId, movingUserId); + await _cipherRepository.MoveAsync(cipherIds, destinationFolderId, movingUserId, UseFlexibleCollections); // push await _pushService.PushSyncCiphersAsync(movingUserId); } @@ -865,9 +872,10 @@ public class CipherService : ICipherService } else { - var ciphers = await _cipherRepository.GetManyByUserIdAsync(deletingUserId); + var ciphers = await _cipherRepository.GetManyByUserIdAsync(deletingUserId, useFlexibleCollections: UseFlexibleCollections); deletingCiphers = ciphers.Where(c => cipherIdsSet.Contains(c.Id) && c.Edit).Select(x => (Cipher)x).ToList(); - await _cipherRepository.SoftDeleteAsync(deletingCiphers.Select(c => c.Id), deletingUserId); + + await _cipherRepository.SoftDeleteAsync(deletingCiphers.Select(c => c.Id), deletingUserId, UseFlexibleCollections); } var events = deletingCiphers.Select(c => @@ -930,9 +938,10 @@ public class CipherService : ICipherService } else { - var ciphers = await _cipherRepository.GetManyByUserIdAsync(restoringUserId); + var ciphers = await _cipherRepository.GetManyByUserIdAsync(restoringUserId, useFlexibleCollections: UseFlexibleCollections); restoringCiphers = ciphers.Where(c => cipherIdsSet.Contains(c.Id) && c.Edit).Select(c => (CipherOrganizationDetails)c).ToList(); - revisionDate = await _cipherRepository.RestoreAsync(restoringCiphers.Select(c => c.Id), restoringUserId); + + revisionDate = await _cipherRepository.RestoreAsync(restoringCiphers.Select(c => c.Id), restoringUserId, UseFlexibleCollections); } var events = restoringCiphers.Select(c => @@ -967,7 +976,7 @@ public class CipherService : ICipherService } else { - var ciphers = await _cipherRepository.GetManyByUserIdAsync(userId, true); + var ciphers = await _cipherRepository.GetManyByUserIdAsync(userId, useFlexibleCollections: UseFlexibleCollections, withOrganizations: true); orgCiphers = ciphers.Where(c => c.OrganizationId == organizationId); } diff --git a/src/Events/Controllers/CollectController.cs b/src/Events/Controllers/CollectController.cs index a2bef7a7df..7c7962309c 100644 --- a/src/Events/Controllers/CollectController.cs +++ b/src/Events/Controllers/CollectController.cs @@ -1,4 +1,5 @@ -using Bit.Core.Context; +using Bit.Core; +using Bit.Core.Context; using Bit.Core.Enums; using Bit.Core.Repositories; using Bit.Core.Services; @@ -18,19 +19,24 @@ public class CollectController : Controller private readonly IEventService _eventService; private readonly ICipherRepository _cipherRepository; private readonly IOrganizationRepository _organizationRepository; + private readonly IFeatureService _featureService; public CollectController( ICurrentContext currentContext, IEventService eventService, ICipherRepository cipherRepository, - IOrganizationRepository organizationRepository) + IOrganizationRepository organizationRepository, + IFeatureService featureService) { _currentContext = currentContext; _eventService = eventService; _cipherRepository = cipherRepository; _organizationRepository = organizationRepository; + _featureService = featureService; } + bool UseFlexibleCollections => _featureService.IsEnabled(FeatureFlagKeys.FlexibleCollections, _currentContext); + [HttpPost] public async Task Post([FromBody] IEnumerable model) { @@ -69,8 +75,10 @@ public class CollectController : Controller } else { + var useFlexibleCollections = _featureService.IsEnabled(FeatureFlagKeys.FlexibleCollections, _currentContext); cipher = await _cipherRepository.GetByIdAsync(eventModel.CipherId.Value, - _currentContext.UserId.Value); + _currentContext.UserId.Value, + useFlexibleCollections); } if (cipher == null) { diff --git a/src/Infrastructure.Dapper/Vault/Repositories/CipherRepository.cs b/src/Infrastructure.Dapper/Vault/Repositories/CipherRepository.cs index 4f6c8e25f7..b1e4043a99 100644 --- a/src/Infrastructure.Dapper/Vault/Repositories/CipherRepository.cs +++ b/src/Infrastructure.Dapper/Vault/Repositories/CipherRepository.cs @@ -22,12 +22,16 @@ public class CipherRepository : Repository, ICipherRepository : base(connectionString, readOnlyConnectionString) { } - public async Task GetByIdAsync(Guid id, Guid userId) + public async Task GetByIdAsync(Guid id, Guid userId, bool useFlexibleCollections) { + var sprocName = useFlexibleCollections + ? $"[{Schema}].[CipherDetails_ReadByIdUserId_V2]" + : $"[{Schema}].[CipherDetails_ReadByIdUserId]"; + using (var connection = new SqlConnection(ConnectionString)) { var results = await connection.QueryAsync( - $"[{Schema}].[CipherDetails_ReadByIdUserId]", + sprocName, new { Id = id, UserId = userId }, commandType: CommandType.StoredProcedure); @@ -75,12 +79,14 @@ public class CipherRepository : Repository, ICipherRepository } } - public async Task> GetManyByUserIdAsync(Guid userId, bool withOrganizations = true) + public async Task> GetManyByUserIdAsync(Guid userId, bool useFlexibleCollections, bool withOrganizations = true) { string sprocName = null; if (withOrganizations) { - sprocName = $"[{Schema}].[CipherDetails_ReadByUserId]"; + sprocName = useFlexibleCollections + ? $"[{Schema}].[CipherDetails_ReadByUserId_V2]" + : $"[{Schema}].[CipherDetails_ReadByUserId]"; } else { @@ -228,12 +234,16 @@ public class CipherRepository : Repository, ICipherRepository } } - public async Task DeleteAsync(IEnumerable ids, Guid userId) + public async Task DeleteAsync(IEnumerable ids, Guid userId, bool useFlexibleCollections) { + var sprocName = useFlexibleCollections + ? $"[{Schema}].[Cipher_Delete_V2]" + : $"[{Schema}].[Cipher_Delete]"; + using (var connection = new SqlConnection(ConnectionString)) { var results = await connection.ExecuteAsync( - $"[{Schema}].[Cipher_Delete]", + sprocName, new { Ids = ids.ToGuidIdArrayTVP(), UserId = userId }, commandType: CommandType.StoredProcedure); } @@ -261,12 +271,16 @@ public class CipherRepository : Repository, ICipherRepository } } - public async Task MoveAsync(IEnumerable ids, Guid? folderId, Guid userId) + public async Task MoveAsync(IEnumerable ids, Guid? folderId, Guid userId, bool useFlexibleCollections) { + var sprocName = useFlexibleCollections + ? $"[{Schema}].[Cipher_Move_V2]" + : $"[{Schema}].[Cipher_Move]"; + using (var connection = new SqlConnection(ConnectionString)) { var results = await connection.ExecuteAsync( - $"[{Schema}].[Cipher_Move]", + sprocName, new { Ids = ids.ToGuidIdArrayTVP(), FolderId = folderId, UserId = userId }, commandType: CommandType.StoredProcedure); } @@ -657,23 +671,31 @@ public class CipherRepository : Repository, ICipherRepository } } - public async Task SoftDeleteAsync(IEnumerable ids, Guid userId) + public async Task SoftDeleteAsync(IEnumerable ids, Guid userId, bool useFlexibleCollections) { + var sprocName = useFlexibleCollections + ? $"[{Schema}].[Cipher_SoftDelete_V2]" + : $"[{Schema}].[Cipher_SoftDelete]"; + using (var connection = new SqlConnection(ConnectionString)) { var results = await connection.ExecuteAsync( - $"[{Schema}].[Cipher_SoftDelete]", + sprocName, new { Ids = ids.ToGuidIdArrayTVP(), UserId = userId }, commandType: CommandType.StoredProcedure); } } - public async Task RestoreAsync(IEnumerable ids, Guid userId) + public async Task RestoreAsync(IEnumerable ids, Guid userId, bool useFlexibleCollections) { + var sprocName = useFlexibleCollections + ? $"[{Schema}].[Cipher_Restore_V2]" + : $"[{Schema}].[Cipher_Restore]"; + using (var connection = new SqlConnection(ConnectionString)) { var results = await connection.ExecuteScalarAsync( - $"[{Schema}].[Cipher_Restore]", + sprocName, new { Ids = ids.ToGuidIdArrayTVP(), UserId = userId }, commandType: CommandType.StoredProcedure); diff --git a/src/Infrastructure.EntityFramework/Repositories/Queries/UserCipherDetailsQuery.cs b/src/Infrastructure.EntityFramework/Repositories/Queries/UserCipherDetailsQuery.cs index d1f8b0adf7..a5f06fb61c 100644 --- a/src/Infrastructure.EntityFramework/Repositories/Queries/UserCipherDetailsQuery.cs +++ b/src/Infrastructure.EntityFramework/Repositories/Queries/UserCipherDetailsQuery.cs @@ -8,11 +8,21 @@ namespace Bit.Infrastructure.EntityFramework.Repositories.Queries; public class UserCipherDetailsQuery : IQuery { private readonly Guid? _userId; - public UserCipherDetailsQuery(Guid? userId) + private readonly bool _useFlexibleCollections; + public UserCipherDetailsQuery(Guid? userId, bool useFlexibleCollections) { _userId = userId; + _useFlexibleCollections = useFlexibleCollections; } + public virtual IQueryable Run(DatabaseContext dbContext) + { + return _useFlexibleCollections + ? Run_VNext(dbContext) + : Run_VCurrent(dbContext); + } + + private IQueryable Run_VCurrent(DatabaseContext dbContext) { var query = from c in dbContext.Ciphers @@ -78,6 +88,71 @@ public class UserCipherDetailsQuery : IQuery return union; } + private IQueryable Run_VNext(DatabaseContext dbContext) + { + var query = from c in dbContext.Ciphers + + join ou in dbContext.OrganizationUsers + on new { CipherUserId = c.UserId, c.OrganizationId, UserId = _userId, Status = OrganizationUserStatusType.Confirmed } equals + new { CipherUserId = (Guid?)null, OrganizationId = (Guid?)ou.OrganizationId, ou.UserId, ou.Status } + + join o in dbContext.Organizations + on new { c.OrganizationId, OuOrganizationId = ou.OrganizationId, Enabled = true } equals + new { OrganizationId = (Guid?)o.Id, OuOrganizationId = o.Id, o.Enabled } + + join cc in dbContext.CollectionCiphers + on c.Id equals cc.CipherId into cc_g + from cc in cc_g.DefaultIfEmpty() + + join cu in dbContext.CollectionUsers + on new { cc.CollectionId, OrganizationUserId = ou.Id } equals + new { cu.CollectionId, cu.OrganizationUserId } into cu_g + from cu in cu_g.DefaultIfEmpty() + + join gu in dbContext.GroupUsers + on new { CollectionId = (Guid?)cu.CollectionId, OrganizationUserId = ou.Id } equals + new { CollectionId = (Guid?)null, gu.OrganizationUserId } into gu_g + from gu in gu_g.DefaultIfEmpty() + + join g in dbContext.Groups + on gu.GroupId equals g.Id into g_g + from g in g_g.DefaultIfEmpty() + + join cg in dbContext.CollectionGroups + on new { cc.CollectionId, gu.GroupId } equals + new { cg.CollectionId, cg.GroupId } into cg_g + from cg in cg_g.DefaultIfEmpty() + + where cu.CollectionId != null || cg.CollectionId != null + + select c; + + var query2 = from c in dbContext.Ciphers + where c.UserId == _userId + select c; + + var union = query.Union(query2).Select(c => new CipherDetails + { + Id = c.Id, + UserId = c.UserId, + OrganizationId = c.OrganizationId, + Type = c.Type, + Data = c.Data, + Attachments = c.Attachments, + CreationDate = c.CreationDate, + RevisionDate = c.RevisionDate, + DeletedDate = c.DeletedDate, + Favorite = _userId.HasValue && c.Favorites != null && c.Favorites.ToLowerInvariant().Contains($"\"{_userId}\":true"), + FolderId = GetFolderId(_userId, c), + Edit = true, + Reprompt = c.Reprompt, + ViewPassword = true, + OrganizationUseTotp = false, + Key = c.Key + }); + return union; + } + private static Guid? GetFolderId(Guid? userId, Cipher cipher) { try diff --git a/src/Infrastructure.EntityFramework/Vault/Repositories/CipherRepository.cs b/src/Infrastructure.EntityFramework/Vault/Repositories/CipherRepository.cs index 573e4fc3bd..418938aab9 100644 --- a/src/Infrastructure.EntityFramework/Vault/Repositories/CipherRepository.cs +++ b/src/Infrastructure.EntityFramework/Vault/Repositories/CipherRepository.cs @@ -199,9 +199,9 @@ public class CipherRepository : Repository ids, Guid userId) + public async Task DeleteAsync(IEnumerable ids, Guid userId, bool useFlexibleCollections) { - await ToggleCipherStates(ids, userId, CipherStateAction.HardDelete); + await ToggleCipherStates(ids, userId, CipherStateAction.HardDelete, useFlexibleCollections); } public async Task DeleteAttachmentAsync(Guid cipherId, string attachmentId) @@ -302,12 +302,12 @@ public class CipherRepository : Repository GetByIdAsync(Guid id, Guid userId) + public async Task GetByIdAsync(Guid id, Guid userId, bool useFlexibleCollections) { using (var scope = ServiceScopeFactory.CreateScope()) { var dbContext = GetDatabaseContext(scope); - var userCipherDetails = new UserCipherDetailsQuery(userId); + var userCipherDetails = new UserCipherDetailsQuery(userId, useFlexibleCollections); var data = await userCipherDetails.Run(dbContext).FirstOrDefaultAsync(c => c.Id == id); return data; } @@ -347,13 +347,13 @@ public class CipherRepository : Repository> GetManyByUserIdAsync(Guid userId, bool withOrganizations = true) + public async Task> GetManyByUserIdAsync(Guid userId, bool useFlexibleCollections, bool withOrganizations = true) { using (var scope = ServiceScopeFactory.CreateScope()) { var dbContext = GetDatabaseContext(scope); IQueryable cipherDetailsView = withOrganizations ? - new UserCipherDetailsQuery(userId).Run(dbContext) : + new UserCipherDetailsQuery(userId, useFlexibleCollections).Run(dbContext) : new CipherDetailsQuery(userId).Run(dbContext); if (!withOrganizations) { @@ -395,13 +395,13 @@ public class CipherRepository : Repository ids, Guid? folderId, Guid userId) + public async Task MoveAsync(IEnumerable ids, Guid? folderId, Guid userId, bool useFlexibleCollections) { using (var scope = ServiceScopeFactory.CreateScope()) { var dbContext = GetDatabaseContext(scope); var cipherEntities = dbContext.Ciphers.Where(c => ids.Contains(c.Id)); - var userCipherDetails = new UserCipherDetailsQuery(userId).Run(dbContext); + var userCipherDetails = new UserCipherDetailsQuery(userId, useFlexibleCollections).Run(dbContext); var idsToMove = from ucd in userCipherDetails join c in cipherEntities on ucd.Id equals c.Id @@ -632,9 +632,9 @@ public class CipherRepository : Repository RestoreAsync(IEnumerable ids, Guid userId) + public async Task RestoreAsync(IEnumerable ids, Guid userId, bool useFlexibleCollections) { - return await ToggleCipherStates(ids, userId, CipherStateAction.Restore); + return await ToggleCipherStates(ids, userId, CipherStateAction.Restore, useFlexibleCollections); } public async Task RestoreByIdsOrganizationIdAsync(IEnumerable ids, Guid organizationId) @@ -662,12 +662,12 @@ public class CipherRepository : Repository ids, Guid userId) + public async Task SoftDeleteAsync(IEnumerable ids, Guid userId, bool useFlexibleCollections) { - await ToggleCipherStates(ids, userId, CipherStateAction.SoftDelete); + await ToggleCipherStates(ids, userId, CipherStateAction.SoftDelete, useFlexibleCollections); } - private async Task ToggleCipherStates(IEnumerable ids, Guid userId, CipherStateAction action) + private async Task ToggleCipherStates(IEnumerable ids, Guid userId, CipherStateAction action, bool useFlexibleCollections) { static bool FilterDeletedDate(CipherStateAction action, CipherDetails ucd) { @@ -682,7 +682,7 @@ public class CipherRepository : Repository ids.Contains(c.Id))).ToListAsync(); var query = from ucd in await (userCipherDetailsQuery.Run(dbContext)).ToListAsync() join c in cipherEntitiesToCheck diff --git a/src/Sql/Vault/dbo/Functions/UserCipherDetails_V2.sql b/src/Sql/Vault/dbo/Functions/UserCipherDetails_V2.sql new file mode 100644 index 0000000000..1b22bb3de0 --- /dev/null +++ b/src/Sql/Vault/dbo/Functions/UserCipherDetails_V2.sql @@ -0,0 +1,53 @@ +CREATE FUNCTION [dbo].[UserCipherDetails_V2](@UserId UNIQUEIDENTIFIER) +RETURNS TABLE +AS RETURN +WITH [CTE] AS ( + SELECT + [Id], + [OrganizationId] + FROM + [OrganizationUser] + WHERE + [UserId] = @UserId + AND [Status] = 2 -- Confirmed +) +SELECT + C.*, + COALESCE(CU.[ReadOnly], CG.[ReadOnly], 0) AS [Edit], + COALESCE(CU.[HidePasswords], CG.[HidePasswords], 0) AS [ViewPassword], + CASE + WHEN O.[UseTotp] = 1 + THEN 1 + ELSE 0 + END [OrganizationUseTotp] +FROM + [dbo].[CipherDetails](@UserId) C +INNER JOIN + [CTE] OU ON C.[UserId] IS NULL AND C.[OrganizationId] IN (SELECT [OrganizationId] FROM [CTE]) +INNER JOIN + [dbo].[Organization] O ON O.[Id] = OU.[OrganizationId] AND O.[Id] = C.[OrganizationId] AND O.[Enabled] = 1 +LEFT JOIN + [dbo].[CollectionCipher] CC ON CC.[CipherId] = C.[Id] +LEFT JOIN + [dbo].[CollectionUser] CU ON CU.[CollectionId] = CC.[CollectionId] AND CU.[OrganizationUserId] = OU.[Id] +LEFT JOIN + [dbo].[GroupUser] GU ON CU.[CollectionId] IS NULL AND GU.[OrganizationUserId] = OU.[Id] +LEFT JOIN + [dbo].[Group] G ON G.[Id] = GU.[GroupId] +LEFT JOIN + [dbo].[CollectionGroup] CG ON CG.[CollectionId] = CC.[CollectionId] AND CG.[GroupId] = GU.[GroupId] +WHERE + CU.[CollectionId] IS NOT NULL + OR CG.[CollectionId] IS NOT NULL + +UNION ALL + +SELECT + *, + 1 [Edit], + 1 [ViewPassword], + 0 [OrganizationUseTotp] +FROM + [dbo].[CipherDetails](@UserId) +WHERE + [UserId] = @UserId diff --git a/src/Sql/Vault/dbo/Stored Procedures/Cipher/CipherDetails_ReadByIdUserId_V2.sql b/src/Sql/Vault/dbo/Stored Procedures/Cipher/CipherDetails_ReadByIdUserId_V2.sql new file mode 100644 index 0000000000..0b2c8515af --- /dev/null +++ b/src/Sql/Vault/dbo/Stored Procedures/Cipher/CipherDetails_ReadByIdUserId_V2.sql @@ -0,0 +1,16 @@ +CREATE PROCEDURE [dbo].[CipherDetails_ReadByIdUserId_V2] + @Id UNIQUEIDENTIFIER, + @UserId UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + SELECT TOP 1 + * + FROM + [dbo].[UserCipherDetails_V2](@UserId) + WHERE + [Id] = @Id + ORDER BY + [Edit] DESC +END diff --git a/src/Sql/Vault/dbo/Stored Procedures/Cipher/CipherDetails_ReadByUserId_V2.sql b/src/Sql/Vault/dbo/Stored Procedures/Cipher/CipherDetails_ReadByUserId_V2.sql new file mode 100644 index 0000000000..ab021a4f69 --- /dev/null +++ b/src/Sql/Vault/dbo/Stored Procedures/Cipher/CipherDetails_ReadByUserId_V2.sql @@ -0,0 +1,11 @@ +CREATE PROCEDURE [dbo].[CipherDetails_ReadByUserId_V2] + @UserId UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + SELECT + * + FROM + [dbo].[UserCipherDetails_V2](@UserId) +END diff --git a/src/Sql/Vault/dbo/Stored Procedures/Cipher/Cipher_Delete_V2.sql b/src/Sql/Vault/dbo/Stored Procedures/Cipher/Cipher_Delete_V2.sql new file mode 100644 index 0000000000..b57c799347 --- /dev/null +++ b/src/Sql/Vault/dbo/Stored Procedures/Cipher/Cipher_Delete_V2.sql @@ -0,0 +1,73 @@ +CREATE PROCEDURE [dbo].[Cipher_Delete_V2] + @Ids AS [dbo].[GuidIdArray] READONLY, + @UserId AS UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + CREATE TABLE #Temp + ( + [Id] UNIQUEIDENTIFIER NOT NULL, + [UserId] UNIQUEIDENTIFIER NULL, + [OrganizationId] UNIQUEIDENTIFIER NULL, + [Attachments] BIT NOT NULL + ) + + INSERT INTO #Temp + SELECT + [Id], + [UserId], + [OrganizationId], + CASE WHEN [Attachments] IS NULL THEN 0 ELSE 1 END + FROM + [dbo].[UserCipherDetails_V2](@UserId) + WHERE + [Edit] = 1 + AND [Id] IN (SELECT * FROM @Ids) + + -- Delete ciphers + DELETE + FROM + [dbo].[Cipher] + WHERE + [Id] IN (SELECT [Id] FROM #Temp) + + -- Cleanup orgs + DECLARE @OrgId UNIQUEIDENTIFIER + DECLARE [OrgCursor] CURSOR FORWARD_ONLY FOR + SELECT + [OrganizationId] + FROM + #Temp + WHERE + [OrganizationId] IS NOT NULL + GROUP BY + [OrganizationId] + OPEN [OrgCursor] + FETCH NEXT FROM [OrgCursor] INTO @OrgId + WHILE @@FETCH_STATUS = 0 BEGIN + EXEC [dbo].[Organization_UpdateStorage] @OrgId + EXEC [dbo].[User_BumpAccountRevisionDateByOrganizationId] @OrgId + FETCH NEXT FROM [OrgCursor] INTO @OrgId + END + CLOSE [OrgCursor] + DEALLOCATE [OrgCursor] + + -- Cleanup user + DECLARE @UserCiphersWithStorageCount INT + SELECT + @UserCiphersWithStorageCount = COUNT(1) + FROM + #Temp + WHERE + [UserId] IS NOT NULL + AND [Attachments] = 1 + + IF @UserCiphersWithStorageCount > 0 + BEGIN + EXEC [dbo].[User_UpdateStorage] @UserId + END + EXEC [dbo].[User_BumpAccountRevisionDate] @UserId + + DROP TABLE #Temp +END diff --git a/src/Sql/Vault/dbo/Stored Procedures/Cipher/Cipher_Move_V2.sql b/src/Sql/Vault/dbo/Stored Procedures/Cipher/Cipher_Move_V2.sql new file mode 100644 index 0000000000..c495c3a260 --- /dev/null +++ b/src/Sql/Vault/dbo/Stored Procedures/Cipher/Cipher_Move_V2.sql @@ -0,0 +1,36 @@ +CREATE PROCEDURE [dbo].[Cipher_Move_V2] + @Ids AS [dbo].[GuidIdArray] READONLY, + @FolderId AS UNIQUEIDENTIFIER, + @UserId AS UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + DECLARE @UserIdKey VARCHAR(50) = CONCAT('"', @UserId, '"') + DECLARE @UserIdPath VARCHAR(50) = CONCAT('$.', @UserIdKey) + + ;WITH [IdsToMoveCTE] AS ( + SELECT + [Id] + FROM + [dbo].[UserCipherDetails_V2](@UserId) + WHERE + [Id] IN (SELECT * FROM @Ids) + ) + UPDATE + [dbo].[Cipher] + SET + [Folders] = + CASE + WHEN @FolderId IS NOT NULL AND [Folders] IS NULL THEN + CONCAT('{', @UserIdKey, ':"', @FolderId, '"', '}') + WHEN @FolderId IS NOT NULL THEN + JSON_MODIFY([Folders], @UserIdPath, CAST(@FolderId AS VARCHAR(50))) + ELSE + JSON_MODIFY([Folders], @UserIdPath, NULL) + END + WHERE + [Id] IN (SELECT * FROM [IdsToMoveCTE]) + + EXEC [dbo].[User_BumpAccountRevisionDate] @UserId +END diff --git a/src/Sql/Vault/dbo/Stored Procedures/Cipher/Cipher_Restore_V2.sql b/src/Sql/Vault/dbo/Stored Procedures/Cipher/Cipher_Restore_V2.sql new file mode 100644 index 0000000000..13b5c1c16f --- /dev/null +++ b/src/Sql/Vault/dbo/Stored Procedures/Cipher/Cipher_Restore_V2.sql @@ -0,0 +1,62 @@ +CREATE PROCEDURE [dbo].[Cipher_Restore_V2] + @Ids AS [dbo].[GuidIdArray] READONLY, + @UserId AS UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + CREATE TABLE #Temp + ( + [Id] UNIQUEIDENTIFIER NOT NULL, + [UserId] UNIQUEIDENTIFIER NULL, + [OrganizationId] UNIQUEIDENTIFIER NULL + ) + + INSERT INTO #Temp + SELECT + [Id], + [UserId], + [OrganizationId] + FROM + [dbo].[UserCipherDetails_V2](@UserId) + WHERE + [Edit] = 1 + AND [DeletedDate] IS NOT NULL + AND [Id] IN (SELECT * FROM @Ids) + + DECLARE @UtcNow DATETIME2(7) = GETUTCDATE(); + UPDATE + [dbo].[Cipher] + SET + [DeletedDate] = NULL, + [RevisionDate] = @UtcNow + WHERE + [Id] IN (SELECT [Id] FROM #Temp) + + -- Bump orgs + DECLARE @OrgId UNIQUEIDENTIFIER + DECLARE [OrgCursor] CURSOR FORWARD_ONLY FOR + SELECT + [OrganizationId] + FROM + #Temp + WHERE + [OrganizationId] IS NOT NULL + GROUP BY + [OrganizationId] + OPEN [OrgCursor] + FETCH NEXT FROM [OrgCursor] INTO @OrgId + WHILE @@FETCH_STATUS = 0 BEGIN + EXEC [dbo].[User_BumpAccountRevisionDateByOrganizationId] @OrgId + FETCH NEXT FROM [OrgCursor] INTO @OrgId + END + CLOSE [OrgCursor] + DEALLOCATE [OrgCursor] + + -- Bump user + EXEC [dbo].[User_BumpAccountRevisionDate] @UserId + + DROP TABLE #Temp + + SELECT @UtcNow +END diff --git a/src/Sql/Vault/dbo/Stored Procedures/Cipher/Cipher_SoftDelete_V2.sql b/src/Sql/Vault/dbo/Stored Procedures/Cipher/Cipher_SoftDelete_V2.sql new file mode 100644 index 0000000000..9a9424767b --- /dev/null +++ b/src/Sql/Vault/dbo/Stored Procedures/Cipher/Cipher_SoftDelete_V2.sql @@ -0,0 +1,60 @@ +CREATE PROCEDURE [dbo].[Cipher_SoftDelete_V2] + @Ids AS [dbo].[GuidIdArray] READONLY, + @UserId AS UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + CREATE TABLE #Temp + ( + [Id] UNIQUEIDENTIFIER NOT NULL, + [UserId] UNIQUEIDENTIFIER NULL, + [OrganizationId] UNIQUEIDENTIFIER NULL + ) + + INSERT INTO #Temp + SELECT + [Id], + [UserId], + [OrganizationId] + FROM + [dbo].[UserCipherDetails_V2](@UserId) + WHERE + [Edit] = 1 + AND [DeletedDate] IS NULL + AND [Id] IN (SELECT * FROM @Ids) + + -- Delete ciphers + DECLARE @UtcNow DATETIME2(7) = GETUTCDATE(); + UPDATE + [dbo].[Cipher] + SET + [DeletedDate] = @UtcNow, + [RevisionDate] = @UtcNow + WHERE + [Id] IN (SELECT [Id] FROM #Temp) + + -- Cleanup orgs + DECLARE @OrgId UNIQUEIDENTIFIER + DECLARE [OrgCursor] CURSOR FORWARD_ONLY FOR + SELECT + [OrganizationId] + FROM + #Temp + WHERE + [OrganizationId] IS NOT NULL + GROUP BY + [OrganizationId] + OPEN [OrgCursor] + FETCH NEXT FROM [OrgCursor] INTO @OrgId + WHILE @@FETCH_STATUS = 0 BEGIN + EXEC [dbo].[User_BumpAccountRevisionDateByOrganizationId] @OrgId + FETCH NEXT FROM [OrgCursor] INTO @OrgId + END + CLOSE [OrgCursor] + DEALLOCATE [OrgCursor] + + EXEC [dbo].[User_BumpAccountRevisionDate] @UserId + + DROP TABLE #Temp +END diff --git a/test/Api.Test/Vault/Controllers/CiphersControllerTests.cs b/test/Api.Test/Vault/Controllers/CiphersControllerTests.cs index 8839f43f16..78e3d04ef5 100644 --- a/test/Api.Test/Vault/Controllers/CiphersControllerTests.cs +++ b/test/Api.Test/Vault/Controllers/CiphersControllerTests.cs @@ -34,10 +34,10 @@ public class CiphersControllerTests }; sutProvider.GetDependency() - .GetByIdAsync(cipherId, userId) + .GetByIdAsync(cipherId, userId, Arg.Any()) .Returns(Task.FromResult(cipherDetails)); - var result = await sutProvider.Sut.PutPartial(cipherId.ToString(), new CipherPartialRequestModel { Favorite = isFavorite, FolderId = folderId.ToString() }); + var result = await sutProvider.Sut.PutPartial(cipherId, new CipherPartialRequestModel { Favorite = isFavorite, FolderId = folderId.ToString() }); Assert.Equal(folderId, result.FolderId); Assert.Equal(isFavorite, result.Favorite); diff --git a/test/Api.Test/Vault/Controllers/SyncControllerTests.cs b/test/Api.Test/Vault/Controllers/SyncControllerTests.cs index 2f29ba6109..c46dcd73af 100644 --- a/test/Api.Test/Vault/Controllers/SyncControllerTests.cs +++ b/test/Api.Test/Vault/Controllers/SyncControllerTests.cs @@ -107,7 +107,7 @@ public class SyncControllerTests .Returns(providerUserOrganizationDetails); folderRepository.GetManyByUserIdAsync(user.Id).Returns(folders); - cipherRepository.GetManyByUserIdAsync(user.Id).Returns(ciphers); + cipherRepository.GetManyByUserIdAsync(user.Id, useFlexibleCollections: Arg.Any()).Returns(ciphers); sendRepository .GetManyByUserIdAsync(user.Id).Returns(sends); @@ -198,7 +198,7 @@ public class SyncControllerTests .Returns(providerUserOrganizationDetails); folderRepository.GetManyByUserIdAsync(user.Id).Returns(folders); - cipherRepository.GetManyByUserIdAsync(user.Id).Returns(ciphers); + cipherRepository.GetManyByUserIdAsync(user.Id, useFlexibleCollections: Arg.Any()).Returns(ciphers); sendRepository .GetManyByUserIdAsync(user.Id).Returns(sends); @@ -272,7 +272,7 @@ public class SyncControllerTests .Returns(providerUserOrganizationDetails); folderRepository.GetManyByUserIdAsync(user.Id).Returns(folders); - cipherRepository.GetManyByUserIdAsync(user.Id).Returns(ciphers); + cipherRepository.GetManyByUserIdAsync(user.Id, useFlexibleCollections: Arg.Any()).Returns(ciphers); sendRepository .GetManyByUserIdAsync(user.Id).Returns(sends); @@ -335,7 +335,7 @@ public class SyncControllerTests .GetManyByUserIdAsync(default); await cipherRepository.ReceivedWithAnyArgs(1) - .GetManyByUserIdAsync(default); + .GetManyByUserIdAsync(default, useFlexibleCollections: default); await sendRepository.ReceivedWithAnyArgs(1) .GetManyByUserIdAsync(default); diff --git a/test/Core.Test/Vault/Services/CipherServiceTests.cs b/test/Core.Test/Vault/Services/CipherServiceTests.cs index d299084c9c..d22b26ea18 100644 --- a/test/Core.Test/Vault/Services/CipherServiceTests.cs +++ b/test/Core.Test/Vault/Services/CipherServiceTests.cs @@ -675,9 +675,9 @@ public class CipherServiceTests cipher.RevisionDate = previousRevisionDate; } - sutProvider.GetDependency().GetManyByUserIdAsync(restoringUserId).Returns(ciphers); + sutProvider.GetDependency().GetManyByUserIdAsync(restoringUserId, useFlexibleCollections: Arg.Any()).Returns(ciphers); var revisionDate = previousRevisionDate + TimeSpan.FromMinutes(1); - sutProvider.GetDependency().RestoreAsync(Arg.Any>(), restoringUserId).Returns(revisionDate); + sutProvider.GetDependency().RestoreAsync(Arg.Any>(), restoringUserId, Arg.Any()).Returns(revisionDate); await sutProvider.Sut.RestoreManyAsync(cipherIds, restoringUserId); @@ -789,8 +789,8 @@ public class CipherServiceTests await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().GetManyOrganizationDetailsByOrganizationIdAsync(default); await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().RestoreByIdsOrganizationIdAsync(default, default); await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().RestoreByIdsOrganizationIdAsync(default, default); - await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().GetManyByUserIdAsync(default); - await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().RestoreAsync(default, default); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().GetManyByUserIdAsync(default, useFlexibleCollections: default); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().RestoreAsync(default, default, default); await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().LogCipherEventsAsync(default); await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().PushSyncCiphersAsync(default); } diff --git a/util/Migrator/DbScripts/2023-11-28_00_DeprecateAccessAll_UserCollectionDetails.sql b/util/Migrator/DbScripts/2023-11-28_00_DeprecateAccessAll_UserCollectionDetails.sql new file mode 100644 index 0000000000..7b026c4b49 --- /dev/null +++ b/util/Migrator/DbScripts/2023-11-28_00_DeprecateAccessAll_UserCollectionDetails.sql @@ -0,0 +1,334 @@ +-- Flexible Collections: create new UserCipherDetails sproc that doesn't use AccessAll logic + +CREATE OR ALTER FUNCTION [dbo].[UserCipherDetails_V2](@UserId UNIQUEIDENTIFIER) +RETURNS TABLE +AS RETURN +WITH [CTE] AS ( + SELECT + [Id], + [OrganizationId] + FROM + [OrganizationUser] + WHERE + [UserId] = @UserId + AND [Status] = 2 -- Confirmed +) +SELECT + C.*, + COALESCE(CU.[ReadOnly], CG.[ReadOnly], 0) AS [Edit], + COALESCE(CU.[HidePasswords], CG.[HidePasswords], 0) AS [ViewPassword], + CASE + WHEN O.[UseTotp] = 1 + THEN 1 + ELSE 0 + END [OrganizationUseTotp] +FROM + [dbo].[CipherDetails](@UserId) C +INNER JOIN + [CTE] OU ON C.[UserId] IS NULL AND C.[OrganizationId] IN (SELECT [OrganizationId] FROM [CTE]) +INNER JOIN + [dbo].[Organization] O ON O.[Id] = OU.[OrganizationId] AND O.[Id] = C.[OrganizationId] AND O.[Enabled] = 1 +LEFT JOIN + [dbo].[CollectionCipher] CC ON CC.[CipherId] = C.[Id] +LEFT JOIN + [dbo].[CollectionUser] CU ON CU.[CollectionId] = CC.[CollectionId] AND CU.[OrganizationUserId] = OU.[Id] +LEFT JOIN + [dbo].[GroupUser] GU ON CU.[CollectionId] IS NULL AND GU.[OrganizationUserId] = OU.[Id] +LEFT JOIN + [dbo].[Group] G ON G.[Id] = GU.[GroupId] +LEFT JOIN + [dbo].[CollectionGroup] CG ON CG.[CollectionId] = CC.[CollectionId] AND CG.[GroupId] = GU.[GroupId] +WHERE + CU.[CollectionId] IS NOT NULL + OR CG.[CollectionId] IS NOT NULL + +UNION ALL + +SELECT + *, + 1 [Edit], + 1 [ViewPassword], + 0 [OrganizationUseTotp] +FROM + [dbo].[CipherDetails](@UserId) +WHERE + [UserId] = @UserId +GO + +-- Create v2 sprocs for all sprocs that call UserCipherDetails + +-- CipherDetails_ReadByIdUserId_V2 +CREATE OR ALTER PROCEDURE [dbo].[CipherDetails_ReadByIdUserId_V2] + @Id UNIQUEIDENTIFIER, + @UserId UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + SELECT TOP 1 + * + FROM + [dbo].[UserCipherDetails_V2](@UserId) + WHERE + [Id] = @Id + ORDER BY + [Edit] DESC +END +GO + +-- CipherDetails_ReadByUserId_V2 +CREATE OR ALTER PROCEDURE [dbo].[CipherDetails_ReadByUserId_V2] + @UserId UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + SELECT + * + FROM + [dbo].[UserCipherDetails_V2](@UserId) +END +GO + +-- Cipher_Delete_V2 +CREATE OR ALTER PROCEDURE [dbo].[Cipher_Delete_V2] + @Ids AS [dbo].[GuidIdArray] READONLY, + @UserId AS UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + CREATE TABLE #Temp + ( + [Id] UNIQUEIDENTIFIER NOT NULL, + [UserId] UNIQUEIDENTIFIER NULL, + [OrganizationId] UNIQUEIDENTIFIER NULL, + [Attachments] BIT NOT NULL + ) + + INSERT INTO #Temp + SELECT + [Id], + [UserId], + [OrganizationId], + CASE WHEN [Attachments] IS NULL THEN 0 ELSE 1 END + FROM + [dbo].[UserCipherDetails_V2](@UserId) + WHERE + [Edit] = 1 + AND [Id] IN (SELECT * FROM @Ids) + + -- Delete ciphers + DELETE + FROM + [dbo].[Cipher] + WHERE + [Id] IN (SELECT [Id] FROM #Temp) + + -- Cleanup orgs + DECLARE @OrgId UNIQUEIDENTIFIER + DECLARE [OrgCursor] CURSOR FORWARD_ONLY FOR + SELECT + [OrganizationId] + FROM + #Temp + WHERE + [OrganizationId] IS NOT NULL + GROUP BY + [OrganizationId] + OPEN [OrgCursor] + FETCH NEXT FROM [OrgCursor] INTO @OrgId + WHILE @@FETCH_STATUS = 0 BEGIN + EXEC [dbo].[Organization_UpdateStorage] @OrgId + EXEC [dbo].[User_BumpAccountRevisionDateByOrganizationId] @OrgId + FETCH NEXT FROM [OrgCursor] INTO @OrgId + END + CLOSE [OrgCursor] + DEALLOCATE [OrgCursor] + + -- Cleanup user + DECLARE @UserCiphersWithStorageCount INT + SELECT + @UserCiphersWithStorageCount = COUNT(1) + FROM + #Temp + WHERE + [UserId] IS NOT NULL + AND [Attachments] = 1 + + IF @UserCiphersWithStorageCount > 0 + BEGIN + EXEC [dbo].[User_UpdateStorage] @UserId + END + EXEC [dbo].[User_BumpAccountRevisionDate] @UserId + + DROP TABLE #Temp +END +GO + +-- Cipher_Move_V2 +CREATE OR ALTER PROCEDURE [dbo].[Cipher_Move_V2] + @Ids AS [dbo].[GuidIdArray] READONLY, + @FolderId AS UNIQUEIDENTIFIER, + @UserId AS UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + DECLARE @UserIdKey VARCHAR(50) = CONCAT('"', @UserId, '"') + DECLARE @UserIdPath VARCHAR(50) = CONCAT('$.', @UserIdKey) + + ;WITH [IdsToMoveCTE] AS ( + SELECT + [Id] + FROM + [dbo].[UserCipherDetails_V2](@UserId) + WHERE + [Id] IN (SELECT * FROM @Ids) + ) + UPDATE + [dbo].[Cipher] + SET + [Folders] = + CASE + WHEN @FolderId IS NOT NULL AND [Folders] IS NULL THEN + CONCAT('{', @UserIdKey, ':"', @FolderId, '"', '}') + WHEN @FolderId IS NOT NULL THEN + JSON_MODIFY([Folders], @UserIdPath, CAST(@FolderId AS VARCHAR(50))) + ELSE + JSON_MODIFY([Folders], @UserIdPath, NULL) + END + WHERE + [Id] IN (SELECT * FROM [IdsToMoveCTE]) + + EXEC [dbo].[User_BumpAccountRevisionDate] @UserId +END +GO + +-- Cipher_Restore_V2 +CREATE OR ALTER PROCEDURE [dbo].[Cipher_Restore_V2] + @Ids AS [dbo].[GuidIdArray] READONLY, + @UserId AS UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + CREATE TABLE #Temp + ( + [Id] UNIQUEIDENTIFIER NOT NULL, + [UserId] UNIQUEIDENTIFIER NULL, + [OrganizationId] UNIQUEIDENTIFIER NULL + ) + + INSERT INTO #Temp + SELECT + [Id], + [UserId], + [OrganizationId] + FROM + [dbo].[UserCipherDetails_V2](@UserId) + WHERE + [Edit] = 1 + AND [DeletedDate] IS NOT NULL + AND [Id] IN (SELECT * FROM @Ids) + + DECLARE @UtcNow DATETIME2(7) = GETUTCDATE(); + UPDATE + [dbo].[Cipher] + SET + [DeletedDate] = NULL, + [RevisionDate] = @UtcNow + WHERE + [Id] IN (SELECT [Id] FROM #Temp) + + -- Bump orgs + DECLARE @OrgId UNIQUEIDENTIFIER + DECLARE [OrgCursor] CURSOR FORWARD_ONLY FOR + SELECT + [OrganizationId] + FROM + #Temp + WHERE + [OrganizationId] IS NOT NULL + GROUP BY + [OrganizationId] + OPEN [OrgCursor] + FETCH NEXT FROM [OrgCursor] INTO @OrgId + WHILE @@FETCH_STATUS = 0 BEGIN + EXEC [dbo].[User_BumpAccountRevisionDateByOrganizationId] @OrgId + FETCH NEXT FROM [OrgCursor] INTO @OrgId + END + CLOSE [OrgCursor] + DEALLOCATE [OrgCursor] + + -- Bump user + EXEC [dbo].[User_BumpAccountRevisionDate] @UserId + + DROP TABLE #Temp + + SELECT @UtcNow +END +GO + +-- Cipher_SoftDelete_V2 +CREATE OR ALTER PROCEDURE [dbo].[Cipher_SoftDelete_V2] + @Ids AS [dbo].[GuidIdArray] READONLY, + @UserId AS UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + CREATE TABLE #Temp + ( + [Id] UNIQUEIDENTIFIER NOT NULL, + [UserId] UNIQUEIDENTIFIER NULL, + [OrganizationId] UNIQUEIDENTIFIER NULL + ) + + INSERT INTO #Temp + SELECT + [Id], + [UserId], + [OrganizationId] + FROM + [dbo].[UserCipherDetails_V2](@UserId) + WHERE + [Edit] = 1 + AND [DeletedDate] IS NULL + AND [Id] IN (SELECT * FROM @Ids) + + -- Delete ciphers + DECLARE @UtcNow DATETIME2(7) = GETUTCDATE(); + UPDATE + [dbo].[Cipher] + SET + [DeletedDate] = @UtcNow, + [RevisionDate] = @UtcNow + WHERE + [Id] IN (SELECT [Id] FROM #Temp) + + -- Cleanup orgs + DECLARE @OrgId UNIQUEIDENTIFIER + DECLARE [OrgCursor] CURSOR FORWARD_ONLY FOR + SELECT + [OrganizationId] + FROM + #Temp + WHERE + [OrganizationId] IS NOT NULL + GROUP BY + [OrganizationId] + OPEN [OrgCursor] + FETCH NEXT FROM [OrgCursor] INTO @OrgId + WHILE @@FETCH_STATUS = 0 BEGIN + EXEC [dbo].[User_BumpAccountRevisionDateByOrganizationId] @OrgId + FETCH NEXT FROM [OrgCursor] INTO @OrgId + END + CLOSE [OrgCursor] + DEALLOCATE [OrgCursor] + + EXEC [dbo].[User_BumpAccountRevisionDate] @UserId + + DROP TABLE #Temp +END +GO