1
0
mirror of https://github.com/bitwarden/server.git synced 2024-11-28 13:15:12 +01:00

Removed DocumentDB repositories and domain type dependencies for them. Moved account registration process to not require email address verification in preparation for client app registration process.

This commit is contained in:
Kyle Spearrin 2016-02-20 23:25:44 -05:00
parent a523364844
commit 9914399e8b
28 changed files with 17 additions and 1054 deletions

View File

@ -1,7 +1,6 @@
using System;
using System.Threading.Tasks;
using Microsoft.AspNet.Authorization;
using Microsoft.AspNet.DataProtection;
using Microsoft.AspNet.Mvc;
using Bit.Api.Models;
using Bit.Core.Exceptions;
@ -36,18 +35,11 @@ namespace Bit.Api.Controllers
_currentContext = currentContext;
}
[HttpPost("register-token")]
[AllowAnonymous]
public async Task PostRegisterToken([FromBody]RegisterTokenRequestModel model)
{
await _userService.InitiateRegistrationAsync(model.Email);
}
[HttpPost("register")]
[AllowAnonymous]
public async Task PostRegister([FromBody]RegisterRequestModel model)
{
var result = await _userService.RegisterUserAsync(model.Token, model.ToUser(), model.MasterPasswordHash);
var result = await _userService.RegisterUserAsync(model.ToUser(), model.MasterPasswordHash);
if(result.Succeeded)
{
return;

View File

@ -5,8 +5,6 @@ namespace Bit.Api.Models
{
public class RegisterRequestModel
{
[Required]
public string Token { get; set; }
[Required]
[StringLength(50)]
public string Name { get; set; }

View File

@ -1,12 +0,0 @@
using System.ComponentModel.DataAnnotations;
namespace Bit.Api.Models
{
public class RegisterTokenRequestModel
{
[Required]
[EmailAddress]
[StringLength(50)]
public string Email { get; set; }
}
}

View File

@ -10,7 +10,6 @@
"version": "0.0.1",
"target": "project"
},
"Microsoft.AspNet.DataProtection.Extensions": "1.0.0-rc1-final",
"Microsoft.AspNet.IISPlatformHandler": "1.0.0-rc1-final",
"Microsoft.AspNet.Mvc": "6.0.0-rc1-final",
"Microsoft.AspNet.Server.Kestrel": "1.0.0-rc1-final",

View File

@ -1,22 +1,12 @@
using System;
using Newtonsoft.Json;
using Bit.Core.Enums;
namespace Bit.Core.Domains
{
public abstract class Cipher : IDataObject
{
internal static string TypeValue = "cipher";
[JsonProperty("id")]
public string Id { get; set; }
[JsonProperty("type")]
public string Type { get; private set; } = TypeValue;
public abstract CipherType CipherType { get; protected set; }
public string UserId { get; set; }
public string Name { get; set; }
public bool Dirty { get; set; }
public DateTime CreationDate { get; internal set; } = DateTime.UtcNow;
public DateTime RevisionDate { get; internal set; } = DateTime.UtcNow;
}

View File

@ -1,9 +1,6 @@
using Bit.Core.Enums;
namespace Bit.Core.Domains
namespace Bit.Core.Domains
{
public class Folder : Cipher, IDataObject
{
public override CipherType CipherType { get; protected set; } = CipherType.Folder;
}
}

View File

@ -4,10 +4,7 @@ namespace Bit.Core.Domains
{
public class Site : Cipher, IDataObject
{
public override CipherType CipherType { get; protected set; } = CipherType.Site;
public string FolderId { get; set; }
public string Uri { get; set; }
public string Username { get; set; }
public string Password { get; set; }

View File

@ -1,30 +1,21 @@
using System;
using Newtonsoft.Json;
using Bit.Core.Enums;
namespace Bit.Core.Domains
{
public class User : IDataObject
{
internal static string TypeValue = "user";
[JsonProperty("id")]
public string Id { get; set; }
[JsonProperty("type")]
public string Type { get; private set; } = TypeValue;
public string Name { get; set; }
public string Email { get; set; }
public string MasterPassword { get; set; }
public string MasterPasswordHint { get; set; }
public string Culture { get; set; } = "en-US";
public string SecurityStamp { get; set; }
public string OldEmail { get; set; }
public string OldMasterPassword { get; set; }
public bool TwoFactorEnabled { get; set; }
public TwoFactorProvider? TwoFactorProvider { get; set; }
public string AuthenticatorKey { get; set; }
public DateTime CreationDate { get; internal set; } = DateTime.UtcNow;
public DateTime RevisionDate { get; internal set; } = DateTime.UtcNow;
}
}

View File

@ -1,12 +1,7 @@
using Newtonsoft.Json;
namespace Bit.Core
namespace Bit.Core
{
public interface IDataObject
{
[JsonProperty("id")]
string Id { get; set; }
[JsonProperty("type")]
string Type { get; }
}
}

View File

@ -1,67 +0,0 @@
using System;
using Microsoft.Azure.Documents.Client;
namespace Bit.Core.Repositories.DocumentDB
{
public abstract class BaseRepository<T> where T : IDataObject
{
public BaseRepository(DocumentClient client, string databaseId, string documentType = null)
{
Client = client;
DatabaseId = databaseId;
DatabaseUri = UriFactory.CreateDatabaseUri(databaseId);
PartitionResolver = client.PartitionResolvers[DatabaseUri.OriginalString];
if(string.IsNullOrWhiteSpace(documentType))
{
DocumentType = typeof(T).Name.ToLower();
}
else
{
DocumentType = documentType;
}
}
protected DocumentClient Client { get; private set; }
protected string DatabaseId { get; private set; }
protected Uri DatabaseUri { get; private set; }
protected IPartitionResolver PartitionResolver { get; private set; }
protected string DocumentType { get; private set; }
protected string ResolveSprocIdLink(T obj, string sprocId)
{
return string.Format("{0}/sprocs/{1}", ResolveCollectionIdLink(obj), sprocId);
}
protected string ResolveSprocIdLink(string partitionKey, string sprocId)
{
return string.Format("{0}/sprocs/{1}", ResolveCollectionIdLink(partitionKey), sprocId);
}
protected string ResolveDocumentIdLink(T obj)
{
return string.Format("{0}/docs/{1}", ResolveCollectionIdLink(obj), obj.Id);
}
protected string ResolveDocumentIdLink(string id)
{
return ResolveDocumentIdLink(id, id);
}
protected string ResolveDocumentIdLink(string partitionKey, string id)
{
return string.Format("{0}/docs/{1}", ResolveCollectionIdLink(partitionKey), id);
}
protected string ResolveCollectionIdLink(T obj)
{
var partitionKey = PartitionResolver.GetPartitionKey(obj);
return ResolveCollectionIdLink(partitionKey);
}
protected string ResolveCollectionIdLink(object partitionKey)
{
return PartitionResolver.ResolveForCreate(partitionKey);
}
}
}

View File

@ -1,88 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Azure.Documents.Client;
using Bit.Core.Domains;
using Bit.Core.Repositories.DocumentDB.Utilities;
namespace Bit.Core.Repositories.DocumentDB
{
public class CipherRepository : BaseRepository<Cipher>, ICipherRepository
{
public CipherRepository(DocumentClient client, string databaseId, string documentType = null)
: 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)
{
// Make sure we are dealing with cipher types since we accept any via dynamic.
var cleanedCiphers = ciphers.Where(c => c is Cipher);
if(cleanedCiphers.Count() == 0)
{
return;
}
var takeCount = DocumentDBHelpers.GetTakeCount(ciphers, 500);
await DocumentDBHelpers.ExecuteWithRetryAsync(async () =>
{
var userId = ((Cipher)cleanedCiphers.First()).UserId;
StoredProcedureResponse<int> sprocResponse = await Client.ExecuteStoredProcedureAsync<int>(
ResolveSprocIdLink(userId, "updateDirtyCiphers"),
cleanedCiphers.Take(takeCount),
userId);
var replacedCount = sprocResponse.Response;
if(replacedCount != cleanedCiphers.Count())
{
await UpdateDirtyCiphersAsync(cleanedCiphers.Skip(replacedCount));
}
});
}
public async Task CreateAsync(IEnumerable<dynamic> ciphers)
{
// Make sure we are dealing with cipher types since we accept any via dynamic.
var cleanedCiphers = ciphers.Where(c => c is Cipher);
if(cleanedCiphers.Count() == 0)
{
return;
}
var takeCount = DocumentDBHelpers.GetTakeCount(ciphers, 500);
await DocumentDBHelpers.ExecuteWithRetryAsync(async () =>
{
var userId = ((Cipher)cleanedCiphers.First()).UserId;
StoredProcedureResponse<int> sprocResponse = await Client.ExecuteStoredProcedureAsync<int>(
ResolveSprocIdLink(userId, "bulkCreate"),
cleanedCiphers.Take(takeCount));
var createdCount = sprocResponse.Response;
if(createdCount != cleanedCiphers.Count())
{
await CreateAsync(cleanedCiphers.Skip(createdCount));
}
});
}
}
}

View File

@ -1,70 +0,0 @@
using System.Collections.Generic;
using System.Linq;
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
{
public class FolderRepository : Repository<Folder>, IFolderRepository
{
public FolderRepository(DocumentClient client, string databaseId)
: base(client, databaseId)
{ }
public async Task<Folder> GetByIdAsync(string id, string userId)
{
ResourceResponse<Document> doc = null;
var docLink = ResolveDocumentIdLink(userId, id);
await DocumentDBHelpers.ExecuteWithRetryAsync(async () =>
{
doc = await Client.ReadDocumentAsync(docLink);
});
if(doc?.Resource == null)
{
return null;
}
var folder = (Folder)((dynamic)doc.Resource);
if(folder.UserId != userId)
{
return null;
}
return folder;
}
public async Task<ICollection<Folder>> GetManyByUserIdAsync(string userId)
{
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(0);
});
return docs.ToList();
}
public async Task<ICollection<Folder>> GetManyByUserIdAsync(string userId, bool dirty)
{
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(0);
});
return docs.ToList();
}
}
}

View File

@ -1,126 +0,0 @@
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
{
public abstract class Repository<T> : BaseRepository<T>, IRepository<T> where T : IDataObject
{
public Repository(DocumentClient client, string databaseId, string documentType = null)
: base(client, databaseId, documentType)
{ }
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.
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)
{
await DocumentDBHelpers.ExecuteWithRetryAsync(async () =>
{
var result = await Client.CreateDocumentAsync(DatabaseUri, obj);
obj.Id = result.Resource.Id;
});
}
public virtual async Task ReplaceAsync(T obj)
{
var docLink = ResolveDocumentIdLink(obj);
await DocumentDBHelpers.ExecuteWithRetryAsync(async () =>
{
await Client.ReplaceDocumentAsync(docLink, obj);
});
}
public virtual async Task UpsertAsync(T obj)
{
var docLink = ResolveDocumentIdLink(obj);
await DocumentDBHelpers.ExecuteWithRetryAsync(async () =>
{
await Client.UpsertDocumentAsync(docLink, obj);
});
}
public virtual async Task DeleteAsync(T obj)
{
var docLink = ResolveDocumentIdLink(obj);
await DocumentDBHelpers.ExecuteWithRetryAsync(async () =>
{
await Client.DeleteDocumentAsync(docLink);
});
}
public virtual async Task DeleteByIdAsync(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 DeleteByPartitionIdAsync method to implement your override.
IEnumerable<Document> docs = null;
await DocumentDBHelpers.ExecuteWithRetryAsync(() =>
{
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)
{
ResourceResponse<Document> doc = null;
var docLink = ResolveDocumentIdLink(id);
await DocumentDBHelpers.ExecuteWithRetryAsync(async () =>
{
doc = await Client.ReadDocumentAsync(docLink);
});
if(doc?.Resource == null)
{
return default(T);
}
return (T)((dynamic)doc.Resource);
}
protected async Task DeleteByPartitionIdAsync(string id)
{
var docLink = ResolveDocumentIdLink(id);
await DocumentDBHelpers.ExecuteWithRetryAsync(async () =>
{
await Client.DeleteDocumentAsync(docLink);
});
}
}
}

View File

@ -1,71 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
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
{
public class SiteRepository : Repository<Site>, ISiteRepository
{
public SiteRepository(DocumentClient client, string databaseId)
: base(client, databaseId)
{ }
public async Task<Site> GetByIdAsync(string id, string userId)
{
ResourceResponse<Document> doc = null;
var docLink = ResolveDocumentIdLink(userId, id);
await DocumentDBHelpers.ExecuteWithRetryAsync(async () =>
{
doc = await Client.ReadDocumentAsync(docLink);
});
if(doc?.Resource == null)
{
return null;
}
var site = (Site)((dynamic)doc.Resource);
if(site.UserId != userId)
{
return null;
}
return site;
}
public async Task<ICollection<Site>> GetManyByUserIdAsync(string userId)
{
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(0);
});
return docs.ToList();
}
public async Task<ICollection<Site>> GetManyByUserIdAsync(string userId, bool dirty)
{
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(0);
});
return docs.ToList();
}
}
}

