1
0
mirror of https://github.com/bitwarden/server synced 2025-12-26 21:23:39 +00:00

initial commit of source

This commit is contained in:
Kyle Spearrin
2015-12-08 22:57:38 -05:00
commit 437b971003
87 changed files with 3819 additions and 0 deletions

View File

@@ -0,0 +1,67 @@
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

@@ -0,0 +1,40 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Azure.Documents.Client;
using Bit.Core.Domains;
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 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 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),
userId,
Cipher.TypeValue);
var replacedCount = sprocResponse.Response;
if(replacedCount != cleanedCiphers.Count())
{
await UpdateDirtyCiphersAsync(cleanedCiphers.Skip(replacedCount));
}
}
}
}

View File

@@ -0,0 +1,49 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Azure.Documents.Client;
using Bit.Core.Domains;
using Bit.Core.Enums;
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)
{
var doc = await Client.ReadDocumentAsync(ResolveDocumentIdLink(userId, id));
if(doc?.Resource == null)
{
return null;
}
var folder = (Folder)((dynamic)doc.Resource);
if(folder.UserId != userId)
{
return null;
}
return folder;
}
public 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();
return Task.FromResult<ICollection<Folder>>(docs.ToList());
}
public 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();
return Task.FromResult<ICollection<Folder>>(docs.ToList());
}
}
}

View File

@@ -0,0 +1,76 @@
using System;
using System.Linq;
using System.Threading.Tasks;
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 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());
}
public virtual async Task CreateAsync(T obj)
{
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);
}
public virtual async Task UpsertAsync(T obj)
{
await Client.UpsertDocumentAsync(ResolveDocumentIdLink(obj), obj);
}
public virtual async Task DeleteAsync(T obj)
{
await Client.DeleteDocumentAsync(ResolveDocumentIdLink(obj));
}
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.
var docs = Client.CreateDocumentQuery(DatabaseUri, new FeedOptions { MaxItemCount = 1 })
.Where(d => d.Id == id).AsEnumerable();
if(docs.Count() > 0)
{
await Client.DeleteDocumentAsync(docs.First().SelfLink);
}
}
protected async Task<T> GetByPartitionIdAsync(string id)
{
var doc = await Client.ReadDocumentAsync(ResolveDocumentIdLink(id));
if(doc?.Resource == null)
{
return default(T);
}
return (T)((dynamic)doc.Resource);
}
protected async Task DeleteByPartitionIdAsync(string id)
{
await Client.DeleteDocumentAsync(ResolveDocumentIdLink(id));
}
}
}

View File

@@ -0,0 +1,50 @@
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;
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)
{
var doc = await Client.ReadDocumentAsync(ResolveDocumentIdLink(userId, id));
if(doc?.Resource == null)
{
return null;
}
var site = (Site)((dynamic)doc.Resource);
if(site.UserId != userId)
{
return null;
}
return site;
}
public 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();
return Task.FromResult<ICollection<Site>>(docs.ToList());
}
public 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();
return Task.FromResult<ICollection<Site>>(docs.ToList());
}
}
}

View File

@@ -0,0 +1,87 @@
// Update an array of dirty ciphers for a user.
function bulkUpdateDirtyCiphers(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

@@ -0,0 +1,107 @@
// 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

@@ -0,0 +1,37 @@
using System;
using System.Linq;
using System.Threading.Tasks;
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 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 Client.ExecuteStoredProcedureAsync<Domains.User>(ResolveSprocIdLink(user, "replaceUserAndDirtyCiphers"), user);
}
public override async Task DeleteByIdAsync(string id)
{
await DeleteByPartitionIdAsync(id);
}
}
}

View File

@@ -0,0 +1,50 @@
using System;
using Microsoft.Azure.Documents.Client;
namespace Bit.Core.Repositories.DocumentDB.Utilities
{
public class DocumentClientHelpers
{
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;
}
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

@@ -0,0 +1,35 @@
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

@@ -0,0 +1,10 @@
using System.Collections.Generic;
using System.Threading.Tasks;
namespace Bit.Core.Repositories
{
public interface ICipherRepository
{
Task UpdateDirtyCiphersAsync(IEnumerable<dynamic> ciphers);
}
}

View File

@@ -0,0 +1,13 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Bit.Core.Domains;
namespace Bit.Core.Repositories
{
public interface IFolderRepository : IRepository<Folder>
{
Task<Folder> GetByIdAsync(string id, string userId);
Task<ICollection<Folder>> GetManyByUserIdAsync(string userId);
Task<ICollection<Folder>> GetManyByUserIdAsync(string userId, bool dirty);
}
}

View File

@@ -0,0 +1,14 @@
using System.Threading.Tasks;
namespace Bit.Core.Repositories
{
public interface IRepository<T> where T : IDataObject
{
Task<T> GetByIdAsync(string id);
Task CreateAsync(T obj);
Task ReplaceAsync(T obj);
Task UpsertAsync(T obj);
Task DeleteByIdAsync(string id);
Task DeleteAsync(T obj);
}
}

View File

@@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Bit.Core.Domains;
namespace Bit.Core.Repositories
{
public interface ISiteRepository : IRepository<Site>
{
Task<Site> GetByIdAsync(string id, string userId);
Task<ICollection<Site>> GetManyByUserIdAsync(string userId);
Task<ICollection<Site>> GetManyByUserIdAsync(string userId, bool dirty);
}
}

View File

@@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Bit.Core.Domains;
namespace Bit.Core.Repositories
{
public interface IUserRepository : IRepository<User>
{
Task<User> GetByEmailAsync(string email);
Task ReplaceAndDirtyCiphersAsync(User user);
}
}