1
0
mirror of https://github.com/bitwarden/server.git synced 2024-11-21 12:05:42 +01: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.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));
}

View File

@ -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<Cipher>();
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)));
}

View File

@ -40,6 +40,10 @@ public class CiphersController : Controller
private readonly ILogger<CiphersController> _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<CiphersController> 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<CipherResponseModel> Get(string id)
public async Task<CipherResponseModel> 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<CipherDetailsResponseModel> GetDetails(string id)
public async Task<CipherDetailsResponseModel> 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<Guid, IGrouping<Guid, CollectionCipher>> collectionCiphersGroupDict = null;
if (hasOrgs)
{
@ -175,7 +180,7 @@ public class CiphersController : Controller
public async Task<CipherResponseModel> 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<CipherResponseModel> PutPartial(string id, [FromBody] CipherPartialRequestModel model)
public async Task<CipherResponseModel> 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<CipherResponseModel> PutShare(string id, [FromBody] CipherShareRequestModel model)
public async Task<CipherResponseModel> 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<CipherResponseModel> PutRestore(string id)
public async Task<CipherResponseModel> 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<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 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<AttachmentUploadDataResponseModel> RenewFileUploadUrl(string id, string attachmentId)
public async Task<AttachmentUploadDataResponseModel> 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<CipherResponseModel> PostAttachment(string id)
public async Task<CipherResponseModel> 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<AttachmentResponseModel> GetAttachmentData(string id, string attachmentId)
public async Task<AttachmentResponseModel> 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<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.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<CollectionDetails> collections = null;

View File

@ -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<User> _passwordHasher;
private readonly IOrganizationService _organizationService;
private readonly IDataProtectorTokenFactory<EmergencyAccessInviteTokenable> _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<User> passwordHasher,
GlobalSettings globalSettings,
IOrganizationService organizationService,
IDataProtectorTokenFactory<EmergencyAccessInviteTokenable> dataProtectorTokenizer)
IDataProtectorTokenFactory<EmergencyAccessInviteTokenable> 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<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.");
}
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);
}

View File

@ -8,11 +8,11 @@ namespace Bit.Core.Vault.Repositories;
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<ICollection<CipherOrganizationDetails>> GetManyOrganizationDetailsByOrganizationIdAsync(Guid organizationId);
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 CreateAsync(Cipher cipher, IEnumerable<Guid> collectionIds);
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 UpdateAttachmentAsync(CipherAttachment attachment);
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 MoveAsync(IEnumerable<Guid> ids, Guid? folderId, Guid userId);
Task MoveAsync(IEnumerable<Guid> ids, Guid? folderId, Guid userId, bool useFlexibleCollections);
Task DeleteByUserIdAsync(Guid userId);
Task DeleteByOrganizationIdAsync(Guid organizationId);
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<Collection> collections,
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<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 DeleteDeletedAsync(DateTime deletedDateBefore);
}

View File

@ -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);
}

View File

@ -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<IActionResult> Post([FromBody] IEnumerable<EventModel> 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)
{

View File

@ -22,12 +22,16 @@ public class CipherRepository : Repository<Cipher, Guid>, ICipherRepository
: 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))
{
var results = await connection.QueryAsync<CipherDetails>(
$"[{Schema}].[CipherDetails_ReadByIdUserId]",
sprocName,
new { Id = id, UserId = userId },
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;
if (withOrganizations)
{
sprocName = $"[{Schema}].[CipherDetails_ReadByUserId]";
sprocName = useFlexibleCollections
? $"[{Schema}].[CipherDetails_ReadByUserId_V2]"
: $"[{Schema}].[CipherDetails_ReadByUserId]";
}
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))
{
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<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))
{
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<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))
{
var results = await connection.ExecuteAsync(
$"[{Schema}].[Cipher_SoftDelete]",
sprocName,
new { Ids = ids.ToGuidIdArrayTVP(), UserId = userId },
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))
{
var results = await connection.ExecuteScalarAsync<DateTime>(
$"[{Schema}].[Cipher_Restore]",
sprocName,
new { Ids = ids.ToGuidIdArrayTVP(), UserId = userId },
commandType: CommandType.StoredProcedure);

View File

@ -8,11 +8,21 @@ namespace Bit.Infrastructure.EntityFramework.Repositories.Queries;
public class UserCipherDetailsQuery : IQuery<CipherDetails>
{
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<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
@ -78,6 +88,71 @@ public class UserCipherDetailsQuery : IQuery<CipherDetails>
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)
{
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)
@ -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())
{
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<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())
{
var dbContext = GetDatabaseContext(scope);
IQueryable<CipherDetails> 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<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())
{
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<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)
@ -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)
{
@ -682,7 +682,7 @@ public class CipherRepository : Repository<Core.Vault.Entities.Cipher, Cipher, G
using (var scope = ServiceScopeFactory.CreateScope())
{
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 query = from ucd in await (userCipherDetailsQuery.Run(dbContext)).ToListAsync()
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>()
.GetByIdAsync(cipherId, userId)
.GetByIdAsync(cipherId, userId, Arg.Any<bool>())
.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);

View File

@ -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<bool>()).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<bool>()).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<bool>()).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);

View File

@ -675,9 +675,9 @@ public class CipherServiceTests
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);
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);
@ -789,8 +789,8 @@ public class CipherServiceTests
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().GetManyByUserIdAsync(default);
await sutProvider.GetDependency<ICipherRepository>().DidNotReceiveWithAnyArgs().RestoreAsync(default, default);
await sutProvider.GetDependency<ICipherRepository>().DidNotReceiveWithAnyArgs().GetManyByUserIdAsync(default, useFlexibleCollections: default);
await sutProvider.GetDependency<ICipherRepository>().DidNotReceiveWithAnyArgs().RestoreAsync(default, default, default);
await sutProvider.GetDependency<IEventService>().DidNotReceiveWithAnyArgs().LogCipherEventsAsync(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