View File

@ -1,60 +0,0 @@
/**
* This script called as stored procedure to import lots of documents in one batch.
* The script sets response body to the number of docs imported and is called multiple times
* by the client until total number of docs desired by the client is imported.
* @param {Object[]} docs - Array of documents to import.
*/
function bulkCreate(docs) {
var collection = getContext().getCollection();
var collectionLink = collection.getSelfLink();
// The count of imported docs, also used as current doc index.
var count = 0;
// Validate input.
if (!docs) throw new Error('The array is undefined or null.');
var docsLength = docs.length;
if (docsLength == 0) {
getContext().getResponse().setBody(0);
return;
}
// Call the CRUD API to create a document.
tryCreate(docs[count], callback);
// Note that there are 2 exit conditions:
// 1) The createDocument request was not accepted.
// In this case the callback will not be called, we just call setBody and we are done.
// 2) The callback was called docs.length times.
// In this case all documents were created and we don't need to call tryCreate anymore. Just call setBody and we are done.
function tryCreate(doc, callback) {
var isAccepted = collection.createDocument(collectionLink, doc, callback);
// If the request was accepted, callback will be called.
// Otherwise report current count back to the client,
// which will call the script again with remaining set of docs.
// This condition will happen when this stored procedure has been running too long
// and is about to get cancelled by the server. This will allow the calling client
// to resume this batch from the point we got to before isAccepted was set to false
if (!isAccepted) getContext().getResponse().setBody(count);
}
// This is called when collection.createDocument is done and the document has been persisted.
function callback(err, doc, options) {
if (err) throw err;
// One more document has been inserted, increment the count.
count++;
if (count >= docsLength) {
// If we have created all documents, we are done. Just set the response.
getContext().getResponse().setBody(count);
}
else {
// Create next document.
tryCreate(docs[count], callback);
}
}
}

