1
0
mirror of https://github.com/bitwarden/server.git synced 2025-05-20 19:14:32 -05:00

Added cipher service with bulk import to account controller

This commit is contained in:
Kyle Spearrin 2015-12-26 23:09:53 -05:00
parent 437b971003
commit 8d7178bc74
14 changed files with 246 additions and 29 deletions

View File

@ -11,6 +11,7 @@ using Bit.Core.Domains;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core; using Bit.Core;
using System.Security.Claims; using System.Security.Claims;
using System.Linq;
namespace Bit.Api.Controllers namespace Bit.Api.Controllers
{ {
@ -19,16 +20,18 @@ namespace Bit.Api.Controllers
public class AccountsController : Controller public class AccountsController : Controller
{ {
private readonly IUserService _userService; private readonly IUserService _userService;
private readonly ICipherService _cipherService;
private readonly UserManager<User> _userManager; private readonly UserManager<User> _userManager;
private readonly CurrentContext _currentContext; private readonly CurrentContext _currentContext;
public AccountsController( public AccountsController(
IDataProtectionProvider dataProtectionProvider,
IUserService userService, IUserService userService,
ICipherService cipherService,
UserManager<User> userManager, UserManager<User> userManager,
CurrentContext currentContext) CurrentContext currentContext)
{ {
_userService = userService; _userService = userService;
_cipherService = cipherService;
_userManager = userManager; _userManager = userManager;
_currentContext = currentContext; _currentContext = currentContext;
} }
@ -206,5 +209,14 @@ namespace Bit.Api.Controllers
var response = new TwoFactorResponseModel(user); var response = new TwoFactorResponseModel(user);
return response; return response;
} }
[HttpPost("import")]
public async Task PostImport([FromBody]ImportRequestModel model)
{
await _cipherService.ImportCiphersAsync(
model.Folders.Select(f => f.ToFolder(User.GetUserId())).ToList(),
model.Sites.Select(s => s.ToSite(User.GetUserId())).ToList(),
model.SiteRelationships);
}
} }
} }

View File

@ -144,5 +144,4 @@ namespace Bit.Api.Controllers
} }
} }
} }
} }

View File

@ -0,0 +1,12 @@
using System;
using System.Collections.Generic;
namespace Bit.Api.Models
{
public class ImportRequestModel
{
public FolderRequestModel[] Folders { get; set; }
public SiteRequestModel[] Sites { get; set; }
public KeyValuePair<int, int>[] SiteRelationships { get; set; }
}
}

View File

@ -71,10 +71,6 @@ namespace Bit.Api.Models
{ {
yield return new ValidationResult("Uri is required for a site cypher.", new[] { "Uri" }); yield return new ValidationResult("Uri is required for a site cypher.", new[] { "Uri" });
} }
if(string.IsNullOrWhiteSpace(Username))
{
yield return new ValidationResult("Username is required for a site cypher.", new[] { "Username" });
}
if(string.IsNullOrWhiteSpace(Password)) if(string.IsNullOrWhiteSpace(Password))
{ {
yield return new ValidationResult("Password is required for a site cypher.", new[] { "Password" }); yield return new ValidationResult("Password is required for a site cypher.", new[] { "Password" });

View File

@ -14,7 +14,6 @@ namespace Bit.Api.Models
[Required] [Required]
[EncryptedString] [EncryptedString]
public string Uri { get; set; } public string Uri { get; set; }
[Required]
[EncryptedString] [EncryptedString]
public string Username { get; set; } public string Username { get; set; }
[Required] [Required]
@ -31,7 +30,7 @@ namespace Bit.Api.Models
FolderId = string.IsNullOrWhiteSpace(FolderId) ? null : FolderId, FolderId = string.IsNullOrWhiteSpace(FolderId) ? null : FolderId,
Name = Name, Name = Name,
Uri = Uri, Uri = Uri,
Username = Username, Username = string.IsNullOrWhiteSpace(Username) ? null : Username,
Password = Password, Password = Password,
Notes = string.IsNullOrWhiteSpace(Notes) ? null : Notes Notes = string.IsNullOrWhiteSpace(Notes) ? null : Notes
}; };
@ -42,7 +41,7 @@ namespace Bit.Api.Models
existingSite.FolderId = string.IsNullOrWhiteSpace(FolderId) ? null : FolderId; existingSite.FolderId = string.IsNullOrWhiteSpace(FolderId) ? null : FolderId;
existingSite.Name = Name; existingSite.Name = Name;
existingSite.Uri = Uri; existingSite.Uri = Uri;
existingSite.Username = Username; existingSite.Username = string.IsNullOrWhiteSpace(Username) ? null : Username;
existingSite.Password = Password; existingSite.Password = Password;
existingSite.Notes = string.IsNullOrWhiteSpace(Notes) ? null : Notes; existingSite.Notes = string.IsNullOrWhiteSpace(Notes) ? null : Notes;

