1
0
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:
Kyle Spearrin 2015-12-29 21:45:21 -05:00
parent 55be0c739e
commit 972290d1ec
15 changed files with 250 additions and 180 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -8,6 +8,5 @@ namespace Bit.Core.Repositories
public interface IUserRepository : IRepository<User>
{
Task<User> GetByEmailAsync(string email);
Task ReplaceAndDirtyCiphersAsync(User user);
}
}

View File

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