View File

@ -1,81 +0,0 @@
/**
* A DocumentDB stored procedure that bulk deletes documents for a given query.<br/>
* Note: You may need to execute this sproc multiple times (depending whether the sproc is able to delete every document within the execution timeout limit).
*
* @function
* @param {string} query - A query that provides the documents to be deleted (e.g. "SELECT * FROM c WHERE c.founded_year = 2008")
* @returns {Object.<number, boolean>} Returns an object with the two properties:<br/>
* deleted - contains a count of documents deleted<br/>
* continuation - a boolean whether you should execute the sproc again (true if there are more documents to delete; false otherwise).
*/
function bulkDelete(query) {
var collection = getContext().getCollection();
var collectionLink = collection.getSelfLink();
var response = getContext().getResponse();
var responseBody = {
deleted: 0,
continuation: true
};
// Validate input.
if (!query) throw new Error('The query is undefined or null.');
tryQueryAndDelete();
// Recursively runs the query w/ support for continuation tokens.
// Calls tryDelete(documents) as soon as the query returns documents.
function tryQueryAndDelete(continuation) {
var requestOptions = { continuation: continuation };
var isAccepted = collection.queryDocuments(collectionLink, query, requestOptions, function (err, retrievedDocs, responseOptions) {
if (err) throw err;
if (retrievedDocs.length > 0) {
// Begin deleting documents as soon as documents are returned form the query results.
// tryDelete() resumes querying after deleting; no need to page through continuation tokens.
// - this is to prioritize writes over reads given timeout constraints.
tryDelete(retrievedDocs);
}
else if (responseOptions.continuation) {
// Else if the query came back empty, but with a continuation token; repeat the query w/ the token.
tryQueryAndDelete(responseOptions.continuation);
}
else {
// Else if there are no more documents and no continuation token - we are finished deleting documents.
responseBody.continuation = false;
response.setBody(responseBody);
}
});
// If we hit execution bounds - return continuation: true.
if (!isAccepted) {
response.setBody(responseBody);
}
}
// Recursively deletes documents passed in as an array argument.
// Attempts to query for more on empty array.
function tryDelete(documents) {
if (documents.length > 0) {
// Delete the first document in the array.
var isAccepted = collection.deleteDocument(documents[0]._self, {}, function (err, responseOptions) {
if (err) throw err;
responseBody.deleted++;
documents.shift();
// Delete the next document in the array.
tryDelete(documents);
});
// If we hit execution bounds - return continuation: true.
if (!isAccepted) {
response.setBody(responseBody);
}
}
else {
// If the document array is empty, query for more documents.
tryQueryAndDelete();
}
}
}

