1
0
mirror of https://github.com/bitwarden/server.git synced 2025-05-22 12:04:27 -05:00

[AC-1330] [AC-1815] [Server] Deprecate access control indicator - UserCipherDetails (#3372)

* Create UserCipherDetails_v2 and update logic to remove AccessAll
* Create v2 variants of all sprocs that rely on it
* Add feature flag logic to call old or new sproc
* Make equivalent changes to EF queries
This commit is contained in:
Thomas Rittson 2023-11-28 11:14:33 +10:00 committed by GitHub
parent b062ab8043
commit 12667dbb3f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 904 additions and 107 deletions

View File

@ -2,6 +2,8 @@
using Bit.Admin.Models; using Bit.Admin.Models;
using Bit.Admin.Services; using Bit.Admin.Services;
using Bit.Admin.Utilities; using Bit.Admin.Utilities;
using Bit.Core;
using Bit.Core.Context;
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Services; using Bit.Core.Services;
@ -21,19 +23,28 @@ public class UsersController : Controller
private readonly IPaymentService _paymentService; private readonly IPaymentService _paymentService;
private readonly GlobalSettings _globalSettings; private readonly GlobalSettings _globalSettings;
private readonly IAccessControlService _accessControlService; private readonly IAccessControlService _accessControlService;
private readonly ICurrentContext _currentContext;
private readonly IFeatureService _featureService;
private bool UseFlexibleCollections =>
_featureService.IsEnabled(FeatureFlagKeys.FlexibleCollections, _currentContext);
public UsersController( public UsersController(
IUserRepository userRepository, IUserRepository userRepository,
ICipherRepository cipherRepository, ICipherRepository cipherRepository,
IPaymentService paymentService, IPaymentService paymentService,
GlobalSettings globalSettings, GlobalSettings globalSettings,
IAccessControlService accessControlService) IAccessControlService accessControlService,
ICurrentContext currentContext,
IFeatureService featureService)
{ {
_userRepository = userRepository; _userRepository = userRepository;
_cipherRepository = cipherRepository; _cipherRepository = cipherRepository;
_paymentService = paymentService; _paymentService = paymentService;
_globalSettings = globalSettings; _globalSettings = globalSettings;
_accessControlService = accessControlService; _accessControlService = accessControlService;
_currentContext = currentContext;
_featureService = featureService;
} }
[RequirePermission(Permission.User_List_View)] [RequirePermission(Permission.User_List_View)]
@ -69,7 +80,7 @@ public class UsersController : Controller
return RedirectToAction("Index"); return RedirectToAction("Index");
} }
var ciphers = await _cipherRepository.GetManyByUserIdAsync(id); var ciphers = await _cipherRepository.GetManyByUserIdAsync(id, useFlexibleCollections: UseFlexibleCollections);
return View(new UserViewModel(user, ciphers)); return View(new UserViewModel(user, ciphers));
} }
@ -82,7 +93,7 @@ public class UsersController : Controller
return RedirectToAction("Index"); return RedirectToAction("Index");
} }
var ciphers = await _cipherRepository.GetManyByUserIdAsync(id); var ciphers = await _cipherRepository.GetManyByUserIdAsync(id, useFlexibleCollections: UseFlexibleCollections);
var billingInfo = await _paymentService.GetBillingAsync(user); var billingInfo = await _paymentService.GetBillingAsync(user);
return View(new UserEditModel(user, ciphers, billingInfo, _globalSettings)); return View(new UserEditModel(user, ciphers, billingInfo, _globalSettings));
} }

View File

