mirror of
https://github.com/bitwarden/server.git
synced 2024-11-21 12:05:42 +01:00
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.
This commit is contained in:
parent
55be0c739e
commit
972290d1ec
@ -44,7 +44,7 @@ namespace Bit.Core.Identity
|
||||
context.AuthenticationTicket = new AuthenticationTicket(context.HttpContext.User, new AuthenticationProperties(), context.Options.AuthenticationScheme);
|
||||
}
|
||||
|
||||
return Task.FromResult<object>(null);
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -47,13 +47,13 @@ namespace Bit.Core.Identity
|
||||
|
||||
public Task SetNormalizedRoleNameAsync(Role role, string normalizedName, CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.FromResult<object>(null);
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public Task SetRoleNameAsync(Role role, string roleName, CancellationToken cancellationToken)
|
||||
{
|
||||
role.Name = roleName;
|
||||
return Task.FromResult<object>(null);
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public Task<IdentityResult> UpdateAsync(Role role, CancellationToken cancellationToken)
|
||||
|
@ -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<object>(null);
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public Task SetEmailConfirmedAsync(User user, bool confirmed, CancellationToken cancellationToken = default(CancellationToken))
|
||||
{
|
||||
// do nothing
|
||||
return Task.FromResult<object>(null);
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public Task SetNormalizedEmailAsync(User user, string normalizedEmail, CancellationToken cancellationToken = default(CancellationToken))
|
||||
{
|
||||
user.Email = normalizedEmail;
|
||||
return Task.FromResult<object>(null);
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public Task SetNormalizedUserNameAsync(User user, string normalizedName, CancellationToken cancellationToken = default(CancellationToken))
|
||||
{
|
||||
user.Email = normalizedName;
|
||||
return Task.FromResult<object>(null);
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public Task SetPasswordHashAsync(User user, string passwordHash, CancellationToken cancellationToken = default(CancellationToken))
|
||||
{
|
||||
user.MasterPassword = passwordHash;
|
||||
return Task.FromResult<object>(null);
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public Task SetUserNameAsync(User user, string userName, CancellationToken cancellationToken = default(CancellationToken))
|
||||
{
|
||||
user.Email = userName;
|
||||
return Task.FromResult<object>(null);
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public async Task<IdentityResult> 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<object>(null);
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public Task<bool> 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<object>(null);
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public Task<string> GetSecurityStampAsync(User user, CancellationToken cancellationToken)
|
||||
|
@ -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<dynamic> sprocResponse = await Client.ExecuteStoredProcedureAsync<dynamic>(
|
||||
ResolveSprocIdLink(userId, "dirtyCiphers"),
|
||||
userId);
|
||||
|
||||
if(!(bool)sprocResponse.Response.continuation)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public async Task UpdateDirtyCiphersAsync(IEnumerable<dynamic> ciphers)
|
||||
{
|
||||
await DocumentDBHelpers.ExecuteWithRetryAsync(async () =>
|
||||
@ -27,9 +45,8 @@ namespace Bit.Core.Repositories.DocumentDB
|
||||
|
||||
var userId = ((Cipher)cleanedCiphers.First()).UserId;
|
||||
StoredProcedureResponse<int> sprocResponse = await Client.ExecuteStoredProcedureAsync<int>(
|
||||
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<int> sprocResponse = await Client.ExecuteStoredProcedureAsync<int>(
|
||||
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())
|
||||
|
@ -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<Folder> GetByIdAsync(string id, string userId)
|
||||
{
|
||||
var doc = await Client.ReadDocumentAsync(ResolveDocumentIdLink(userId, id));
|
||||
ResourceResponse<Document> 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<ICollection<Folder>> GetManyByUserIdAsync(string userId)
|
||||
public async Task<ICollection<Folder>> GetManyByUserIdAsync(string userId)
|
||||
{
|
||||
var docs = Client.CreateDocumentQuery<Folder>(DatabaseUri, null, userId)
|
||||
.Where(d => d.Type == Cipher.TypeValue && d.CipherType == CipherType.Folder && d.UserId == userId).AsEnumerable();
|
||||
IEnumerable<Folder> docs = null;
|
||||
await DocumentDBHelpers.ExecuteWithRetryAsync(() =>
|
||||
{
|
||||
docs = Client.CreateDocumentQuery<Folder>(DatabaseUri, null, userId)
|
||||
.Where(d => d.Type == Cipher.TypeValue && d.CipherType == CipherType.Folder && d.UserId == userId).AsEnumerable();
|
||||
|
||||
return Task.FromResult<ICollection<Folder>>(docs.ToList());
|
||||
return Task.FromResult(0);
|
||||
});
|
||||
|
||||
return docs.ToList();
|
||||
}
|
||||
|
||||
public Task<ICollection<Folder>> GetManyByUserIdAsync(string userId, bool dirty)
|
||||
public async Task<ICollection<Folder>> GetManyByUserIdAsync(string userId, bool dirty)
|
||||
{
|
||||
var docs = Client.CreateDocumentQuery<Folder>(DatabaseUri, null, userId)
|
||||
.Where(d => d.Type == Cipher.TypeValue && d.CipherType == CipherType.Folder && d.UserId == userId && d.Dirty == dirty).AsEnumerable();
|
||||
IEnumerable<Folder> docs = null;
|
||||
await DocumentDBHelpers.ExecuteWithRetryAsync(() =>
|
||||
{
|
||||
docs = Client.CreateDocumentQuery<Folder>(DatabaseUri, null, userId)
|
||||
.Where(d => d.Type == Cipher.TypeValue && d.CipherType == CipherType.Folder && d.UserId == userId && d.Dirty == dirty).AsEnumerable();
|
||||
|
||||
return Task.FromResult<ICollection<Folder>>(docs.ToList());
|
||||
return Task.FromResult(0);
|
||||
});
|
||||
|
||||
return docs.ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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<T> GetByIdAsync(string id)
|
||||
public virtual async Task<T> 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<T>(DatabaseUri, new FeedOptions { MaxItemCount = 1 })
|
||||
.Where(d => d.Id == id).AsEnumerable();
|
||||
|
||||
return Task.FromResult(docs.FirstOrDefault());
|
||||
IEnumerable<T> docs = null;
|
||||
await DocumentDBHelpers.ExecuteWithRetryAsync(() =>
|
||||
{
|
||||
docs = Client.CreateDocumentQuery<T>(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<Document> 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<T> GetByPartitionIdAsync(string id)
|
||||
{
|
||||
var doc = await Client.ReadDocumentAsync(ResolveDocumentIdLink(id));
|
||||
ResourceResponse<Document> 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);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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<Site> GetByIdAsync(string id, string userId)
|
||||
{
|
||||
var doc = await Client.ReadDocumentAsync(ResolveDocumentIdLink(userId, id));
|
||||
ResourceResponse<Document> 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<ICollection<Site>> GetManyByUserIdAsync(string userId)
|
||||
public async Task<ICollection<Site>> GetManyByUserIdAsync(string userId)
|
||||
{
|
||||
var docs = Client.CreateDocumentQuery<Site>(DatabaseUri, null, userId)
|
||||
.Where(d => d.Type == Cipher.TypeValue && d.CipherType == CipherType.Site && d.UserId == userId).AsEnumerable();
|
||||
IEnumerable<Site> docs = null;
|
||||
await DocumentDBHelpers.ExecuteWithRetryAsync(() =>
|
||||
{
|
||||
docs = Client.CreateDocumentQuery<Site>(DatabaseUri, null, userId)
|
||||
.Where(d => d.Type == Cipher.TypeValue && d.CipherType == CipherType.Site && d.UserId == userId).AsEnumerable();
|
||||
|
||||
return Task.FromResult<ICollection<Site>>(docs.ToList());
|
||||
return Task.FromResult(0);
|
||||
});
|
||||
|
||||
return docs.ToList();
|
||||
}
|
||||
|
||||
public Task<ICollection<Site>> GetManyByUserIdAsync(string userId, bool dirty)
|
||||
public async Task<ICollection<Site>> GetManyByUserIdAsync(string userId, bool dirty)
|
||||
{
|
||||
var docs = Client.CreateDocumentQuery<Site>(DatabaseUri, null, userId)
|
||||
.Where(d => d.Type == Cipher.TypeValue && d.CipherType == CipherType.Site && d.UserId == userId && d.Dirty == dirty).AsEnumerable();
|
||||
IEnumerable<Site> docs = null;
|
||||
await DocumentDBHelpers.ExecuteWithRetryAsync(() =>
|
||||
{
|
||||
docs = Client.CreateDocumentQuery<Site>(DatabaseUri, null, userId)
|
||||
.Where(d => d.Type == Cipher.TypeValue && d.CipherType == CipherType.Site && d.UserId == userId && d.Dirty == dirty).AsEnumerable();
|
||||
|
||||
return Task.FromResult<ICollection<Site>>(docs.ToList());
|
||||
return Task.FromResult(0);
|
||||
});
|
||||
|
||||
return docs.ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
@ -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<Domains.User> GetByEmailAsync(string email)
|
||||
public async Task<Domains.User> GetByEmailAsync(string email)
|
||||
{
|
||||
var docs = Client.CreateDocumentQuery<Domains.User>(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<Domains.User> docs = null;
|
||||
await DocumentDBHelpers.ExecuteWithRetryAsync(() =>
|
||||
{
|
||||
await Client.ExecuteStoredProcedureAsync<Domains.User>(
|
||||
ResolveSprocIdLink(user, "replaceUserAndDirtyCiphers"),
|
||||
user);
|
||||
docs = Client.CreateDocumentQuery<Domains.User>(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)
|
||||
|
@ -31,32 +31,40 @@ namespace Bit.Core.Repositories.DocumentDB.Utilities
|
||||
return client;
|
||||
}
|
||||
|
||||
public static async Task ExecuteWithRetryAsync(Func<Task> func)
|
||||
public static async Task ExecuteWithRetryAsync(Func<Task> 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)
|
||||
{
|
||||
|
@ -5,6 +5,7 @@ namespace Bit.Core.Repositories
|
||||
{
|
||||
public interface ICipherRepository
|
||||
{
|
||||
Task DirtyCiphersAsync(string userId);
|
||||
Task UpdateDirtyCiphersAsync(IEnumerable<dynamic> ciphers);
|
||||
Task CreateAsync(IEnumerable<dynamic> ciphers);
|
||||
}
|
||||
|
@ -8,6 +8,5 @@ namespace Bit.Core.Repositories
|
||||
public interface IUserRepository : IRepository<User>
|
||||
{
|
||||
Task<User> GetByEmailAsync(string email);
|
||||
Task ReplaceAndDirtyCiphersAsync(User user);
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user