View File

@ -1,65 +0,0 @@
// Update all ciphers for a user to be dirty
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,87 +0,0 @@
// Update an array of dirty ciphers for a user.
function updateDirtyCiphers(ciphers, userId) {
var context = getContext();
var collection = context.getCollection();
var collectionLink = collection.getSelfLink();
var response = context.getResponse();
var count = 0;
// Validate input.
if (!ciphers) {
throw new Error('The ciphers array is undefined or null.');
}
var ciphersLength = ciphers.length;
if (ciphersLength == 0) {
response.setBody(0);
return;
}
queryAndReplace(ciphers[count]);
function queryAndReplace(cipher, continuation) {
var query = {
query: "SELECT * FROM root r WHERE r.id = @id AND r.UserId = @userId AND r.type = 'cipher' AND r.Dirty = true",
parameters: [{ name: '@id', value: cipher.id }, { name: '@userId', value: userId }]
};
var requestOptions = { continuation: continuation };
var accepted = collection.queryDocuments(collectionLink, query, requestOptions, function (err, documents, responseOptions) {
if (err) throw err;
if (documents.length > 0) {
replace(documents[0], cipher);
}
else if (responseOptions.continuation) {
// try again
queryAndReplace(cipher, responseOptions.continuation);
}
else {
// doc not found, skip it
next();
}
});
if (!accepted) {
response.setBody(count);
}
}
function replace(doc, placementCipher) {
// site
if (doc.CipherType == 1) {
doc.Username = placementCipher.Username;
doc.Password = placementCipher.Password;
doc.Notes = placementCipher.Notes;
doc.Uri = placementCipher.Uri;
}
doc.Name = placementCipher.Name;
doc.RevisionDate = placementCipher.RevisionDate;
// no longer dirty
doc.Dirty = false;
var accepted = collection.replaceDocument(doc._self, doc, function (err) {
if (err) throw err;
next();
});
if (!accepted) {
response.setBody(count);
}
}
function next() {
count++;
if (count >= ciphersLength) {
response.setBody(count);
}
else {
queryAndReplace(ciphers[count]);
}
}
}