View File

@ -53,7 +53,7 @@ namespace Bit.Api
services.AddSingleton(s => globalSettings); services.AddSingleton(s => globalSettings);
// Repositories // Repositories
var documentDBClient = DocumentClientHelpers.InitClient(globalSettings.DocumentDB); var documentDBClient = DocumentDBHelpers.InitClient(globalSettings.DocumentDB);
services.AddSingleton<IUserRepository>(s => new Repos.UserRepository(documentDBClient, globalSettings.DocumentDB.DatabaseId)); services.AddSingleton<IUserRepository>(s => new Repos.UserRepository(documentDBClient, globalSettings.DocumentDB.DatabaseId));
services.AddSingleton<ISiteRepository>(s => new Repos.SiteRepository(documentDBClient, globalSettings.DocumentDB.DatabaseId)); services.AddSingleton<ISiteRepository>(s => new Repos.SiteRepository(documentDBClient, globalSettings.DocumentDB.DatabaseId));
services.AddSingleton<IFolderRepository>(s => new Repos.FolderRepository(documentDBClient, globalSettings.DocumentDB.DatabaseId)); services.AddSingleton<IFolderRepository>(s => new Repos.FolderRepository(documentDBClient, globalSettings.DocumentDB.DatabaseId));
@ -116,6 +116,7 @@ namespace Bit.Api
// Services // Services
services.AddSingleton<IMailService, MailService>(); services.AddSingleton<IMailService, MailService>();
services.AddSingleton<ICipherService, CipherService>();
services.AddScoped<IUserService, UserService>(); services.AddScoped<IUserService, UserService>();
// Cors // Cors

View File

@ -4,6 +4,7 @@ using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Azure.Documents.Client; using Microsoft.Azure.Documents.Client;
using Bit.Core.Domains; using Bit.Core.Domains;
using Bit.Core.Repositories.DocumentDB.Utilities;
namespace Bit.Core.Repositories.DocumentDB namespace Bit.Core.Repositories.DocumentDB
{ {
@ -14,6 +15,8 @@ namespace Bit.Core.Repositories.DocumentDB
{ } { }
public async Task UpdateDirtyCiphersAsync(IEnumerable<dynamic> ciphers) public async Task UpdateDirtyCiphersAsync(IEnumerable<dynamic> ciphers)
{
await DocumentDBHelpers.QueryWithRetryAsync(async () =>
{ {
// Make sure we are dealing with cipher types since we accept any via dynamic. // Make sure we are dealing with cipher types since we accept any via dynamic.
var cleanedCiphers = ciphers.Where(c => c is Cipher); var cleanedCiphers = ciphers.Where(c => c is Cipher);
@ -27,14 +30,39 @@ namespace Bit.Core.Repositories.DocumentDB
ResolveSprocIdLink(userId, "bulkUpdateDirtyCiphers"), ResolveSprocIdLink(userId, "bulkUpdateDirtyCiphers"),
// Do sets of 50. Recursion will handle the rest below. // Do sets of 50. Recursion will handle the rest below.
cleanedCiphers.Take(50), cleanedCiphers.Take(50),
userId, userId);
Cipher.TypeValue);
var replacedCount = sprocResponse.Response; var replacedCount = sprocResponse.Response;
if(replacedCount != cleanedCiphers.Count()) if(replacedCount != cleanedCiphers.Count())
{ {
await UpdateDirtyCiphersAsync(cleanedCiphers.Skip(replacedCount)); await UpdateDirtyCiphersAsync(cleanedCiphers.Skip(replacedCount));
} }
});
}
public async Task CreateAsync(IEnumerable<dynamic> ciphers)
{
await DocumentDBHelpers.QueryWithRetryAsync(async () =>
{
// 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, "bulkCreate"),
// Do sets of 50. Recursion will handle the rest below.
cleanedCiphers.Take(50));
var createdCount = sprocResponse.Response;
if(createdCount != cleanedCiphers.Count())
{
await CreateAsync(cleanedCiphers.Skip(createdCount));
}
});
} }
} }
} }

View File

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

