From 972290d1ec85173478bccc8d70b86dcd3446e2a5 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 29 Dec 2015 21:45:21 -0500 Subject: [PATCH] Added retrt logic to all documentdb queries. Updated change password and email process to use multi step for cirty ciphers and replace user. Fixed RefreshSecurityStampAsync to not dirty ciphers. --- .../Identity/JwtBearerEventImplementations.cs | 2 +- src/Core/Identity/RoleStore.cs | 4 +- src/Core/Identity/UserStore.cs | 16 +-- .../DocumentDB/CipherRepository.cs | 26 ++++- .../DocumentDB/FolderRepository.cs | 39 +++++-- .../Repositories/DocumentDB/Repository.cs | 80 ++++++++++--- .../Repositories/DocumentDB/SiteRepository.cs | 39 +++++-- .../Stored Procedures/dirtyCiphers.js | 63 +++++++++++ .../replaceUserAndDirtyCiphers.js | 107 ------------------ ...eDirtyCiphers.js => updateDirtyCiphers.js} | 2 +- .../Repositories/DocumentDB/UserRepository.cs | 24 ++-- .../DocumentDB/Utilities/DocumentDBHelpers.cs | 18 ++- src/Core/Repositories/ICipherRepository.cs | 1 + src/Core/Repositories/IUserRepository.cs | 1 - src/Core/Services/UserService.cs | 8 +- 15 files changed, 250 insertions(+), 180 deletions(-) create mode 100644 src/Core/Repositories/DocumentDB/Stored Procedures/dirtyCiphers.js delete mode 100644 src/Core/Repositories/DocumentDB/Stored Procedures/replaceUserAndDirtyCiphers.js rename src/Core/Repositories/DocumentDB/Stored Procedures/{bulkUpdateDirtyCiphers.js => updateDirtyCiphers.js} (97%) diff --git a/src/Core/Identity/JwtBearerEventImplementations.cs b/src/Core/Identity/JwtBearerEventImplementations.cs index 124d1a8ed..42b6d4a99 100644 --- a/src/Core/Identity/JwtBearerEventImplementations.cs +++ b/src/Core/Identity/JwtBearerEventImplementations.cs @@ -44,7 +44,7 @@ namespace Bit.Core.Identity context.AuthenticationTicket = new AuthenticationTicket(context.HttpContext.User, new AuthenticationProperties(), context.Options.AuthenticationScheme); } - return Task.FromResult(null); + return Task.FromResult(0); } } } diff --git a/src/Core/Identity/RoleStore.cs b/src/Core/Identity/RoleStore.cs index 4a4a125e4..ebb29e56e 100644 --- a/src/Core/Identity/RoleStore.cs +++ b/src/Core/Identity/RoleStore.cs @@ -47,13 +47,13 @@ namespace Bit.Core.Identity public Task SetNormalizedRoleNameAsync(Role role, string normalizedName, CancellationToken cancellationToken) { - return Task.FromResult(null); + return Task.FromResult(0); } public Task SetRoleNameAsync(Role role, string roleName, CancellationToken cancellationToken) { role.Name = roleName; - return Task.FromResult(null); + return Task.FromResult(0); } public Task UpdateAsync(Role role, CancellationToken cancellationToken) diff --git a/src/Core/Identity/UserStore.cs b/src/Core/Identity/UserStore.cs index 354ce6e80..a68f2e356 100644 --- a/src/Core/Identity/UserStore.cs +++ b/src/Core/Identity/UserStore.cs @@ -115,37 +115,37 @@ namespace Bit.Core.Identity public Task SetEmailAsync(User user, string email, CancellationToken cancellationToken = default(CancellationToken)) { user.Email = email; - return Task.FromResult(null); + return Task.FromResult(0); } public Task SetEmailConfirmedAsync(User user, bool confirmed, CancellationToken cancellationToken = default(CancellationToken)) { // do nothing - return Task.FromResult(null); + return Task.FromResult(0); } public Task SetNormalizedEmailAsync(User user, string normalizedEmail, CancellationToken cancellationToken = default(CancellationToken)) { user.Email = normalizedEmail; - return Task.FromResult(null); + return Task.FromResult(0); } public Task SetNormalizedUserNameAsync(User user, string normalizedName, CancellationToken cancellationToken = default(CancellationToken)) { user.Email = normalizedName; - return Task.FromResult(null); + return Task.FromResult(0); } public Task SetPasswordHashAsync(User user, string passwordHash, CancellationToken cancellationToken = default(CancellationToken)) { user.MasterPassword = passwordHash; - return Task.FromResult(null); + return Task.FromResult(0); } public Task SetUserNameAsync(User user, string userName, CancellationToken cancellationToken = default(CancellationToken)) { user.Email = userName; - return Task.FromResult(null); + return Task.FromResult(0); } public async Task UpdateAsync(User user, CancellationToken cancellationToken = default(CancellationToken)) @@ -157,7 +157,7 @@ namespace Bit.Core.Identity public Task SetTwoFactorEnabledAsync(User user, bool enabled, CancellationToken cancellationToken) { user.TwoFactorEnabled = enabled; - return Task.FromResult(null); + return Task.FromResult(0); } public Task GetTwoFactorEnabledAsync(User user, CancellationToken cancellationToken) @@ -168,7 +168,7 @@ namespace Bit.Core.Identity public Task SetSecurityStampAsync(User user, string stamp, CancellationToken cancellationToken) { user.SecurityStamp = stamp; - return Task.FromResult(null); + return Task.FromResult(0); } public Task GetSecurityStampAsync(User user, CancellationToken cancellationToken) diff --git a/src/Core/Repositories/DocumentDB/CipherRepository.cs b/src/Core/Repositories/DocumentDB/CipherRepository.cs index 533ec6d1c..14eafac48 100644 --- a/src/Core/Repositories/DocumentDB/CipherRepository.cs +++ b/src/Core/Repositories/DocumentDB/CipherRepository.cs @@ -14,6 +14,24 @@ namespace Bit.Core.Repositories.DocumentDB : base(client, databaseId, documentType) { } + public async Task DirtyCiphersAsync(string userId) + { + await DocumentDBHelpers.ExecuteWithRetryAsync(async () => + { + while(true) + { + StoredProcedureResponse sprocResponse = await Client.ExecuteStoredProcedureAsync( + ResolveSprocIdLink(userId, "dirtyCiphers"), + userId); + + if(!(bool)sprocResponse.Response.continuation) + { + break; + } + } + }); + } + public async Task UpdateDirtyCiphersAsync(IEnumerable ciphers) { await DocumentDBHelpers.ExecuteWithRetryAsync(async () => @@ -27,9 +45,8 @@ namespace Bit.Core.Repositories.DocumentDB var userId = ((Cipher)cleanedCiphers.First()).UserId; StoredProcedureResponse sprocResponse = await Client.ExecuteStoredProcedureAsync( - ResolveSprocIdLink(userId, "bulkUpdateDirtyCiphers"), - // Do sets of 50. Recursion will handle the rest below. - cleanedCiphers.Take(50), + ResolveSprocIdLink(userId, "updateDirtyCiphers"), + cleanedCiphers, userId); var replacedCount = sprocResponse.Response; @@ -54,8 +71,7 @@ namespace Bit.Core.Repositories.DocumentDB var userId = ((Cipher)cleanedCiphers.First()).UserId; StoredProcedureResponse sprocResponse = await Client.ExecuteStoredProcedureAsync( ResolveSprocIdLink(userId, "bulkCreate"), - // Do sets of 50. Recursion will handle the rest below. - cleanedCiphers.Take(50)); + cleanedCiphers); var createdCount = sprocResponse.Response; if(createdCount != cleanedCiphers.Count()) diff --git a/src/Core/Repositories/DocumentDB/FolderRepository.cs b/src/Core/Repositories/DocumentDB/FolderRepository.cs index eb21bed00..6289823ff 100644 --- a/src/Core/Repositories/DocumentDB/FolderRepository.cs +++ b/src/Core/Repositories/DocumentDB/FolderRepository.cs @@ -4,6 +4,8 @@ using System.Threading.Tasks; using Microsoft.Azure.Documents.Client; using Bit.Core.Domains; using Bit.Core.Enums; +using Bit.Core.Repositories.DocumentDB.Utilities; +using Microsoft.Azure.Documents; namespace Bit.Core.Repositories.DocumentDB { @@ -15,7 +17,14 @@ namespace Bit.Core.Repositories.DocumentDB public async Task GetByIdAsync(string id, string userId) { - var doc = await Client.ReadDocumentAsync(ResolveDocumentIdLink(userId, id)); + ResourceResponse doc = null; + var docLink = ResolveDocumentIdLink(userId, id); + + await DocumentDBHelpers.ExecuteWithRetryAsync(async () => + { + doc = await Client.ReadDocumentAsync(docLink); + }); + if(doc?.Resource == null) { return null; @@ -30,20 +39,32 @@ namespace Bit.Core.Repositories.DocumentDB return folder; } - public Task> GetManyByUserIdAsync(string userId) + public async Task> GetManyByUserIdAsync(string userId) { - var docs = Client.CreateDocumentQuery(DatabaseUri, null, userId) - .Where(d => d.Type == Cipher.TypeValue && d.CipherType == CipherType.Folder && d.UserId == userId).AsEnumerable(); + IEnumerable docs = null; + await DocumentDBHelpers.ExecuteWithRetryAsync(() => + { + docs = Client.CreateDocumentQuery(DatabaseUri, null, userId) + .Where(d => d.Type == Cipher.TypeValue && d.CipherType == CipherType.Folder && d.UserId == userId).AsEnumerable(); - return Task.FromResult>(docs.ToList()); + return Task.FromResult(0); + }); + + return docs.ToList(); } - public Task> GetManyByUserIdAsync(string userId, bool dirty) + public async Task> GetManyByUserIdAsync(string userId, bool dirty) { - var docs = Client.CreateDocumentQuery(DatabaseUri, null, userId) - .Where(d => d.Type == Cipher.TypeValue && d.CipherType == CipherType.Folder && d.UserId == userId && d.Dirty == dirty).AsEnumerable(); + IEnumerable docs = null; + await DocumentDBHelpers.ExecuteWithRetryAsync(() => + { + docs = Client.CreateDocumentQuery(DatabaseUri, null, userId) + .Where(d => d.Type == Cipher.TypeValue && d.CipherType == CipherType.Folder && d.UserId == userId && d.Dirty == dirty).AsEnumerable(); - return Task.FromResult>(docs.ToList()); + return Task.FromResult(0); + }); + + return docs.ToList(); } } } diff --git a/src/Core/Repositories/DocumentDB/Repository.cs b/src/Core/Repositories/DocumentDB/Repository.cs index 9b54cf164..834d8629d 100644 --- a/src/Core/Repositories/DocumentDB/Repository.cs +++ b/src/Core/Repositories/DocumentDB/Repository.cs @@ -1,6 +1,9 @@ using System; +using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using Bit.Core.Repositories.DocumentDB.Utilities; +using Microsoft.Azure.Documents; using Microsoft.Azure.Documents.Client; namespace Bit.Core.Repositories.DocumentDB @@ -11,36 +14,61 @@ namespace Bit.Core.Repositories.DocumentDB : base(client, databaseId, documentType) { } - public virtual Task GetByIdAsync(string id) + public virtual async Task GetByIdAsync(string id) { // NOTE: Not an ideal condition, scanning all collections. // Override this method if you can implement a direct partition lookup based on the id. // Use the inherited GetByPartitionIdAsync method to implement your override. - var docs = Client.CreateDocumentQuery(DatabaseUri, new FeedOptions { MaxItemCount = 1 }) - .Where(d => d.Id == id).AsEnumerable(); - return Task.FromResult(docs.FirstOrDefault()); + IEnumerable docs = null; + await DocumentDBHelpers.ExecuteWithRetryAsync(() => + { + docs = Client.CreateDocumentQuery(DatabaseUri, new FeedOptions { MaxItemCount = 1 }) + .Where(d => d.Id == id).AsEnumerable(); + + return Task.FromResult(0); + }); + + return docs.FirstOrDefault(); } public virtual async Task CreateAsync(T obj) { - var result = await Client.CreateDocumentAsync(DatabaseUri, obj); - obj.Id = result.Resource.Id; + await DocumentDBHelpers.ExecuteWithRetryAsync(async () => + { + var result = await Client.CreateDocumentAsync(DatabaseUri, obj); + obj.Id = result.Resource.Id; + }); } public virtual async Task ReplaceAsync(T obj) { - await Client.ReplaceDocumentAsync(ResolveDocumentIdLink(obj), obj); + var docLink = ResolveDocumentIdLink(obj); + + await DocumentDBHelpers.ExecuteWithRetryAsync(async () => + { + await Client.ReplaceDocumentAsync(docLink, obj); + }); } public virtual async Task UpsertAsync(T obj) { - await Client.UpsertDocumentAsync(ResolveDocumentIdLink(obj), obj); + var docLink = ResolveDocumentIdLink(obj); + + await DocumentDBHelpers.ExecuteWithRetryAsync(async () => + { + await Client.UpsertDocumentAsync(docLink, obj); + }); } public virtual async Task DeleteAsync(T obj) { - await Client.DeleteDocumentAsync(ResolveDocumentIdLink(obj)); + var docLink = ResolveDocumentIdLink(obj); + + await DocumentDBHelpers.ExecuteWithRetryAsync(async () => + { + await Client.DeleteDocumentAsync(docLink); + }); } public virtual async Task DeleteByIdAsync(string id) @@ -48,18 +76,35 @@ namespace Bit.Core.Repositories.DocumentDB // NOTE: Not an ideal condition, scanning all collections. // Override this method if you can implement a direct partition lookup based on the id. // Use the inherited DeleteByPartitionIdAsync method to implement your override. - var docs = Client.CreateDocumentQuery(DatabaseUri, new FeedOptions { MaxItemCount = 1 }) - .Where(d => d.Id == id).AsEnumerable(); - if(docs.Count() > 0) + IEnumerable docs = null; + await DocumentDBHelpers.ExecuteWithRetryAsync(() => { - await Client.DeleteDocumentAsync(docs.First().SelfLink); + docs = Client.CreateDocumentQuery(DatabaseUri, new FeedOptions { MaxItemCount = 1 }) + .Where(d => d.Id == id).AsEnumerable(); + + return Task.FromResult(0); + }); + + if(docs != null && docs.Count() > 0) + { + await DocumentDBHelpers.ExecuteWithRetryAsync(async () => + { + await Client.DeleteDocumentAsync(docs.First().SelfLink); + }); } } protected async Task GetByPartitionIdAsync(string id) { - var doc = await Client.ReadDocumentAsync(ResolveDocumentIdLink(id)); + ResourceResponse doc = null; + var docLink = ResolveDocumentIdLink(id); + + await DocumentDBHelpers.ExecuteWithRetryAsync(async () => + { + doc = await Client.ReadDocumentAsync(docLink); + }); + if(doc?.Resource == null) { return default(T); @@ -70,7 +115,12 @@ namespace Bit.Core.Repositories.DocumentDB protected async Task DeleteByPartitionIdAsync(string id) { - await Client.DeleteDocumentAsync(ResolveDocumentIdLink(id)); + var docLink = ResolveDocumentIdLink(id); + + await DocumentDBHelpers.ExecuteWithRetryAsync(async () => + { + await Client.DeleteDocumentAsync(docLink); + }); } } } diff --git a/src/Core/Repositories/DocumentDB/SiteRepository.cs b/src/Core/Repositories/DocumentDB/SiteRepository.cs index 5b5dc21df..7a4756ef0 100644 --- a/src/Core/Repositories/DocumentDB/SiteRepository.cs +++ b/src/Core/Repositories/DocumentDB/SiteRepository.cs @@ -5,6 +5,8 @@ using System.Threading.Tasks; using Microsoft.Azure.Documents.Client; using Bit.Core.Domains; using Bit.Core.Enums; +using Bit.Core.Repositories.DocumentDB.Utilities; +using Microsoft.Azure.Documents; namespace Bit.Core.Repositories.DocumentDB { @@ -16,7 +18,14 @@ namespace Bit.Core.Repositories.DocumentDB public async Task GetByIdAsync(string id, string userId) { - var doc = await Client.ReadDocumentAsync(ResolveDocumentIdLink(userId, id)); + ResourceResponse doc = null; + var docLink = ResolveDocumentIdLink(userId, id); + + await DocumentDBHelpers.ExecuteWithRetryAsync(async () => + { + doc = await Client.ReadDocumentAsync(docLink); + }); + if(doc?.Resource == null) { return null; @@ -31,20 +40,32 @@ namespace Bit.Core.Repositories.DocumentDB return site; } - public Task> GetManyByUserIdAsync(string userId) + public async Task> GetManyByUserIdAsync(string userId) { - var docs = Client.CreateDocumentQuery(DatabaseUri, null, userId) - .Where(d => d.Type == Cipher.TypeValue && d.CipherType == CipherType.Site && d.UserId == userId).AsEnumerable(); + IEnumerable docs = null; + await DocumentDBHelpers.ExecuteWithRetryAsync(() => + { + docs = Client.CreateDocumentQuery(DatabaseUri, null, userId) + .Where(d => d.Type == Cipher.TypeValue && d.CipherType == CipherType.Site && d.UserId == userId).AsEnumerable(); - return Task.FromResult>(docs.ToList()); + return Task.FromResult(0); + }); + + return docs.ToList(); } - public Task> GetManyByUserIdAsync(string userId, bool dirty) + public async Task> GetManyByUserIdAsync(string userId, bool dirty) { - var docs = Client.CreateDocumentQuery(DatabaseUri, null, userId) - .Where(d => d.Type == Cipher.TypeValue && d.CipherType == CipherType.Site && d.UserId == userId && d.Dirty == dirty).AsEnumerable(); + IEnumerable docs = null; + await DocumentDBHelpers.ExecuteWithRetryAsync(() => + { + docs = Client.CreateDocumentQuery(DatabaseUri, null, userId) + .Where(d => d.Type == Cipher.TypeValue && d.CipherType == CipherType.Site && d.UserId == userId && d.Dirty == dirty).AsEnumerable(); - return Task.FromResult>(docs.ToList()); + return Task.FromResult(0); + }); + + return docs.ToList(); } } } diff --git a/src/Core/Repositories/DocumentDB/Stored Procedures/dirtyCiphers.js b/src/Core/Repositories/DocumentDB/Stored Procedures/dirtyCiphers.js new file mode 100644 index 000000000..0af4884bd --- /dev/null +++ b/src/Core/Repositories/DocumentDB/Stored Procedures/dirtyCiphers.js @@ -0,0 +1,63 @@ +function dirtyCiphers(userId) { + var collection = getContext().getCollection(); + var collectionLink = collection.getSelfLink(); + var response = getContext().getResponse(); + var responseBody = { + updated: 0, + continuation: true + }; + + if (!userId) throw new Error('The userId is undefined or null.'); + + tryQueryAndUpdate(); + + function tryQueryAndUpdate(continuation) { + var query = { + query: "SELECT * FROM root r WHERE r.UserId = @userId AND r.type = 'cipher' AND r.Dirty = false", + parameters: [{ name: '@userId', value: userId }] + }; + + var requestOptions = { continuation: continuation }; + var accepted = collection.queryDocuments(collectionLink, query, requestOptions, function (err, retrievedDocs, responseOptions) { + if (err) throw err; + + if (retrievedDocs.length > 0) { + tryUpdate(retrievedDocs); + } + else if (responseOptions.continuation) { + tryQueryAndUpdate(responseOptions.continuation); + } + else { + responseBody.continuation = false; + response.setBody(responseBody); + } + }); + + if (!accepted) { + response.setBody(responseBody); + } + } + + function tryUpdate(documents) { + if (documents.length > 0) { + // dirty it + documents[0].Dirty = true; + + var accepted = collection.replaceDocument(documents[0]._self, documents[0], {}, function (err, replacedDoc) { + if (err) throw err; + + responseBody.updated++; + documents.shift(); + + tryUpdate(documents); + }); + + if (!accepted) { + response.setBody(responseBody); + } + } + else { + tryQueryAndUpdate(); + } + } +} diff --git a/src/Core/Repositories/DocumentDB/Stored Procedures/replaceUserAndDirtyCiphers.js b/src/Core/Repositories/DocumentDB/Stored Procedures/replaceUserAndDirtyCiphers.js deleted file mode 100644 index d95fd017b..000000000 --- a/src/Core/Repositories/DocumentDB/Stored Procedures/replaceUserAndDirtyCiphers.js +++ /dev/null @@ -1,107 +0,0 @@ -// Replace user document and mark all related ciphers as dirty. - -function replaceUserAndDirtyCiphers(user) { - var context = getContext(); - var collection = context.getCollection(); - var collectionLink = collection.getSelfLink(); - var response = context.getResponse(); - - // Validate input. - if (!user) { - throw new Error('The user is undefined or null.'); - } - - getUser(function (userDoc) { - replaceUser(userDoc, function (replacedDoc) { - queryAndDirtyCiphers(function () { - response.setBody(replacedDoc); - }); - }); - }); - - function getUser(callback, continuation) { - var query = { - query: 'SELECT * FROM root r WHERE r.id = @id', - parameters: [{ name: '@id', value: user.id }] - }; - - var requestOptions = { continuation: continuation }; - var accepted = collection.queryDocuments(collectionLink, query, requestOptions, function (err, documents, responseOptions) { - if (err) throw err; - - if (documents.length > 0) { - callback(documents[0]); - } - else if (responseOptions.continuation) { - getUser(responseOptions.continuation); - } - else { - throw new Error('User not found.'); - } - }); - - if (!accepted) { - throw new Error('The stored procedure timed out.'); - } - } - - function replaceUser(userDoc, callback) { - var accepted = collection.replaceDocument(userDoc._self, user, {}, function (err, replacedDoc) { - if (err) throw err; - - callback(replacedDoc); - }); - - if (!accepted) { - throw new Error('The stored procedure timed out.'); - } - } - - function queryAndDirtyCiphers(callback, continuation) { - var query = { - query: 'SELECT * FROM root r WHERE r.type = @type AND r.UserId = @userId', - parameters: [{ name: '@type', value: 'cipher' }, { name: '@userId', value: user.id }] - }; - - var requestOptions = { continuation: continuation }; - var accepted = collection.queryDocuments(collectionLink, query, requestOptions, function (err, documents, responseOptions) { - if (err) throw err; - - if (documents.length > 0) { - dirtyCiphers(documents, callback); - } - else if (responseOptions.continuation) { - queryAndDirtyCiphers(callback, responseOptions.continuation); - } - else { - callback(); - } - }); - - if (!accepted) { - throw new Error('The stored procedure timed out.'); - } - } - - function dirtyCiphers(documents, callback) { - if (documents.length > 0) { - // dirty the cipher - documents[0].Dirty = true; - - var requestOptions = { etag: documents[0]._etag }; - var accepted = collection.replaceDocument(documents[0]._self, documents[0], requestOptions, function (err) { - if (err) throw err; - - documents.shift(); - dirtyCiphers(documents, callback); - }); - - if (!accepted) { - throw new Error('The stored procedure timed out.'); - } - } - else { - callback(); - } - } -} diff --git a/src/Core/Repositories/DocumentDB/Stored Procedures/bulkUpdateDirtyCiphers.js b/src/Core/Repositories/DocumentDB/Stored Procedures/updateDirtyCiphers.js similarity index 97% rename from src/Core/Repositories/DocumentDB/Stored Procedures/bulkUpdateDirtyCiphers.js rename to src/Core/Repositories/DocumentDB/Stored Procedures/updateDirtyCiphers.js index f688ed20b..0f6e4378d 100644 --- a/src/Core/Repositories/DocumentDB/Stored Procedures/bulkUpdateDirtyCiphers.js +++ b/src/Core/Repositories/DocumentDB/Stored Procedures/updateDirtyCiphers.js @@ -1,6 +1,6 @@ // Update an array of dirty ciphers for a user. -function bulkUpdateDirtyCiphers(ciphers, userId) { +function updateDirtyCiphers(ciphers, userId) { var context = getContext(); var collection = context.getCollection(); var collectionLink = collection.getSelfLink(); diff --git a/src/Core/Repositories/DocumentDB/UserRepository.cs b/src/Core/Repositories/DocumentDB/UserRepository.cs index 214d8605e..11835b585 100644 --- a/src/Core/Repositories/DocumentDB/UserRepository.cs +++ b/src/Core/Repositories/DocumentDB/UserRepository.cs @@ -1,8 +1,8 @@ using System; +using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Bit.Core.Repositories.DocumentDB.Utilities; -using Microsoft.Azure.Documents; using Microsoft.Azure.Documents.Client; namespace Bit.Core.Repositories.DocumentDB @@ -18,22 +18,18 @@ namespace Bit.Core.Repositories.DocumentDB return await GetByPartitionIdAsync(id); } - public Task GetByEmailAsync(string email) + public async Task GetByEmailAsync(string email) { - var docs = Client.CreateDocumentQuery(DatabaseUri, new FeedOptions { MaxItemCount = 1 }) - .Where(d => d.Type == Domains.User.TypeValue && d.Email == email).AsEnumerable(); - - return Task.FromResult(docs.FirstOrDefault()); - } - - public async Task ReplaceAndDirtyCiphersAsync(Domains.User user) - { - await DocumentDBHelpers.ExecuteWithRetryAsync(async () => + IEnumerable docs = null; + await DocumentDBHelpers.ExecuteWithRetryAsync(() => { - await Client.ExecuteStoredProcedureAsync( - ResolveSprocIdLink(user, "replaceUserAndDirtyCiphers"), - user); + docs = Client.CreateDocumentQuery(DatabaseUri, new FeedOptions { MaxItemCount = 1 }) + .Where(d => d.Type == Domains.User.TypeValue && d.Email == email).AsEnumerable(); + + return Task.FromResult(0); }); + + return docs.FirstOrDefault(); } public override async Task DeleteAsync(Domains.User user) diff --git a/src/Core/Repositories/DocumentDB/Utilities/DocumentDBHelpers.cs b/src/Core/Repositories/DocumentDB/Utilities/DocumentDBHelpers.cs index 3ffe3ee4c..5bc8ca992 100644 --- a/src/Core/Repositories/DocumentDB/Utilities/DocumentDBHelpers.cs +++ b/src/Core/Repositories/DocumentDB/Utilities/DocumentDBHelpers.cs @@ -31,32 +31,40 @@ namespace Bit.Core.Repositories.DocumentDB.Utilities return client; } - public static async Task ExecuteWithRetryAsync(Func func) + public static async Task ExecuteWithRetryAsync(Func func, int? retryMax = null) { + var executionAttempt = 1; while(true) { try { await func(); - break; + return; } catch(DocumentClientException e) { - await HandleDocumentClientExceptionAsync(e); + await HandleDocumentClientExceptionAsync(e, executionAttempt, retryMax); } catch(AggregateException e) { var docEx = e.InnerException as DocumentClientException; if(docEx != null) { - await HandleDocumentClientExceptionAsync(docEx); + await HandleDocumentClientExceptionAsync(docEx, executionAttempt, retryMax); } } + + executionAttempt++; } } - private static async Task HandleDocumentClientExceptionAsync(DocumentClientException e) + private static async Task HandleDocumentClientExceptionAsync(DocumentClientException e, int retryCount, int? retryMax) { + if(retryMax.HasValue && retryCount >= retryMax.Value) + { + throw e; + } + var statusCode = (int)e.StatusCode; if(statusCode == 429 || statusCode == 503) { diff --git a/src/Core/Repositories/ICipherRepository.cs b/src/Core/Repositories/ICipherRepository.cs index b924199f7..cfc09b2a6 100644 --- a/src/Core/Repositories/ICipherRepository.cs +++ b/src/Core/Repositories/ICipherRepository.cs @@ -5,6 +5,7 @@ namespace Bit.Core.Repositories { public interface ICipherRepository { + Task DirtyCiphersAsync(string userId); Task UpdateDirtyCiphersAsync(IEnumerable ciphers); Task CreateAsync(IEnumerable ciphers); } diff --git a/src/Core/Repositories/IUserRepository.cs b/src/Core/Repositories/IUserRepository.cs index 606b2e160..2278c9ab8 100644 --- a/src/Core/Repositories/IUserRepository.cs +++ b/src/Core/Repositories/IUserRepository.cs @@ -8,6 +8,5 @@ namespace Bit.Core.Repositories public interface IUserRepository : IRepository { Task GetByEmailAsync(string email); - Task ReplaceAndDirtyCiphersAsync(User user); } } diff --git a/src/Core/Services/UserService.cs b/src/Core/Services/UserService.cs index 65ac4093a..a7d3c80e7 100644 --- a/src/Core/Services/UserService.cs +++ b/src/Core/Services/UserService.cs @@ -169,7 +169,8 @@ namespace Bit.Core.Services user.MasterPassword = _passwordHasher.HashPassword(user, newMasterPassword); user.SecurityStamp = Guid.NewGuid().ToString(); - await _userRepository.ReplaceAndDirtyCiphersAsync(user); + await _cipherRepository.DirtyCiphersAsync(user.Id); + await _userRepository.ReplaceAsync(user); await _cipherRepository.UpdateDirtyCiphersAsync(ciphers); // TODO: what if something fails? rollback? @@ -197,7 +198,8 @@ namespace Bit.Core.Services return result; } - await _userRepository.ReplaceAndDirtyCiphersAsync(user); + await _cipherRepository.DirtyCiphersAsync(user.Id); + await _userRepository.ReplaceAsync(user); await _cipherRepository.UpdateDirtyCiphersAsync(ciphers); // TODO: what if something fails? rollback? @@ -224,7 +226,7 @@ namespace Bit.Core.Services return result; } - await _userRepository.ReplaceAndDirtyCiphersAsync(user); + await _userRepository.ReplaceAsync(user); return IdentityResult.Success; }