View File

@ -1,58 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Bit.Core.Repositories.DocumentDB.Utilities;
using Microsoft.Azure.Documents.Client;
namespace Bit.Core.Repositories.DocumentDB
{
public class UserRepository : Repository<Domains.User>, IUserRepository
{
public UserRepository(DocumentClient client, string databaseId)
: base(client, databaseId)
{ }
public override async Task<Domains.User> GetByIdAsync(string id)
{
return await GetByPartitionIdAsync(id);
}
public async Task<Domains.User> GetByEmailAsync(string email)
{
IEnumerable<Domains.User> docs = null;
await DocumentDBHelpers.ExecuteWithRetryAsync(() =>
{
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)
{
await DeleteByIdAsync(user.Id);
}
public override async Task DeleteByIdAsync(string id)
{
await DocumentDBHelpers.ExecuteWithRetryAsync(async () =>
{
while(true)
{
StoredProcedureResponse<dynamic> sprocResponse = await Client.ExecuteStoredProcedureAsync<dynamic>(
ResolveSprocIdLink(id, "bulkDelete"),
string.Format("SELECT * FROM c WHERE c.id = '{0}' OR c.UserId = '{0}'", id));
if(!(bool)sprocResponse.Response.continuation)
{
break;
}
}
});
}
}
}

View File

@ -1,118 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Azure.Documents;
using Microsoft.Azure.Documents.Client;
using Newtonsoft.Json;
namespace Bit.Core.Repositories.DocumentDB.Utilities
{
public class DocumentDBHelpers
{
public static DocumentClient InitClient(GlobalSettings.DocumentDBSettings settings)
{
var client = new DocumentClient(
new Uri(settings.Uri),
settings.Key,
new ConnectionPolicy
{
ConnectionMode = ConnectionMode.Direct,
ConnectionProtocol = Protocol.Tcp
});
var hashResolver = new ManagedHashPartitionResolver(
GetPartitionKeyExtractor(),
settings.DatabaseId,
settings.CollectionIdPrefix,
settings.NumberOfCollections,
null);
client.PartitionResolvers[UriFactory.CreateDatabaseUri(settings.DatabaseId).OriginalString] = hashResolver;
client.OpenAsync().Wait();
return client;
}
public static async Task ExecuteWithRetryAsync(Func<Task> func, int? retryMax = null)
{
var executionAttempt = 1;
while(true)
{
try
{
await func();
return;
}
catch(DocumentClientException e)
{
await HandleDocumentClientExceptionAsync(e, executionAttempt, retryMax);
}
catch(AggregateException e)
{
var docEx = e.InnerException as DocumentClientException;
if(docEx != null)
{
await HandleDocumentClientExceptionAsync(docEx, executionAttempt, retryMax);
}
}
executionAttempt++;
}
}
public static int GetTakeCount(IEnumerable<object> docs, int maxSizeKb = 500)
{
var takeCount = docs.Count();
while(takeCount > 1)
{
var bytes = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(docs.Take(takeCount)));
if((bytes.Length / 1000) <= maxSizeKb)
{
// array is is small enough
break;
}
takeCount = Convert.ToInt32(Math.Ceiling((double)takeCount / 2));
}
return takeCount;
}
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)
{
await Task.Delay(e.RetryAfter);
}
else {
throw e;
}
}
private static Func<object, string> GetPartitionKeyExtractor()
{
return doc =>
{
if(doc is Domains.User)
{
return ((Domains.User)doc).Id;
}
if(doc is Domains.Cipher)
{
return ((Domains.Cipher)doc).UserId;
}
throw new InvalidOperationException("Document type is not resolvable for the partition key extractor.");
};
}
}
}