@ -58,6 +58,8 @@ public class AccountsController : Controller
private readonly IFeatureService _featureService; private readonly IFeatureService _featureService;
private readonly ICurrentContext _currentContext; private readonly ICurrentContext _currentContext;
private bool UseFlexibleCollections =>
_featureService.IsEnabled(FeatureFlagKeys.FlexibleCollections, _currentContext);
public AccountsController( public AccountsController(
GlobalSettings globalSettings, GlobalSettings globalSettings,
@ -415,7 +417,7 @@ public class AccountsController : Controller
var ciphers = new List<Cipher>(); var ciphers = new List<Cipher>();
if (model.Ciphers.Any()) if (model.Ciphers.Any())
{ {
var existingCiphers = await _cipherRepository.GetManyByUserIdAsync(user.Id); var existingCiphers = await _cipherRepository.GetManyByUserIdAsync(user.Id, useFlexibleCollections: UseFlexibleCollections);
ciphers.AddRange(existingCiphers ciphers.AddRange(existingCiphers
.Join(model.Ciphers, c => c.Id, c => c.Id, (existing, c) => c.ToCipher(existing))); .Join(model.Ciphers, c => c.Id, c => c.Id, (existing, c) => c.ToCipher(existing)));
} }

View File

@ -40,6 +40,10 @@ public class CiphersController : Controller
private readonly ILogger<CiphersController> _logger; private readonly ILogger<CiphersController> _logger;
private readonly GlobalSettings _globalSettings; private readonly GlobalSettings _globalSettings;
private readonly Version _cipherKeyEncryptionMinimumVersion = new Version(Constants.CipherKeyEncryptionMinimumVersion); private readonly Version _cipherKeyEncryptionMinimumVersion = new Version(Constants.CipherKeyEncryptionMinimumVersion);
private readonly IFeatureService _featureService;
private bool UseFlexibleCollections =>
_featureService.IsEnabled(FeatureFlagKeys.FlexibleCollections, _currentContext);
public CiphersController( public CiphersController(
ICipherRepository cipherRepository, ICipherRepository cipherRepository,
@ -50,7 +54,8 @@ public class CiphersController : Controller
IProviderService providerService, IProviderService providerService,
ICurrentContext currentContext, ICurrentContext currentContext,
ILogger<CiphersController> logger, ILogger<CiphersController> logger,
GlobalSettings globalSettings) GlobalSettings globalSettings,
IFeatureService featureService)
{ {
_cipherRepository = cipherRepository; _cipherRepository = cipherRepository;
_collectionCipherRepository = collectionCipherRepository; _collectionCipherRepository = collectionCipherRepository;
@ -61,13 +66,14 @@ public class CiphersController : Controller
_currentContext = currentContext; _currentContext = currentContext;
_logger = logger; _logger = logger;
_globalSettings = globalSettings; _globalSettings = globalSettings;
_featureService = featureService;
} }
[HttpGet("{id}")] [HttpGet("{id}")]
public async Task<CipherResponseModel> Get(string id) public async Task<CipherResponseModel> Get(Guid id)
{ {
var userId = _userService.GetProperUserId(User).Value; var userId = _userService.GetProperUserId(User).Value;
var cipher = await _cipherRepository.GetByIdAsync(new Guid(id), userId); var cipher = await GetByIdAsync(id, userId);
if (cipher == null) if (cipher == null)
{ {
throw new NotFoundException(); throw new NotFoundException();
@ -91,17 +97,16 @@ public class CiphersController : Controller
[HttpGet("{id}/full-details")] [HttpGet("{id}/full-details")]
[HttpGet("{id}/details")] [HttpGet("{id}/details")]
public async Task<CipherDetailsResponseModel> GetDetails(string id) public async Task<CipherDetailsResponseModel> GetDetails(Guid id)
{ {
var userId = _userService.GetProperUserId(User).Value; var userId = _userService.GetProperUserId(User).Value;
var cipherId = new Guid(id); var cipher = await GetByIdAsync(id, userId);
var cipher = await _cipherRepository.GetByIdAsync(cipherId, userId);
if (cipher == null) if (cipher == null)
{ {
throw new NotFoundException(); throw new NotFoundException();
} }
var collectionCiphers = await _collectionCipherRepository.GetManyByUserIdCipherIdAsync(userId, cipherId); var collectionCiphers = await _collectionCipherRepository.GetManyByUserIdCipherIdAsync(userId, id);
return new CipherDetailsResponseModel(cipher, _globalSettings, collectionCiphers); return new CipherDetailsResponseModel(cipher, _globalSettings, collectionCiphers);
} }
@ -111,7 +116,7 @@ public class CiphersController : Controller
var userId = _userService.GetProperUserId(User).Value; var userId = _userService.GetProperUserId(User).Value;
var hasOrgs = _currentContext.Organizations?.Any() ?? false; var hasOrgs = _currentContext.Organizations?.Any() ?? false;
// TODO: Use hasOrgs proper for cipher listing here? // 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<Guid, IGrouping<Guid, CollectionCipher>> collectionCiphersGroupDict = null; Dictionary<Guid, IGrouping<Guid, CollectionCipher>> collectionCiphersGroupDict = null;
if (hasOrgs) if (hasOrgs)
{ {
@ -175,7 +180,7 @@ public class CiphersController : Controller
public async Task<CipherResponseModel> Put(Guid id, [FromBody] CipherRequestModel model) public async Task<CipherResponseModel> Put(Guid id, [FromBody] CipherRequestModel model)
{ {
var userId = _userService.GetProperUserId(User).Value; var userId = _userService.GetProperUserId(User).Value;
var cipher = await _cipherRepository.GetByIdAsync(id, userId); var cipher = await GetByIdAsync(id, userId);
if (cipher == null) if (cipher == null)
{ {
throw new NotFoundException(); throw new NotFoundException();
@ -247,25 +252,23 @@ public class CiphersController : Controller
[HttpPut("{id}/partial")] [HttpPut("{id}/partial")]
[HttpPost("{id}/partial")] [HttpPost("{id}/partial")]
public async Task<CipherResponseModel> PutPartial(string id, [FromBody] CipherPartialRequestModel model) public async Task<CipherResponseModel> PutPartial(Guid id, [FromBody] CipherPartialRequestModel model)
{ {
var userId = _userService.GetProperUserId(User).Value; var userId = _userService.GetProperUserId(User).Value;
var folderId = string.IsNullOrWhiteSpace(model.FolderId) ? null : (Guid?)new Guid(model.FolderId); var folderId = string.IsNullOrWhiteSpace(model.FolderId) ? null : (Guid?)new Guid(model.FolderId);
var cipherId = new Guid(id); await _cipherRepository.UpdatePartialAsync(id, userId, folderId, model.Favorite);
await _cipherRepository.UpdatePartialAsync(cipherId, userId, folderId, model.Favorite);
var cipher = await _cipherRepository.GetByIdAsync(cipherId, userId); var cipher = await GetByIdAsync(id, userId);
var response = new CipherResponseModel(cipher, _globalSettings); var response = new CipherResponseModel(cipher, _globalSettings);
return response; return response;
} }
[HttpPut("{id}/share")] [HttpPut("{id}/share")]
[HttpPost("{id}/share")] [HttpPost("{id}/share")]
public async Task<CipherResponseModel> PutShare(string id, [FromBody] CipherShareRequestModel model) public async Task<CipherResponseModel> PutShare(Guid id, [FromBody] CipherShareRequestModel model)
{ {
var userId = _userService.GetProperUserId(User).Value; var userId = _userService.GetProperUserId(User).Value;
var cipherId = new Guid(id); var cipher = await _cipherRepository.GetByIdAsync(id);
var cipher = await _cipherRepository.GetByIdAsync(cipherId);
if (cipher == null || cipher.UserId != userId || if (cipher == null || cipher.UserId != userId ||
!await _currentContext.OrganizationUser(new Guid(model.Cipher.OrganizationId))) !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), await _cipherService.ShareAsync(original, model.Cipher.ToCipher(cipher), new Guid(model.Cipher.OrganizationId),
model.CollectionIds.Select(c => new Guid(c)), userId, model.Cipher.LastKnownRevisionDate); 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); var response = new CipherResponseModel(sharedCipher, _globalSettings);
return response; return response;
} }
[HttpPut("{id}/collections")] [HttpPut("{id}/collections")]
[HttpPost("{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 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 || if (cipher == null || !cipher.OrganizationId.HasValue ||
!await _currentContext.OrganizationUser(cipher.OrganizationId.Value)) !await _currentContext.OrganizationUser(cipher.OrganizationId.Value))
{ {
@ -318,10 +321,10 @@ public class CiphersController : Controller
[HttpDelete("{id}")] [HttpDelete("{id}")]
[HttpPost("{id}/delete")] [HttpPost("{id}/delete")]
public async Task Delete(string id) public async Task Delete(Guid id)
{ {
var userId = _userService.GetProperUserId(User).Value; var userId = _userService.GetProperUserId(User).Value;
var cipher = await _cipherRepository.GetByIdAsync(new Guid(id), userId); var cipher = await GetByIdAsync(id, userId);
if (cipher == null) if (cipher == null)
{ {
throw new NotFoundException(); throw new NotFoundException();
@ -380,10 +383,10 @@ public class CiphersController : Controller
} }
[HttpPut("{id}/delete")] [HttpPut("{id}/delete")]
public async Task PutDelete(string id) public async Task PutDelete(Guid id)
{ {
var userId = _userService.GetProperUserId(User).Value; var userId = _userService.GetProperUserId(User).Value;
var cipher = await _cipherRepository.GetByIdAsync(new Guid(id), userId); var cipher = await GetByIdAsync(id, userId);
if (cipher == null) if (cipher == null)
{ {
throw new NotFoundException(); throw new NotFoundException();
@ -436,10 +439,10 @@ public class CiphersController : Controller
} }
[HttpPut("{id}/restore")] [HttpPut("{id}/restore")]
public async Task<CipherResponseModel> PutRestore(string id) public async Task<CipherResponseModel> PutRestore(Guid id)
{ {
var userId = _userService.GetProperUserId(User).Value; var userId = _userService.GetProperUserId(User).Value;
var cipher = await _cipherRepository.GetByIdAsync(new Guid(id), userId); var cipher = await GetByIdAsync(id, userId);
if (cipher == null) if (cipher == null)
{ {
throw new NotFoundException(); throw new NotFoundException();
@ -526,7 +529,7 @@ public class CiphersController : Controller
} }
var userId = _userService.GetProperUserId(User).Value; 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 ciphersDict = ciphers.ToDictionary(c => c.Id);
var shareCiphers = new List<(Cipher, DateTime?)>(); var shareCiphers = new List<(Cipher, DateTime?)>();
@ -581,13 +584,12 @@ public class CiphersController : Controller
} }
[HttpPost("{id}/attachment/v2")] [HttpPost("{id}/attachment/v2")]
public async Task<AttachmentUploadDataResponseModel> PostAttachment(string id, [FromBody] AttachmentRequestModel request) public async Task<AttachmentUploadDataResponseModel> PostAttachment(Guid id, [FromBody] AttachmentRequestModel request)
{ {
var idGuid = new Guid(id);
var userId = _userService.GetProperUserId(User).Value; var userId = _userService.GetProperUserId(User).Value;
var cipher = request.AdminRequest ? var cipher = request.AdminRequest ?
await _cipherRepository.GetOrganizationDetailsByIdAsync(idGuid) : await _cipherRepository.GetOrganizationDetailsByIdAsync(id) :
await _cipherRepository.GetByIdAsync(idGuid, userId); await GetByIdAsync(id, userId);
if (cipher == null || (request.AdminRequest && (!cipher.OrganizationId.HasValue || if (cipher == null || (request.AdminRequest && (!cipher.OrganizationId.HasValue ||
!await _currentContext.EditAnyCollection(cipher.OrganizationId.Value)))) !await _currentContext.EditAnyCollection(cipher.OrganizationId.Value))))
@ -615,11 +617,10 @@ public class CiphersController : Controller
} }
[HttpGet("{id}/attachment/{attachmentId}/renew")] [HttpGet("{id}/attachment/{attachmentId}/renew")]
public async Task<AttachmentUploadDataResponseModel> RenewFileUploadUrl(string id, string attachmentId) public async Task<AttachmentUploadDataResponseModel> RenewFileUploadUrl(Guid id, string attachmentId)
{ {
var userId = _userService.GetProperUserId(User).Value; var userId = _userService.GetProperUserId(User).Value;
var cipherId = new Guid(id); var cipher = await GetByIdAsync(id, userId);
var cipher = await _cipherRepository.GetByIdAsync(cipherId, userId);
var attachments = cipher?.GetAttachments(); var attachments = cipher?.GetAttachments();
if (attachments == null || !attachments.ContainsKey(attachmentId) || attachments[attachmentId].Validated) if (attachments == null || !attachments.ContainsKey(attachmentId) || attachments[attachmentId].Validated)
@ -638,7 +639,7 @@ public class CiphersController : Controller
[SelfHosted(SelfHostedOnly = true)] [SelfHosted(SelfHostedOnly = true)]
[RequestSizeLimit(Constants.FileSize501mb)] [RequestSizeLimit(Constants.FileSize501mb)]
[DisableFormValueModelBinding] [DisableFormValueModelBinding]
public async Task PostFileForExistingAttachment(string id, string attachmentId) public async Task PostFileForExistingAttachment(Guid id, string attachmentId)
{ {
if (!Request?.ContentType.Contains("multipart/") ?? true) if (!Request?.ContentType.Contains("multipart/") ?? true)
{ {
@ -646,7 +647,7 @@ public class CiphersController : Controller
} }
var userId = _userService.GetProperUserId(User).Value; 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(); var attachments = cipher?.GetAttachments();
if (attachments == null || !attachments.ContainsKey(attachmentId)) if (attachments == null || !attachments.ContainsKey(attachmentId))
{ {
@ -664,13 +665,12 @@ public class CiphersController : Controller
[Obsolete("Deprecated Attachments API", false)] [Obsolete("Deprecated Attachments API", false)]
[RequestSizeLimit(Constants.FileSize101mb)] [RequestSizeLimit(Constants.FileSize101mb)]
[DisableFormValueModelBinding] [DisableFormValueModelBinding]
public async Task<CipherResponseModel> PostAttachment(string id) public async Task<CipherResponseModel> PostAttachment(Guid id)
{ {
ValidateAttachment(); ValidateAttachment();
var idGuid = new Guid(id);
var userId = _userService.GetProperUserId(User).Value; var userId = _userService.GetProperUserId(User).Value;
var cipher = await _cipherRepository.GetByIdAsync(idGuid, userId); var cipher = await GetByIdAsync(id, userId);
if (cipher == null) if (cipher == null)
{ {
throw new NotFoundException(); throw new NotFoundException();
@ -711,10 +711,10 @@ public class CiphersController : Controller
} }
[HttpGet("{id}/attachment/{attachmentId}")] [HttpGet("{id}/attachment/{attachmentId}")]
public async Task<AttachmentResponseModel> GetAttachmentData(string id, string attachmentId) public async Task<AttachmentResponseModel> GetAttachmentData(Guid id, string attachmentId)
{ {
var userId = _userService.GetProperUserId(User).Value; 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); var result = await _cipherService.GetAttachmentDownloadDataAsync(cipher, attachmentId);
return new AttachmentResponseModel(result); return new AttachmentResponseModel(result);
} }
@ -742,11 +742,10 @@ public class CiphersController : Controller
[HttpDelete("{id}/attachment/{attachmentId}")] [HttpDelete("{id}/attachment/{attachmentId}")]
[HttpPost("{id}/attachment/{attachmentId}/delete")] [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 userId = _userService.GetProperUserId(User).Value;
var cipher = await _cipherRepository.GetByIdAsync(idGuid, userId); var cipher = await GetByIdAsync(id, userId);
if (cipher == null) if (cipher == null)
{ {
throw new NotFoundException(); throw new NotFoundException();
@ -836,4 +835,9 @@ public class CiphersController : Controller
} }
} }
} }
private async Task<CipherDetails> GetByIdAsync(Guid cipherId, Guid userId)
{
return await _cipherRepository.GetByIdAsync(cipherId, userId, UseFlexibleCollections);
}
} }

View File

@ -1,7 +1,9 @@
using Bit.Api.Vault.Models.Response; using Bit.Api.Vault.Models.Response;
using Bit.Core;
using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.AdminConsole.Enums.Provider;
using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Context;
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
@ -30,6 +32,11 @@ public class SyncController : Controller
private readonly IPolicyRepository _policyRepository; private readonly IPolicyRepository _policyRepository;
private readonly ISendRepository _sendRepository; private readonly ISendRepository _sendRepository;
private readonly GlobalSettings _globalSettings; private readonly GlobalSettings _globalSettings;
private readonly ICurrentContext _currentContext;
private readonly IFeatureService _featureService;
private bool UseFlexibleCollections =>
_featureService.IsEnabled(FeatureFlagKeys.FlexibleCollections, _currentContext);
public SyncController( public SyncController(
IUserService userService, IUserService userService,
@ -41,7 +48,9 @@ public class SyncController : Controller
IProviderUserRepository providerUserRepository, IProviderUserRepository providerUserRepository,
IPolicyRepository policyRepository, IPolicyRepository policyRepository,
ISendRepository sendRepository, ISendRepository sendRepository,
GlobalSettings globalSettings) GlobalSettings globalSettings,
ICurrentContext currentContext,
IFeatureService featureService)
{ {
_userService = userService; _userService = userService;
_folderRepository = folderRepository; _folderRepository = folderRepository;
@ -53,6 +62,8 @@ public class SyncController : Controller
_policyRepository = policyRepository; _policyRepository = policyRepository;
_sendRepository = sendRepository; _sendRepository = sendRepository;
_globalSettings = globalSettings; _globalSettings = globalSettings;
_currentContext = currentContext;
_featureService = featureService;
} }
[HttpGet("")] [HttpGet("")]
@ -74,7 +85,7 @@ public class SyncController : Controller
var hasEnabledOrgs = organizationUserDetails.Any(o => o.Enabled); var hasEnabledOrgs = organizationUserDetails.Any(o => o.Enabled);
var folders = await _folderRepository.GetManyByUserIdAsync(user.Id); 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); var sends = await _sendRepository.GetManyByUserIdAsync(user.Id);
IEnumerable<CollectionDetails> collections = null; IEnumerable<CollectionDetails> collections = null;

View File

@ -5,6 +5,7 @@ using Bit.Core.Auth.Enums;
using Bit.Core.Auth.Models; using Bit.Core.Auth.Models;
using Bit.Core.Auth.Models.Business.Tokenables; using Bit.Core.Auth.Models.Business.Tokenables;
using Bit.Core.Auth.Models.Data; using Bit.Core.Auth.Models.Data;
using Bit.Core.Context;
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
@ -33,6 +34,11 @@ public class EmergencyAccessService : IEmergencyAccessService
private readonly IPasswordHasher<User> _passwordHasher; private readonly IPasswordHasher<User> _passwordHasher;
private readonly IOrganizationService _organizationService; private readonly IOrganizationService _organizationService;
private readonly IDataProtectorTokenFactory<EmergencyAccessInviteTokenable> _dataProtectorTokenizer; private readonly IDataProtectorTokenFactory<EmergencyAccessInviteTokenable> _dataProtectorTokenizer;
private readonly ICurrentContext _currentContext;
private readonly IFeatureService _featureService;
private bool UseFlexibleCollections =>
_featureService.IsEnabled(FeatureFlagKeys.FlexibleCollections, _currentContext);
public EmergencyAccessService( public EmergencyAccessService(
IEmergencyAccessRepository emergencyAccessRepository, IEmergencyAccessRepository emergencyAccessRepository,
@ -46,7 +52,9 @@ public class EmergencyAccessService : IEmergencyAccessService
IPasswordHasher<User> passwordHasher, IPasswordHasher<User> passwordHasher,
GlobalSettings globalSettings, GlobalSettings globalSettings,
IOrganizationService organizationService, IOrganizationService organizationService,
IDataProtectorTokenFactory<EmergencyAccessInviteTokenable> dataProtectorTokenizer) IDataProtectorTokenFactory<EmergencyAccessInviteTokenable> dataProtectorTokenizer,
ICurrentContext currentContext,
IFeatureService featureService)
{ {
_emergencyAccessRepository = emergencyAccessRepository; _emergencyAccessRepository = emergencyAccessRepository;
_organizationUserRepository = organizationUserRepository; _organizationUserRepository = organizationUserRepository;
@ -60,6 +68,8 @@ public class EmergencyAccessService : IEmergencyAccessService
_globalSettings = globalSettings; _globalSettings = globalSettings;
_organizationService = organizationService; _organizationService = organizationService;
_dataProtectorTokenizer = dataProtectorTokenizer; _dataProtectorTokenizer = dataProtectorTokenizer;
_currentContext = currentContext;
_featureService = featureService;
} }
public async Task<EmergencyAccess> InviteAsync(User invitingUser, string email, EmergencyAccessType type, int waitTime) public async Task<EmergencyAccess> InviteAsync(User invitingUser, string email, EmergencyAccessType type, int waitTime)
@ -387,7 +397,7 @@ public class EmergencyAccessService : IEmergencyAccessService
throw new BadRequestException("Emergency Access not valid."); 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 return new EmergencyAccessViewData
{ {
@ -405,7 +415,7 @@ public class EmergencyAccessService : IEmergencyAccessService
throw new BadRequestException("Emergency Access not valid."); 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); return await _cipherService.GetAttachmentDownloadDataAsync(cipher, attachmentId);
} }

View File

@ -8,11 +8,11 @@ namespace Bit.Core.Vault.Repositories;
public interface ICipherRepository : IRepository<Cipher, Guid> public interface ICipherRepository : IRepository<Cipher, Guid>
{ {
Task<CipherDetails> GetByIdAsync(Guid id, Guid userId); Task<CipherDetails> GetByIdAsync(Guid id, Guid userId, bool useFlexibleCollections);
Task<CipherOrganizationDetails> GetOrganizationDetailsByIdAsync(Guid id); Task<CipherOrganizationDetails> GetOrganizationDetailsByIdAsync(Guid id);
Task<ICollection<CipherOrganizationDetails>> GetManyOrganizationDetailsByOrganizationIdAsync(Guid organizationId); Task<ICollection<CipherOrganizationDetails>> GetManyOrganizationDetailsByOrganizationIdAsync(Guid organizationId);
Task<bool> GetCanEditByIdAsync(Guid userId, Guid cipherId); Task<bool> GetCanEditByIdAsync(Guid userId, Guid cipherId);
Task<ICollection<CipherDetails>> GetManyByUserIdAsync(Guid userId, bool withOrganizations = true); Task<ICollection<CipherDetails>> GetManyByUserIdAsync(Guid userId, bool useFlexibleCollections, bool withOrganizations = true);
Task<ICollection<Cipher>> GetManyByOrganizationIdAsync(Guid organizationId); Task<ICollection<Cipher>> GetManyByOrganizationIdAsync(Guid organizationId);
Task CreateAsync(Cipher cipher, IEnumerable<Guid> collectionIds); Task CreateAsync(Cipher cipher, IEnumerable<Guid> collectionIds);
Task CreateAsync(CipherDetails cipher); Task CreateAsync(CipherDetails cipher);
@ -23,9 +23,9 @@ public interface ICipherRepository : IRepository<Cipher, Guid>
Task UpdatePartialAsync(Guid id, Guid userId, Guid? folderId, bool favorite); Task UpdatePartialAsync(Guid id, Guid userId, Guid? folderId, bool favorite);
Task UpdateAttachmentAsync(CipherAttachment attachment); Task UpdateAttachmentAsync(CipherAttachment attachment);
Task DeleteAttachmentAsync(Guid cipherId, string attachmentId); Task DeleteAttachmentAsync(Guid cipherId, string attachmentId);
Task DeleteAsync(IEnumerable<Guid> ids, Guid userId); Task DeleteAsync(IEnumerable<Guid> ids, Guid userId, bool useFlexibleCollections);
Task DeleteByIdsOrganizationIdAsync(IEnumerable<Guid> ids, Guid organizationId); Task DeleteByIdsOrganizationIdAsync(IEnumerable<Guid> ids, Guid organizationId);
Task MoveAsync(IEnumerable<Guid> ids, Guid? folderId, Guid userId); Task MoveAsync(IEnumerable<Guid> ids, Guid? folderId, Guid userId, bool useFlexibleCollections);
Task DeleteByUserIdAsync(Guid userId); Task DeleteByUserIdAsync(Guid userId);
Task DeleteByOrganizationIdAsync(Guid organizationId); Task DeleteByOrganizationIdAsync(Guid organizationId);
Task UpdateUserKeysAndCiphersAsync(User user, IEnumerable<Cipher> ciphers, IEnumerable<Folder> folders, IEnumerable<Send> sends); Task UpdateUserKeysAndCiphersAsync(User user, IEnumerable<Cipher> ciphers, IEnumerable<Folder> folders, IEnumerable<Send> sends);
@ -33,9 +33,9 @@ public interface ICipherRepository : IRepository<Cipher, Guid>
Task CreateAsync(IEnumerable<Cipher> ciphers, IEnumerable<Folder> folders); Task CreateAsync(IEnumerable<Cipher> ciphers, IEnumerable<Folder> folders);
Task CreateAsync(IEnumerable<Cipher> ciphers, IEnumerable<Collection> collections, Task CreateAsync(IEnumerable<Cipher> ciphers, IEnumerable<Collection> collections,
IEnumerable<CollectionCipher> collectionCiphers, IEnumerable<CollectionUser> collectionUsers); IEnumerable<CollectionCipher> collectionCiphers, IEnumerable<CollectionUser> collectionUsers);
Task SoftDeleteAsync(IEnumerable<Guid> ids, Guid userId); Task SoftDeleteAsync(IEnumerable<Guid> ids, Guid userId, bool useFlexibleCollections);
Task SoftDeleteByIdsOrganizationIdAsync(IEnumerable<Guid> ids, Guid organizationId); Task SoftDeleteByIdsOrganizationIdAsync(IEnumerable<Guid> ids, Guid organizationId);
Task<DateTime> RestoreAsync(IEnumerable<Guid> ids, Guid userId); Task<DateTime> RestoreAsync(IEnumerable<Guid> ids, Guid userId, bool useFlexibleCollections);
Task<DateTime> RestoreByIdsOrganizationIdAsync(IEnumerable<Guid> ids, Guid organizationId); Task<DateTime> RestoreByIdsOrganizationIdAsync(IEnumerable<Guid> ids, Guid organizationId);
Task DeleteDeletedAsync(DateTime deletedDateBefore); Task DeleteDeletedAsync(DateTime deletedDateBefore);
} }

View File

@ -38,6 +38,10 @@ public class CipherService : ICipherService
private const long _fileSizeLeeway = 1024L * 1024L; // 1MB private const long _fileSizeLeeway = 1024L * 1024L; // 1MB
private readonly IReferenceEventService _referenceEventService; private readonly IReferenceEventService _referenceEventService;
private readonly ICurrentContext _currentContext; private readonly ICurrentContext _currentContext;
private readonly IFeatureService _featureService;
private bool UseFlexibleCollections =>
_featureService.IsEnabled(FeatureFlagKeys.FlexibleCollections, _currentContext);
public CipherService( public CipherService(
ICipherRepository cipherRepository, ICipherRepository cipherRepository,
@ -54,7 +58,8 @@ public class CipherService : ICipherService
IPolicyService policyService, IPolicyService policyService,
GlobalSettings globalSettings, GlobalSettings globalSettings,
IReferenceEventService referenceEventService, IReferenceEventService referenceEventService,
ICurrentContext currentContext) ICurrentContext currentContext,
IFeatureService featureService)
{ {
_cipherRepository = cipherRepository; _cipherRepository = cipherRepository;
_folderRepository = folderRepository; _folderRepository = folderRepository;
@ -71,6 +76,7 @@ public class CipherService : ICipherService
_globalSettings = globalSettings; _globalSettings = globalSettings;
_referenceEventService = referenceEventService; _referenceEventService = referenceEventService;
_currentContext = currentContext; _currentContext = currentContext;
_featureService = featureService;
} }
public async Task SaveAsync(Cipher cipher, Guid savingUserId, DateTime? lastKnownRevisionDate, public async Task SaveAsync(Cipher cipher, Guid savingUserId, DateTime? lastKnownRevisionDate,
@ -424,9 +430,10 @@ public class CipherService : ICipherService
} }
else 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(); 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 => 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 // push
await _pushService.PushSyncCiphersAsync(movingUserId); await _pushService.PushSyncCiphersAsync(movingUserId);
} }
@ -865,9 +872,10 @@ public class CipherService : ICipherService
} }
else 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(); 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 => var events = deletingCiphers.Select(c =>
@ -930,9 +938,10 @@ public class CipherService : ICipherService
} }
else 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(); 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 => var events = restoringCiphers.Select(c =>
@ -967,7 +976,7 @@ public class CipherService : ICipherService
} }
else 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); orgCiphers = ciphers.Where(c => c.OrganizationId == organizationId);
} }

View File

@ -1,4 +1,5 @@
using Bit.Core.Context; using Bit.Core;
using Bit.Core.Context;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Services; using Bit.Core.Services;
@ -18,19 +19,24 @@ public class CollectController : Controller
private readonly IEventService _eventService; private readonly IEventService _eventService;
private readonly ICipherRepository _cipherRepository; private readonly ICipherRepository _cipherRepository;
private readonly IOrganizationRepository _organizationRepository; private readonly IOrganizationRepository _organizationRepository;
private readonly IFeatureService _featureService;
public CollectController( public CollectController(
ICurrentContext currentContext, ICurrentContext currentContext,
IEventService eventService, IEventService eventService,
ICipherRepository cipherRepository, ICipherRepository cipherRepository,
IOrganizationRepository organizationRepository) IOrganizationRepository organizationRepository,
IFeatureService featureService)
{ {
_currentContext = currentContext; _currentContext = currentContext;
_eventService = eventService; _eventService = eventService;
_cipherRepository = cipherRepository; _cipherRepository = cipherRepository;
_organizationRepository = organizationRepository; _organizationRepository = organizationRepository;
_featureService = featureService;
} }
bool UseFlexibleCollections => _featureService.IsEnabled(FeatureFlagKeys.FlexibleCollections, _currentContext);
[HttpPost] [HttpPost]
public async Task<IActionResult> Post([FromBody] IEnumerable<EventModel> model) public async Task<IActionResult> Post([FromBody] IEnumerable<EventModel> model)
{ {
@ -69,8 +75,10 @@ public class CollectController : Controller
} }
else else
{ {
var useFlexibleCollections = _featureService.IsEnabled(FeatureFlagKeys.FlexibleCollections, _currentContext);
cipher = await _cipherRepository.GetByIdAsync(eventModel.CipherId.Value, cipher = await _cipherRepository.GetByIdAsync(eventModel.CipherId.Value,
_currentContext.UserId.Value); _currentContext.UserId.Value,
useFlexibleCollections);
} }
if (cipher == null) if (cipher == null)
{ {

View File

@ -22,12 +22,16 @@ public class CipherRepository : Repository<Cipher, Guid>, ICipherRepository
: base(connectionString, readOnlyConnectionString) : base(connectionString, readOnlyConnectionString)
{ } { }
public async Task<CipherDetails> GetByIdAsync(Guid id, Guid userId) public async Task<CipherDetails> GetByIdAsync(Guid id, Guid userId, bool useFlexibleCollections)
{ {
var sprocName = useFlexibleCollections
? $"[{Schema}].[CipherDetails_ReadByIdUserId_V2]"
: $"[{Schema}].[CipherDetails_ReadByIdUserId]";
using (var connection = new SqlConnection(ConnectionString)) using (var connection = new SqlConnection(ConnectionString))
{ {
var results = await connection.QueryAsync<CipherDetails>( var results = await connection.QueryAsync<CipherDetails>(
$"[{Schema}].[CipherDetails_ReadByIdUserId]", sprocName,
new { Id = id, UserId = userId }, new { Id = id, UserId = userId },
commandType: CommandType.StoredProcedure); commandType: CommandType.StoredProcedure);
@ -75,12 +79,14 @@ public class CipherRepository : Repository<Cipher, Guid>, ICipherRepository
} }
} }
public async Task<ICollection<CipherDetails>> GetManyByUserIdAsync(Guid userId, bool withOrganizations = true) public async Task<ICollection<CipherDetails>> GetManyByUserIdAsync(Guid userId, bool useFlexibleCollections, bool withOrganizations = true)
{ {
string sprocName = null; string sprocName = null;
if (withOrganizations) if (withOrganizations)
{ {
sprocName = $"[{Schema}].[CipherDetails_ReadByUserId]"; sprocName = useFlexibleCollections
? $"[{Schema}].[CipherDetails_ReadByUserId_V2]"
: $"[{Schema}].[CipherDetails_ReadByUserId]";
} }
else else
{ {
@ -228,12 +234,16 @@ public class CipherRepository : Repository<Cipher, Guid>, ICipherRepository
} }
} }
public async Task DeleteAsync(IEnumerable<Guid> ids, Guid userId) public async Task DeleteAsync(IEnumerable<Guid> ids, Guid userId, bool useFlexibleCollections)
{ {
var sprocName = useFlexibleCollections
? $"[{Schema}].[Cipher_Delete_V2]"
: $"[{Schema}].[Cipher_Delete]";
using (var connection = new SqlConnection(ConnectionString)) using (var connection = new SqlConnection(ConnectionString))
{ {
var results = await connection.ExecuteAsync( var results = await connection.ExecuteAsync(
$"[{Schema}].[Cipher_Delete]", sprocName,
new { Ids = ids.ToGuidIdArrayTVP(), UserId = userId }, new { Ids = ids.ToGuidIdArrayTVP(), UserId = userId },
commandType: CommandType.StoredProcedure); commandType: CommandType.StoredProcedure);
} }
@ -261,12 +271,16 @@ public class CipherRepository : Repository<Cipher, Guid>, ICipherRepository
} }
} }
public async Task MoveAsync(IEnumerable<Guid> ids, Guid? folderId, Guid userId) public async Task MoveAsync(IEnumerable<Guid> ids, Guid? folderId, Guid userId, bool useFlexibleCollections)
{ {
var sprocName = useFlexibleCollections
? $"[{Schema}].[Cipher_Move_V2]"
: $"[{Schema}].[Cipher_Move]";
using (var connection = new SqlConnection(ConnectionString)) using (var connection = new SqlConnection(ConnectionString))
{ {
var results = await connection.ExecuteAsync( var results = await connection.ExecuteAsync(
$"[{Schema}].[Cipher_Move]", sprocName,
new { Ids = ids.ToGuidIdArrayTVP(), FolderId = folderId, UserId = userId }, new { Ids = ids.ToGuidIdArrayTVP(), FolderId = folderId, UserId = userId },
commandType: CommandType.StoredProcedure); commandType: CommandType.StoredProcedure);
} }
@ -657,23 +671,31 @@ public class CipherRepository : Repository<Cipher, Guid>, ICipherRepository
} }
} }
public async Task SoftDeleteAsync(IEnumerable<Guid> ids, Guid userId) public async Task SoftDeleteAsync(IEnumerable<Guid> ids, Guid userId, bool useFlexibleCollections)
{ {
var sprocName = useFlexibleCollections
? $"[{Schema}].[Cipher_SoftDelete_V2]"
: $"[{Schema}].[Cipher_SoftDelete]";
using (var connection = new SqlConnection(ConnectionString)) using (var connection = new SqlConnection(ConnectionString))
{ {
var results = await connection.ExecuteAsync( var results = await connection.ExecuteAsync(
$"[{Schema}].[Cipher_SoftDelete]", sprocName,
new { Ids = ids.ToGuidIdArrayTVP(), UserId = userId }, new { Ids = ids.ToGuidIdArrayTVP(), UserId = userId },
commandType: CommandType.StoredProcedure); commandType: CommandType.StoredProcedure);
} }
} }
public async Task<DateTime> RestoreAsync(IEnumerable<Guid> ids, Guid userId) public async Task<DateTime> RestoreAsync(IEnumerable<Guid> ids, Guid userId, bool useFlexibleCollections)
{ {
var sprocName = useFlexibleCollections
? $"[{Schema}].[Cipher_Restore_V2]"
: $"[{Schema}].[Cipher_Restore]";
using (var connection = new SqlConnection(ConnectionString)) using (var connection = new SqlConnection(ConnectionString))
{ {
var results = await connection.ExecuteScalarAsync<DateTime>( var results = await connection.ExecuteScalarAsync<DateTime>(
$"[{Schema}].[Cipher_Restore]", sprocName,
new { Ids = ids.ToGuidIdArrayTVP(), UserId = userId }, new { Ids = ids.ToGuidIdArrayTVP(), UserId = userId },
commandType: CommandType.StoredProcedure); commandType: CommandType.StoredProcedure);

View File

@ -8,11 +8,21 @@ namespace Bit.Infrastructure.EntityFramework.Repositories.Queries;
public class UserCipherDetailsQuery : IQuery<CipherDetails> public class UserCipherDetailsQuery : IQuery<CipherDetails>
{ {
private readonly Guid? _userId; private readonly Guid? _userId;
public UserCipherDetailsQuery(Guid? userId) private readonly bool _useFlexibleCollections;
public UserCipherDetailsQuery(Guid? userId, bool useFlexibleCollections)
{ {
_userId = userId; _userId = userId;
_useFlexibleCollections = useFlexibleCollections;
} }
public virtual IQueryable<CipherDetails> Run(DatabaseContext dbContext) public virtual IQueryable<CipherDetails> Run(DatabaseContext dbContext)
{
return _useFlexibleCollections
? Run_VNext(dbContext)
: Run_VCurrent(dbContext);
}
private IQueryable<CipherDetails> Run_VCurrent(DatabaseContext dbContext)
{ {
var query = from c in dbContext.Ciphers var query = from c in dbContext.Ciphers
@ -78,6 +88,71 @@ public class UserCipherDetailsQuery : IQuery<CipherDetails>
return union; return union;
} }
private IQueryable<CipherDetails> 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) private static Guid? GetFolderId(Guid? userId, Cipher cipher)
{ {
try try

View File

@ -199,9 +199,9 @@ public class CipherRepository : Repository<Core.Vault.Entities.Cipher, Cipher, G
} }
} }
public async Task DeleteAsync(IEnumerable<Guid> ids, Guid userId) public async Task DeleteAsync(IEnumerable<Guid> 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) public async Task DeleteAttachmentAsync(Guid cipherId, string attachmentId)
@ -302,12 +302,12 @@ public class CipherRepository : Repository<Core.Vault.Entities.Cipher, Cipher, G
} }
} }
public async Task<CipherDetails> GetByIdAsync(Guid id, Guid userId) public async Task<CipherDetails> GetByIdAsync(Guid id, Guid userId, bool useFlexibleCollections)
{ {
using (var scope = ServiceScopeFactory.CreateScope()) using (var scope = ServiceScopeFactory.CreateScope())
{ {
var dbContext = GetDatabaseContext(scope); 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); var data = await userCipherDetails.Run(dbContext).FirstOrDefaultAsync(c => c.Id == id);
return data; return data;
} }
@ -347,13 +347,13 @@ public class CipherRepository : Repository<Core.Vault.Entities.Cipher, Cipher, G
} }
} }
public async Task<ICollection<CipherDetails>> GetManyByUserIdAsync(Guid userId, bool withOrganizations = true) public async Task<ICollection<CipherDetails>> GetManyByUserIdAsync(Guid userId, bool useFlexibleCollections, bool withOrganizations = true)
{ {
using (var scope = ServiceScopeFactory.CreateScope()) using (var scope = ServiceScopeFactory.CreateScope())
{ {
var dbContext = GetDatabaseContext(scope); var dbContext = GetDatabaseContext(scope);
IQueryable<CipherDetails> cipherDetailsView = withOrganizations ? IQueryable<CipherDetails> cipherDetailsView = withOrganizations ?
new UserCipherDetailsQuery(userId).Run(dbContext) : new UserCipherDetailsQuery(userId, useFlexibleCollections).Run(dbContext) :
new CipherDetailsQuery(userId).Run(dbContext); new CipherDetailsQuery(userId).Run(dbContext);
if (!withOrganizations) if (!withOrganizations)
{ {
@ -395,13 +395,13 @@ public class CipherRepository : Repository<Core.Vault.Entities.Cipher, Cipher, G
} }
} }
public async Task MoveAsync(IEnumerable<Guid> ids, Guid? folderId, Guid userId) public async Task MoveAsync(IEnumerable<Guid> ids, Guid? folderId, Guid userId, bool useFlexibleCollections)
{ {
using (var scope = ServiceScopeFactory.CreateScope()) using (var scope = ServiceScopeFactory.CreateScope())
{ {
var dbContext = GetDatabaseContext(scope); var dbContext = GetDatabaseContext(scope);
var cipherEntities = dbContext.Ciphers.Where(c => ids.Contains(c.Id)); 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 var idsToMove = from ucd in userCipherDetails
join c in cipherEntities join c in cipherEntities
on ucd.Id equals c.Id on ucd.Id equals c.Id
@ -632,9 +632,9 @@ public class CipherRepository : Repository<Core.Vault.Entities.Cipher, Cipher, G
} }
} }
public async Task<DateTime> RestoreAsync(IEnumerable<Guid> ids, Guid userId) public async Task<DateTime> RestoreAsync(IEnumerable<Guid> ids, Guid userId, bool useFlexibleCollections)
{ {
return await ToggleCipherStates(ids, userId, CipherStateAction.Restore); return await ToggleCipherStates(ids, userId, CipherStateAction.Restore, useFlexibleCollections);
} }
public async Task<DateTime> RestoreByIdsOrganizationIdAsync(IEnumerable<Guid> ids, Guid organizationId) public async Task<DateTime> RestoreByIdsOrganizationIdAsync(IEnumerable<Guid> ids, Guid organizationId)
@ -662,12 +662,12 @@ public class CipherRepository : Repository<Core.Vault.Entities.Cipher, Cipher, G
} }
} }
public async Task SoftDeleteAsync(IEnumerable<Guid> ids, Guid userId) public async Task SoftDeleteAsync(IEnumerable<Guid> ids, Guid userId, bool useFlexibleCollections)
{ {
await ToggleCipherStates(ids, userId, CipherStateAction.SoftDelete); await ToggleCipherStates(ids, userId, CipherStateAction.SoftDelete, useFlexibleCollections);
} }
private async Task<DateTime> ToggleCipherStates(IEnumerable<Guid> ids, Guid userId, CipherStateAction action) private async Task<DateTime> ToggleCipherStates(IEnumerable<Guid> ids, Guid userId, CipherStateAction action, bool useFlexibleCollections)
{ {
static bool FilterDeletedDate(CipherStateAction action, CipherDetails ucd) static bool FilterDeletedDate(CipherStateAction action, CipherDetails ucd)
{ {
@ -682,7 +682,7 @@ public class CipherRepository : Repository<Core.Vault.Entities.Cipher, Cipher, G
using (var scope = ServiceScopeFactory.CreateScope()) using (var scope = ServiceScopeFactory.CreateScope())
{ {
var dbContext = GetDatabaseContext(scope); var dbContext = GetDatabaseContext(scope);
var userCipherDetailsQuery = new UserCipherDetailsQuery(userId); var userCipherDetailsQuery = new UserCipherDetailsQuery(userId, useFlexibleCollections);
var cipherEntitiesToCheck = await (dbContext.Ciphers.Where(c => ids.Contains(c.Id))).ToListAsync(); var cipherEntitiesToCheck = await (dbContext.Ciphers.Where(c => ids.Contains(c.Id))).ToListAsync();
var query = from ucd in await (userCipherDetailsQuery.Run(dbContext)).ToListAsync() var query = from ucd in await (userCipherDetailsQuery.Run(dbContext)).ToListAsync()
join c in cipherEntitiesToCheck join c in cipherEntitiesToCheck

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,11 @@
CREATE PROCEDURE [dbo].[CipherDetails_ReadByUserId_V2]
@UserId UNIQUEIDENTIFIER
AS
BEGIN
SET NOCOUNT ON
SELECT
*
FROM
[dbo].[UserCipherDetails_V2](@UserId)
END

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -34,10 +34,10 @@ public class CiphersControllerTests
}; };
sutProvider.GetDependency<ICipherRepository>() sutProvider.GetDependency<ICipherRepository>()
.GetByIdAsync(cipherId, userId) .GetByIdAsync(cipherId, userId, Arg.Any<bool>())
.Returns(Task.FromResult(cipherDetails)); .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(folderId, result.FolderId);
Assert.Equal(isFavorite, result.Favorite); Assert.Equal(isFavorite, result.Favorite);

View File

@ -107,7 +107,7 @@ public class SyncControllerTests
.Returns(providerUserOrganizationDetails); .Returns(providerUserOrganizationDetails);
folderRepository.GetManyByUserIdAsync(user.Id).Returns(folders); folderRepository.GetManyByUserIdAsync(user.Id).Returns(folders);
cipherRepository.GetManyByUserIdAsync(user.Id).Returns(ciphers); cipherRepository.GetManyByUserIdAsync(user.Id, useFlexibleCollections: Arg.Any<bool>()).Returns(ciphers);
sendRepository sendRepository
.GetManyByUserIdAsync(user.Id).Returns(sends); .GetManyByUserIdAsync(user.Id).Returns(sends);
@ -198,7 +198,7 @@ public class SyncControllerTests
.Returns(providerUserOrganizationDetails); .Returns(providerUserOrganizationDetails);
folderRepository.GetManyByUserIdAsync(user.Id).Returns(folders); folderRepository.GetManyByUserIdAsync(user.Id).Returns(folders);
cipherRepository.GetManyByUserIdAsync(user.Id).Returns(ciphers); cipherRepository.GetManyByUserIdAsync(user.Id, useFlexibleCollections: Arg.Any<bool>()).Returns(ciphers);
sendRepository sendRepository
.GetManyByUserIdAsync(user.Id).Returns(sends); .GetManyByUserIdAsync(user.Id).Returns(sends);
@ -272,7 +272,7 @@ public class SyncControllerTests
.Returns(providerUserOrganizationDetails); .Returns(providerUserOrganizationDetails);
folderRepository.GetManyByUserIdAsync(user.Id).Returns(folders); folderRepository.GetManyByUserIdAsync(user.Id).Returns(folders);
cipherRepository.GetManyByUserIdAsync(user.Id).Returns(ciphers); cipherRepository.GetManyByUserIdAsync(user.Id, useFlexibleCollections: Arg.Any<bool>()).Returns(ciphers);
sendRepository sendRepository
.GetManyByUserIdAsync(user.Id).Returns(sends); .GetManyByUserIdAsync(user.Id).Returns(sends);
@ -335,7 +335,7 @@ public class SyncControllerTests
.GetManyByUserIdAsync(default); .GetManyByUserIdAsync(default);
await cipherRepository.ReceivedWithAnyArgs(1) await cipherRepository.ReceivedWithAnyArgs(1)
.GetManyByUserIdAsync(default); .GetManyByUserIdAsync(default, useFlexibleCollections: default);
await sendRepository.ReceivedWithAnyArgs(1) await sendRepository.ReceivedWithAnyArgs(1)
.GetManyByUserIdAsync(default); .GetManyByUserIdAsync(default);

View File

@ -675,9 +675,9 @@ public class CipherServiceTests
cipher.RevisionDate = previousRevisionDate; cipher.RevisionDate = previousRevisionDate;
} }
sutProvider.GetDependency<ICipherRepository>().GetManyByUserIdAsync(restoringUserId).Returns(ciphers); sutProvider.GetDependency<ICipherRepository>().GetManyByUserIdAsync(restoringUserId, useFlexibleCollections: Arg.Any<bool>()).Returns(ciphers);
var revisionDate = previousRevisionDate + TimeSpan.FromMinutes(1); var revisionDate = previousRevisionDate + TimeSpan.FromMinutes(1);
sutProvider.GetDependency<ICipherRepository>().RestoreAsync(Arg.Any<IEnumerable<Guid>>(), restoringUserId).Returns(revisionDate); sutProvider.GetDependency<ICipherRepository>().RestoreAsync(Arg.Any<IEnumerable<Guid>>(), restoringUserId, Arg.Any<bool>()).Returns(revisionDate);
await sutProvider.Sut.RestoreManyAsync(cipherIds, restoringUserId); await sutProvider.Sut.RestoreManyAsync(cipherIds, restoringUserId);
@ -789,8 +789,8 @@ public class CipherServiceTests
await sutProvider.GetDependency<ICipherRepository>().DidNotReceiveWithAnyArgs().GetManyOrganizationDetailsByOrganizationIdAsync(default); await sutProvider.GetDependency<ICipherRepository>().DidNotReceiveWithAnyArgs().GetManyOrganizationDetailsByOrganizationIdAsync(default);
await sutProvider.GetDependency<ICipherRepository>().DidNotReceiveWithAnyArgs().RestoreByIdsOrganizationIdAsync(default, default); await sutProvider.GetDependency<ICipherRepository>().DidNotReceiveWithAnyArgs().RestoreByIdsOrganizationIdAsync(default, default);
await sutProvider.GetDependency<ICipherRepository>().DidNotReceiveWithAnyArgs().RestoreByIdsOrganizationIdAsync(default, default); await sutProvider.GetDependency<ICipherRepository>().DidNotReceiveWithAnyArgs().RestoreByIdsOrganizationIdAsync(default, default);
await sutProvider.GetDependency<ICipherRepository>().DidNotReceiveWithAnyArgs().GetManyByUserIdAsync(default); await sutProvider.GetDependency<ICipherRepository>().DidNotReceiveWithAnyArgs().GetManyByUserIdAsync(default, useFlexibleCollections: default);
await sutProvider.GetDependency<ICipherRepository>().DidNotReceiveWithAnyArgs().RestoreAsync(default, default); await sutProvider.GetDependency<ICipherRepository>().DidNotReceiveWithAnyArgs().RestoreAsync(default, default, default);
await sutProvider.GetDependency<IEventService>().DidNotReceiveWithAnyArgs().LogCipherEventsAsync(default); await sutProvider.GetDependency<IEventService>().DidNotReceiveWithAnyArgs().LogCipherEventsAsync(default);
await sutProvider.GetDependency<IPushNotificationService>().DidNotReceiveWithAnyArgs().PushSyncCiphersAsync(default); await sutProvider.GetDependency<IPushNotificationService>().DidNotReceiveWithAnyArgs().PushSyncCiphersAsync(default);
} }

View File

@ -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