@ -10,7 +10,7 @@ function bulkUpdateDirtyCiphers(ciphers, userId) {
// Validate input. // Validate input.
if (!ciphers) { if (!ciphers) {
throw new Error("The ciphers array is undefined or null."); throw new Error('The ciphers array is undefined or null.');
} }
var ciphersLength = ciphers.length; var ciphersLength = ciphers.length;

View File

@ -1,6 +1,8 @@
using System; using System;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Bit.Core.Repositories.DocumentDB.Utilities;
using Microsoft.Azure.Documents;
using Microsoft.Azure.Documents.Client; using Microsoft.Azure.Documents.Client;
namespace Bit.Core.Repositories.DocumentDB namespace Bit.Core.Repositories.DocumentDB
@ -25,8 +27,11 @@ namespace Bit.Core.Repositories.DocumentDB
} }
public async Task ReplaceAndDirtyCiphersAsync(Domains.User user) public async Task ReplaceAndDirtyCiphersAsync(Domains.User user)
{
await DocumentDBHelpers.QueryWithRetryAsync(async () =>
{ {
await Client.ExecuteStoredProcedureAsync<Domains.User>(ResolveSprocIdLink(user, "replaceUserAndDirtyCiphers"), user); await Client.ExecuteStoredProcedureAsync<Domains.User>(ResolveSprocIdLink(user, "replaceUserAndDirtyCiphers"), user);
});
} }
public override async Task DeleteByIdAsync(string id) public override async Task DeleteByIdAsync(string id)

View File

@ -1,9 +1,11 @@
using System; using System;
using System.Threading.Tasks;
using Microsoft.Azure.Documents;
using Microsoft.Azure.Documents.Client; using Microsoft.Azure.Documents.Client;
namespace Bit.Core.Repositories.DocumentDB.Utilities namespace Bit.Core.Repositories.DocumentDB.Utilities
{ {
public class DocumentClientHelpers public class DocumentDBHelpers
{ {
public static DocumentClient InitClient(GlobalSettings.DocumentDBSettings settings) public static DocumentClient InitClient(GlobalSettings.DocumentDBSettings settings)
{ {
@ -29,6 +31,43 @@ namespace Bit.Core.Repositories.DocumentDB.Utilities
return client; return client;
} }
public static async Task QueryWithRetryAsync(Func<Task> func)
{
var queryComplete = false;
while(!queryComplete)
{
try
{
await func();
queryComplete = true;
}
catch(DocumentClientException e)
{
await HandleDocumentClientExceptionAsync(e);
}
catch(AggregateException e)
{
var docEx = e.InnerException as DocumentClientException;
if(docEx != null)
{
await HandleDocumentClientExceptionAsync(docEx);
}
}
}
}
private static async Task HandleDocumentClientExceptionAsync(DocumentClientException 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() private static Func<object, string> GetPartitionKeyExtractor()
{ {
return doc => return doc =>

View File

@ -6,5 +6,6 @@ namespace Bit.Core.Repositories
public interface ICipherRepository public interface ICipherRepository
{ {
Task UpdateDirtyCiphersAsync(IEnumerable<dynamic> ciphers); Task UpdateDirtyCiphersAsync(IEnumerable<dynamic> ciphers);
Task CreateAsync(IEnumerable<dynamic> ciphers);
} }
} }

View File

@ -0,0 +1,54 @@
using System;
using System.Linq;
using System.Collections.Generic;
using System.Threading.Tasks;
using Bit.Core.Domains;
using Bit.Core.Repositories;
namespace Bit.Core.Services
{
public class CipherService : ICipherService
{
private readonly IFolderRepository _folderRepository;
private readonly ICipherRepository _cipherRepository;
public CipherService(
IFolderRepository folderRepository,
ICipherRepository cipherRepository)
{
_folderRepository = folderRepository;
_cipherRepository = cipherRepository;
}
public async Task ImportCiphersAsync(
List<Folder> folders,
List<Site> sites,
IEnumerable<KeyValuePair<int, int>> siteRelationships)
{
// create all the folders
var folderTasks = new List<Task>();
foreach(var folder in folders)
{
folderTasks.Add(_folderRepository.CreateAsync(folder));
}
await Task.WhenAll(folderTasks);
// associate the newly created folders to the sites
foreach(var relationship in siteRelationships)
{
var site = sites.ElementAtOrDefault(relationship.Key);
var folder = folders.ElementAtOrDefault(relationship.Value);
if(site == null || folder == null)
{
continue;
}
site.FolderId = folder.Id;
}
// create all the sites
await _cipherRepository.CreateAsync(sites);
}
}
}

View File

@ -0,0 +1,11 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Bit.Core.Domains;
namespace Bit.Core.Services
{
public interface ICipherService
{
Task ImportCiphersAsync(List<Folder> folders, List<Site> sites, IEnumerable<KeyValuePair<int, int>> siteRelationships);
}
}