View File

@ -1,35 +0,0 @@
using System;
using System.Collections.Generic;
using Microsoft.Azure.Documents.Client;
using Microsoft.Azure.Documents.Partitioning;
namespace Bit.Core.Repositories.DocumentDB.Utilities
{
public class ManagedHashPartitionResolver : HashPartitionResolver
{
public ManagedHashPartitionResolver(
Func<object, string> partitionKeyExtractor,
string databaseId,
string collectionIdPrefix,
int numberOfCollections,
IHashGenerator hashGenerator = null)
: base(
partitionKeyExtractor,
GetCollectionIds(databaseId, collectionIdPrefix, numberOfCollections),
128,
hashGenerator)
{ }
private static List<string> GetCollectionIds(string databaseId, string collectionIdPrefix, int numberOfCollections)
{
var collections = new List<string>();
for(int i = 0; i < numberOfCollections; i++)
{
var collectionIdUri = UriFactory.CreateDocumentCollectionUri(databaseId, string.Concat(collectionIdPrefix, i));
collections.Add(collectionIdUri.OriginalString);
}
return collections;
}
}
}

View File

@ -6,6 +6,7 @@ using System.Threading.Tasks;
using Bit.Core.Repositories.SqlServer.Models;
using DataTableProxy;
using Bit.Core.Domains;
using System.Data;
namespace Bit.Core.Repositories.SqlServer
{
@ -28,6 +29,9 @@ namespace Bit.Core.Repositories.SqlServer
return Task.FromResult(0);
}
// Get the id of the expected user
var userId = ((Cipher)ciphers.First()).UserId;
using(var connection = new SqlConnection(ConnectionString))
{
connection.Open();
@ -92,6 +96,8 @@ namespace Bit.Core.Repositories.SqlServer
[dbo].[Folder] F
INNER JOIN
#TempFolder TF ON F.Id = TF.Id
WHERE
F.[UserId] = @UserId
UPDATE
[dbo].[Site]
@ -109,12 +115,15 @@ namespace Bit.Core.Repositories.SqlServer
[dbo].[Site] S
INNER JOIN
#TempSite TS ON S.Id = TS.Id
WHERE
S.[UserId] = @UserId
DROP TABLE #TempFolder
DROP TABLE #TempSite";
using(var cmd = new SqlCommand(sqlUpdate, connection, transaction))
{
cmd.Parameters.Add("@UserId", SqlDbType.UniqueIdentifier).Value = new Guid(userId);
cmd.ExecuteNonQuery();
}

View File

@ -5,8 +5,6 @@ namespace Bit.Core.Services
{
public interface IMailService
{
Task SendAlreadyRegisteredEmailAsync(string registrantEmailAddress);
Task SendRegisterEmailAsync(string registrantEmailAddress, string token);
Task SendWelcomeEmailAsync(User user);
Task SendChangeEmailAlreadyExistsEmailAsync(string fromEmail, string toEmail);
Task SendChangeEmailEmailAsync(string newEmailAddress, string token);

View File

@ -10,8 +10,7 @@ namespace Bit.Core.Services
{
Task<User> GetUserByIdAsync(string userId);
Task SaveUserAsync(User user);
Task InitiateRegistrationAsync(string email);
Task<IdentityResult> RegisterUserAsync(string token, User user, string masterPassword);
Task<IdentityResult> RegisterUserAsync(User user, string masterPassword);
Task SendMasterPasswordHintAsync(string email);
Task InitiateEmailChangeAsync(User user, string newEmail);
Task<IdentityResult> ChangeEmailAsync(User user, string masterPassword, string newEmail, string newMasterPassword, string token, IEnumerable<dynamic> ciphers);

View File

@ -9,8 +9,6 @@ namespace Bit.Core.Services
{
public class MailService : IMailService
{
private const string AlreadyRegisteredTemplateId = "8af9cd2b-e4dd-497a-bcc6-1d5b317ff811";
private const string RegisterTemplateId = "7382e1f9-50c7-428d-aa06-bf584f03cd6a";
private const string WelcomeTemplateId = "d24aa21e-5ead-45d8-a14e-f96ba7ec63ff";
private const string ChangeEmailAlreadyExistsTemplateId = "b28bc69e-9592-4320-b274-bfb955667add";
private const string ChangeEmailTemplateId = "b8d17dd7-c883-4b47-8170-5b845d487929";
@ -29,32 +27,6 @@ namespace Bit.Core.Services
_web = new Web(_globalSettings.Mail.ApiKey);
}
public async Task SendAlreadyRegisteredEmailAsync(string registrantEmailAddress)
{
var message = CreateDefaultMessage(AlreadyRegisteredTemplateId);
message.Subject = "Your Registration";
message.AddTo(registrantEmailAddress);
message.AddSubstitution("{{email}}", new List<string> { registrantEmailAddress });
message.SetCategories(new List<string> { AdministrativeCategoryName, "Already Registered" });
await _web.DeliverAsync(message);
}
public async Task SendRegisterEmailAsync(string registrantEmailAddress, string token)
{
var message = CreateDefaultMessage(RegisterTemplateId);
message.Subject = "Complete Your Registration";
message.AddTo(registrantEmailAddress);
message.AddSubstitution("{{token}}", new List<string> { Uri.EscapeDataString(token) });
message.AddSubstitution("{{email}}", new List<string> { Uri.EscapeDataString(registrantEmailAddress) });
message.SetCategories(new List<string> { AdministrativeCategoryName, "Register" });
message.DisableBypassListManagement();
await _web.DeliverAsync(message);
}
public async Task SendWelcomeEmailAsync(User user)
{
var message = CreateDefaultMessage(WelcomeTemplateId);

View File

@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNet.DataProtection;
using Microsoft.AspNet.Http;
using Microsoft.AspNet.Identity;
using Microsoft.Extensions.Logging;
@ -19,7 +18,6 @@ namespace Bit.Core.Services
private readonly IUserRepository _userRepository;
private readonly ICipherRepository _cipherRepository;
private readonly IMailService _mailService;
private readonly ITimeLimitedDataProtector _registrationEmailDataProtector;
private readonly IdentityErrorDescriber _identityErrorDescriber;
private readonly IdentityOptions _identityOptions;
private readonly IPasswordHasher<User> _passwordHasher;
@ -29,7 +27,6 @@ namespace Bit.Core.Services
IUserRepository userRepository,
ICipherRepository cipherRepository,
IMailService mailService,
IDataProtectionProvider dataProtectionProvider,
IUserStore<User> store,
IOptions<IdentityOptions> optionsAccessor,
IPasswordHasher<User> passwordHasher,
@ -55,7 +52,6 @@ namespace Bit.Core.Services
_userRepository = userRepository;
_cipherRepository = cipherRepository;
_mailService = mailService;
_registrationEmailDataProtector = dataProtectionProvider.CreateProtector("RegistrationEmail").ToTimeLimitedDataProtector();
_identityOptions = optionsAccessor?.Value ?? new IdentityOptions();
_identityErrorDescriber = errors;
_passwordHasher = passwordHasher;
@ -77,34 +73,8 @@ namespace Bit.Core.Services
await _userRepository.ReplaceAsync(user);
}
public async Task InitiateRegistrationAsync(string email)
public async Task<IdentityResult> RegisterUserAsync(User user, string masterPassword)
{
var existingUser = await _userRepository.GetByEmailAsync(email);
if(existingUser != null)
{
await _mailService.SendAlreadyRegisteredEmailAsync(email);
return;
}
var token = _registrationEmailDataProtector.Protect(email, TimeSpan.FromDays(5));
await _mailService.SendRegisterEmailAsync(email, token);
}
public async Task<IdentityResult> RegisterUserAsync(string token, User user, string masterPassword)
{
try
{
var tokenEmail = _registrationEmailDataProtector.Unprotect(token);
if(tokenEmail != user.Email)
{
return IdentityResult.Failed(_identityErrorDescriber.InvalidToken());
}
}
catch
{
return IdentityResult.Failed(_identityErrorDescriber.InvalidToken());
}
var result = await base.CreateAsync(user, masterPassword);
if(result == IdentityResult.Success)
{
@ -163,8 +133,6 @@ namespace Bit.Core.Services
return IdentityResult.Failed(_identityErrorDescriber.DuplicateEmail(newEmail));
}
user.OldEmail = user.Email;
user.OldMasterPassword = user.MasterPassword;
user.Email = newEmail;
user.MasterPassword = _passwordHasher.HashPassword(user, newMasterPassword);
user.SecurityStamp = Guid.NewGuid().ToString();
@ -279,7 +247,6 @@ namespace Bit.Core.Services
}
}
user.OldMasterPassword = user.MasterPassword;
user.MasterPassword = _passwordHasher.HashPassword(user, newPassword);
user.SecurityStamp = Guid.NewGuid().ToString();

View File

@ -12,9 +12,6 @@
"OtpSharp": "1.3.0.4",
"Microsoft.AspNet.Mvc.Abstractions": "6.0.0-rc1-final",
"Sendgrid": "6.3.4",
"Microsoft.AspNet.DataProtection.Extensions": "1.0.0-rc1-final",
"Microsoft.Azure.DocumentDB": "1.5.2",
"Newtonsoft.Json": "8.0.1",
"Dapper": "1.42.0",
"DataTableProxy": "1.2.0"
},