1
0
mirror of https://github.com/bitwarden/server.git synced 2024-12-22 16:57:36 +01:00

refactored data storage to use cipher table. added history table and insert triggers.

This commit is contained in:
Kyle Spearrin 2016-05-21 17:16:22 -04:00
parent 8137847485
commit 3fdb0fcf67
56 changed files with 422 additions and 646 deletions

View File

@ -78,7 +78,7 @@ namespace Bit.Api.Controllers
{ {
// NOTE: It is assumed that the eventual repository call will make sure the updated // NOTE: It is assumed that the eventual repository call will make sure the updated
// ciphers belong to user making this call. Therefore, no check is done here. // ciphers belong to user making this call. Therefore, no check is done here.
var ciphers = CipherRequestModel.ToDynamicCiphers(model.Ciphers, _userManager.GetUserId(User)); var ciphers = model.Ciphers.Select(c => c.ToCipher(_userManager.GetUserId(User)));
var result = await _userService.ChangeEmailAsync( var result = await _userService.ChangeEmailAsync(
_currentContext.User, _currentContext.User,
@ -107,7 +107,7 @@ namespace Bit.Api.Controllers
{ {
// NOTE: It is assumed that the eventual repository call will make sure the updated // NOTE: It is assumed that the eventual repository call will make sure the updated
// ciphers belong to user making this call. Therefore, no check is done here. // ciphers belong to user making this call. Therefore, no check is done here.
var ciphers = CipherRequestModel.ToDynamicCiphers(model.Ciphers, _userManager.GetUserId(User)); var ciphers = model.Ciphers.Select(c => c.ToCipher(_userManager.GetUserId(User)));
var result = await _userService.ChangePasswordAsync( var result = await _userService.ChangePasswordAsync(
_currentContext.User, _currentContext.User,
@ -206,8 +206,8 @@ namespace Bit.Api.Controllers
public async Task PostImport([FromBody]ImportRequestModel model) public async Task PostImport([FromBody]ImportRequestModel model)
{ {
await _cipherService.ImportCiphersAsync( await _cipherService.ImportCiphersAsync(
model.Folders.Select(f => f.ToFolder(_userManager.GetUserId(User))).ToList(), model.Folders.Select(f => f.ToCipher(_userManager.GetUserId(User))).ToList(),
model.Sites.Select(s => s.ToSite(_userManager.GetUserId(User))).ToList(), model.Sites.Select(s => s.ToCipher(_userManager.GetUserId(User))).ToList(),
model.SiteRelationships); model.SiteRelationships);
} }

View File

@ -16,22 +16,22 @@ namespace Bit.Api.Controllers
[Authorize("Application")] [Authorize("Application")]
public class FoldersController : Controller public class FoldersController : Controller
{ {
private readonly IFolderRepository _folderRepository; private readonly ICipherRepository _cipherRepository;
private readonly UserManager<User> _userManager; private readonly UserManager<User> _userManager;
public FoldersController( public FoldersController(
IFolderRepository folderRepository, ICipherRepository cipherRepository,
UserManager<User> userManager) UserManager<User> userManager)
{ {
_folderRepository = folderRepository; _cipherRepository = cipherRepository;
_userManager = userManager; _userManager = userManager;
} }
[HttpGet("{id}")] [HttpGet("{id}")]
public async Task<FolderResponseModel> Get(string id) public async Task<FolderResponseModel> Get(string id)
{ {
var folder = await _folderRepository.GetByIdAsync(id, _userManager.GetUserId(User)); var folder = await _cipherRepository.GetByIdAsync(new Guid(id), new Guid(_userManager.GetUserId(User)));
if(folder == null) if(folder == null || folder.Type != Core.Enums.CipherType.Folder)
{ {
throw new NotFoundException(); throw new NotFoundException();
} }
@ -42,7 +42,7 @@ namespace Bit.Api.Controllers
[HttpGet("")] [HttpGet("")]
public async Task<ListResponseModel<FolderResponseModel>> Get() public async Task<ListResponseModel<FolderResponseModel>> Get()
{ {
ICollection<Folder> folders = await _folderRepository.GetManyByUserIdAsync(_userManager.GetUserId(User)); ICollection<Cipher> folders = await _cipherRepository.GetManyByTypeAndUserIdAsync(Core.Enums.CipherType.Folder, new Guid(_userManager.GetUserId(User)));
var responses = folders.Select(f => new FolderResponseModel(f)); var responses = folders.Select(f => new FolderResponseModel(f));
return new ListResponseModel<FolderResponseModel>(responses); return new ListResponseModel<FolderResponseModel>(responses);
} }
@ -50,34 +50,34 @@ namespace Bit.Api.Controllers
[HttpPost("")] [HttpPost("")]
public async Task<FolderResponseModel> Post([FromBody]FolderRequestModel model) public async Task<FolderResponseModel> Post([FromBody]FolderRequestModel model)
{ {
var folder = model.ToFolder(_userManager.GetUserId(User)); var folder = model.ToCipher(_userManager.GetUserId(User));
await _folderRepository.CreateAsync(folder); await _cipherRepository.CreateAsync(folder);
return new FolderResponseModel(folder); return new FolderResponseModel(folder);
} }
[HttpPut("{id}")] [HttpPut("{id}")]
public async Task<FolderResponseModel> Put(string id, [FromBody]FolderRequestModel model) public async Task<FolderResponseModel> Put(string id, [FromBody]FolderRequestModel model)
{ {
var folder = await _folderRepository.GetByIdAsync(id, _userManager.GetUserId(User)); var folder = await _cipherRepository.GetByIdAsync(new Guid(id), new Guid(_userManager.GetUserId(User)));
if(folder == null) if(folder == null || folder.Type != Core.Enums.CipherType.Folder)
{ {
throw new NotFoundException(); throw new NotFoundException();
} }
await _folderRepository.ReplaceAsync(model.ToFolder(folder)); await _cipherRepository.ReplaceAsync(model.ToCipher(folder));
return new FolderResponseModel(folder); return new FolderResponseModel(folder);
} }
[HttpDelete("{id}")] [HttpDelete("{id}")]
public async Task Delete(string id) public async Task Delete(string id)
{ {
var folder = await _folderRepository.GetByIdAsync(id, _userManager.GetUserId(User)); var folder = await _cipherRepository.GetByIdAsync(new Guid(id), new Guid(_userManager.GetUserId(User)));
if(folder == null) if(folder == null || folder.Type != Core.Enums.CipherType.Folder)
{ {
throw new NotFoundException(); throw new NotFoundException();
} }
await _folderRepository.DeleteAsync(folder); await _cipherRepository.DeleteAsync(folder);
} }
} }
} }

View File

@ -16,25 +16,22 @@ namespace Bit.Api.Controllers
[Authorize("Application")] [Authorize("Application")]
public class SitesController : Controller public class SitesController : Controller
{ {
private readonly ISiteRepository _siteRepository; private readonly ICipherRepository _cipherRepository;
private readonly IFolderRepository _folderRepository;
private readonly UserManager<User> _userManager; private readonly UserManager<User> _userManager;
public SitesController( public SitesController(
ISiteRepository siteRepository, ICipherRepository cipherRepository,
IFolderRepository folderRepository,
UserManager<User> userManager) UserManager<User> userManager)
{ {
_siteRepository = siteRepository; _cipherRepository = cipherRepository;
_folderRepository = folderRepository;
_userManager = userManager; _userManager = userManager;
} }
[HttpGet("{id}")] [HttpGet("{id}")]
public async Task<SiteResponseModel> Get(string id, string[] expand = null) public async Task<SiteResponseModel> Get(string id, string[] expand = null)
{ {
var site = await _siteRepository.GetByIdAsync(id, _userManager.GetUserId(User)); var site = await _cipherRepository.GetByIdAsync(new Guid(id), new Guid(_userManager.GetUserId(User)));
if(site == null) if(site == null || site.Type != Core.Enums.CipherType.Site)
{ {
throw new NotFoundException(); throw new NotFoundException();
} }
@ -47,7 +44,7 @@ namespace Bit.Api.Controllers
[HttpGet("")] [HttpGet("")]
public async Task<ListResponseModel<SiteResponseModel>> Get(string[] expand = null) public async Task<ListResponseModel<SiteResponseModel>> Get(string[] expand = null)
{ {
ICollection<Site> sites = await _siteRepository.GetManyByUserIdAsync(_userManager.GetUserId(User)); ICollection<Cipher> sites = await _cipherRepository.GetManyByTypeAndUserIdAsync(Core.Enums.CipherType.Site, new Guid(_userManager.GetUserId(User)));
var responses = sites.Select(s => new SiteResponseModel(s)).ToList(); var responses = sites.Select(s => new SiteResponseModel(s)).ToList();
await ExpandManyAsync(sites, responses, expand, null); await ExpandManyAsync(sites, responses, expand, null);
return new ListResponseModel<SiteResponseModel>(responses); return new ListResponseModel<SiteResponseModel>(responses);
@ -56,8 +53,8 @@ namespace Bit.Api.Controllers
[HttpPost("")] [HttpPost("")]
public async Task<SiteResponseModel> Post([FromBody]SiteRequestModel model, string[] expand = null) public async Task<SiteResponseModel> Post([FromBody]SiteRequestModel model, string[] expand = null)
{ {
var site = model.ToSite(_userManager.GetUserId(User)); var site = model.ToCipher(_userManager.GetUserId(User));
await _siteRepository.CreateAsync(site); await _cipherRepository.CreateAsync(site);
var response = new SiteResponseModel(site); var response = new SiteResponseModel(site);
await ExpandAsync(site, response, expand, null); await ExpandAsync(site, response, expand, null);
@ -67,13 +64,13 @@ namespace Bit.Api.Controllers
[HttpPut("{id}")] [HttpPut("{id}")]
public async Task<SiteResponseModel> Put(string id, [FromBody]SiteRequestModel model, string[] expand = null) public async Task<SiteResponseModel> Put(string id, [FromBody]SiteRequestModel model, string[] expand = null)
{ {
var site = await _siteRepository.GetByIdAsync(id, _userManager.GetUserId(User)); var site = await _cipherRepository.GetByIdAsync(new Guid(id), new Guid(_userManager.GetUserId(User)));
if(site == null) if(site == null || site.Type != Core.Enums.CipherType.Site)
{ {
throw new NotFoundException(); throw new NotFoundException();
} }
await _siteRepository.ReplaceAsync(model.ToSite(site)); await _cipherRepository.ReplaceAsync(model.ToCipher(site));
var response = new SiteResponseModel(site); var response = new SiteResponseModel(site);
await ExpandAsync(site, response, expand, null); await ExpandAsync(site, response, expand, null);
@ -83,34 +80,34 @@ namespace Bit.Api.Controllers
[HttpDelete("{id}")] [HttpDelete("{id}")]
public async Task Delete(string id) public async Task Delete(string id)
{ {
var site = await _siteRepository.GetByIdAsync(id, _userManager.GetUserId(User)); var site = await _cipherRepository.GetByIdAsync(new Guid(id), new Guid(_userManager.GetUserId(User)));
if(site == null) if(site == null || site.Type != Core.Enums.CipherType.Site)
{ {
throw new NotFoundException(); throw new NotFoundException();
} }
await _siteRepository.DeleteAsync(site); await _cipherRepository.DeleteAsync(site);
} }
private async Task ExpandAsync(Site site, SiteResponseModel response, string[] expand, Folder folder) private async Task ExpandAsync(Cipher site, SiteResponseModel response, string[] expand, Cipher folder)
{ {
if(expand == null || expand.Count() == 0) if(expand == null || expand.Count() == 0)
{ {
return; return;
} }
if(expand.Any(e => e.ToLower() == "folder")) if(expand.Any(e => e.ToLower() == "folder") && site.FolderId.HasValue)
{ {
if(folder == null) if(folder == null)
{ {
folder = await _folderRepository.GetByIdAsync(site.FolderId); folder = await _cipherRepository.GetByIdAsync(site.FolderId.Value);
} }
response.Folder = new FolderResponseModel(folder); response.Folder = new FolderResponseModel(folder);
} }
} }
private async Task ExpandManyAsync(IEnumerable<Site> sites, ICollection<SiteResponseModel> responses, string[] expand, IEnumerable<Folder> folders) private async Task ExpandManyAsync(IEnumerable<Cipher> sites, ICollection<SiteResponseModel> responses, string[] expand, IEnumerable<Cipher> folders)
{ {
if(expand == null || expand.Count() == 0) if(expand == null || expand.Count() == 0)
{ {
@ -121,14 +118,14 @@ namespace Bit.Api.Controllers
{ {
if(folders == null) if(folders == null)
{ {
folders = await _folderRepository.GetManyByUserIdAsync(_userManager.GetUserId(User)); folders = await _cipherRepository.GetManyByTypeAndUserIdAsync(Core.Enums.CipherType.Folder, new Guid(_userManager.GetUserId(User)));
} }
if(folders != null && folders.Count() > 0) if(folders != null && folders.Count() > 0)
{ {
foreach(var response in responses) foreach(var response in responses)
{ {
var site = sites.SingleOrDefault(s => s.Id == response.Id); var site = sites.SingleOrDefault(s => s.Id.ToString() == response.Id);
if(site == null) if(site == null)
{ {
continue; continue;

View File

@ -0,0 +1,37 @@
namespace Bit.Api.Models
{
public class CipherDataModel
{
public CipherDataModel() { }
public CipherDataModel(CipherRequestModel cipher)
{
Name = cipher.Name;
Uri = cipher.Uri;
Username = cipher.Username;
Password = cipher.Password;
Notes = cipher.Notes;
}
public CipherDataModel(SiteRequestModel site)
{
Name = site.Name;
Uri = site.Uri;
Username = site.Username;
Password = site.Password;
Notes = site.Notes;
}
public CipherDataModel(FolderRequestModel folder)
{
Name = folder.Name;
}
public string Name { get; set; }
public string Uri { get; set; }
public string Username { get; set; }
public string Password { get; set; }
public string Notes { get; set; }
}
}

View File

@ -3,8 +3,8 @@ using System.Collections.Generic;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using Bit.Api.Utilities; using Bit.Api.Utilities;
using Bit.Core.Domains; using Bit.Core.Domains;
using System.Linq;
using Bit.Core.Enums; using Bit.Core.Enums;
using Newtonsoft.Json;
namespace Bit.Api.Models namespace Bit.Api.Models
{ {
@ -34,42 +34,18 @@ namespace Bit.Api.Models
[StringLength(5000)] [StringLength(5000)]
public string Notes { get; set; } public string Notes { get; set; }
public virtual Site ToSite(string userId = null) public virtual Cipher ToCipher(string userId = null)
{ {
return new Site return new Cipher
{ {
Id = Id, Id = new Guid(Id),
UserId = userId, UserId = new Guid(userId),
FolderId = string.IsNullOrWhiteSpace(FolderId) ? null : FolderId, FolderId = string.IsNullOrWhiteSpace(FolderId) ? null : (Guid?)new Guid(FolderId),
Name = Name, Type = Type,
Uri = Uri, Data = JsonConvert.SerializeObject(new CipherDataModel(this), new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore })
Username = Username,
Password = Password,
Notes = string.IsNullOrWhiteSpace(Notes) ? null : Notes
}; };
} }
public Folder ToFolder(string userId = null)
{
return new Folder
{
Id = Id,
UserId = userId,
Name = Name
};
}
public static IEnumerable<dynamic> ToDynamicCiphers(CipherRequestModel[] models, string userId)
{
var sites = models.Where(m => m.Type == CipherType.Site).Select(m => m.ToSite(userId)).ToList();
var folders = models.Where(m => m.Type == CipherType.Folder).Select(m => m.ToFolder(userId)).ToList();
var ciphers = new List<dynamic>();
ciphers.AddRange(sites);
ciphers.AddRange(folders);
return ciphers;
}
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext) public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{ {
if(Type == CipherType.Site) if(Type == CipherType.Site)

View File

@ -2,6 +2,7 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using Bit.Api.Utilities; using Bit.Api.Utilities;
using Bit.Core.Domains; using Bit.Core.Domains;
using Newtonsoft.Json;
namespace Bit.Api.Models namespace Bit.Api.Models
{ {
@ -12,18 +13,20 @@ namespace Bit.Api.Models
[StringLength(300)] [StringLength(300)]
public string Name { get; set; } public string Name { get; set; }
public Folder ToFolder(string userId = null) public Cipher ToCipher(string userId = null)
{ {
return new Folder return new Cipher
{ {
UserId = userId, UserId = new Guid(userId),
Name = Name Data = JsonConvert.SerializeObject(new CipherDataModel(this), new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore }),
Type = Core.Enums.CipherType.Folder
}; };
} }
public Folder ToFolder(Folder existingFolder) public Cipher ToCipher(Cipher existingFolder)
{ {
existingFolder.Name = Name; existingFolder.Data = JsonConvert.SerializeObject(new CipherDataModel(this), new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore });
existingFolder.Type = Core.Enums.CipherType.Folder;
return existingFolder; return existingFolder;
} }

View File

@ -2,6 +2,7 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using Bit.Api.Utilities; using Bit.Api.Utilities;
using Bit.Core.Domains; using Bit.Core.Domains;
using Newtonsoft.Json;
namespace Bit.Api.Models namespace Bit.Api.Models
{ {
@ -28,28 +29,22 @@ namespace Bit.Api.Models
[StringLength(5000)] [StringLength(5000)]
public string Notes { get; set; } public string Notes { get; set; }
public Site ToSite(string userId = null) public Cipher ToCipher(string userId = null)
{ {
return new Site return new Cipher
{ {
UserId = userId, UserId = new Guid(userId),
FolderId = string.IsNullOrWhiteSpace(FolderId) ? null : FolderId, FolderId = string.IsNullOrWhiteSpace(FolderId) ? null : (Guid?)new Guid(FolderId),
Name = Name, Data = JsonConvert.SerializeObject(new CipherDataModel(this), new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore }),
Uri = Uri, Type = Core.Enums.CipherType.Site
Username = string.IsNullOrWhiteSpace(Username) ? null : Username,
Password = Password,
Notes = string.IsNullOrWhiteSpace(Notes) ? null : Notes
}; };
} }
public Site ToSite(Site existingSite) public Cipher ToCipher(Cipher existingSite)
{ {
existingSite.FolderId = string.IsNullOrWhiteSpace(FolderId) ? null : FolderId; existingSite.FolderId = string.IsNullOrWhiteSpace(FolderId) ? null : (Guid?)new Guid(FolderId);
existingSite.Name = Name; existingSite.Data = JsonConvert.SerializeObject(new CipherDataModel(this), new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore });
existingSite.Uri = Uri; existingSite.Type = Core.Enums.CipherType.Site;
existingSite.Username = string.IsNullOrWhiteSpace(Username) ? null : Username;
existingSite.Password = Password;
existingSite.Notes = string.IsNullOrWhiteSpace(Notes) ? null : Notes;
return existingSite; return existingSite;
} }

View File

@ -1,21 +1,29 @@
using System; using System;
using Bit.Core.Domains; using Bit.Core.Domains;
using Newtonsoft.Json;
namespace Bit.Api.Models namespace Bit.Api.Models
{ {
public class FolderResponseModel : ResponseModel public class FolderResponseModel : ResponseModel
{ {
public FolderResponseModel(Folder folder) public FolderResponseModel(Cipher cipher)
: base("folder") : base("folder")
{ {
if(folder == null) if(cipher == null)
{ {
throw new ArgumentNullException(nameof(folder)); throw new ArgumentNullException(nameof(cipher));
} }
Id = folder.Id; if(cipher.Type != Core.Enums.CipherType.Folder)
Name = folder.Name; {
RevisionDate = folder.RevisionDate; throw new ArgumentException(nameof(cipher.Type));
}
var data = JsonConvert.DeserializeObject<CipherDataModel>(cipher.Data);
Id = cipher.Id.ToString();
Name = data.Name;
RevisionDate = cipher.RevisionDate;
} }
public string Id { get; set; } public string Id { get; set; }

View File

@ -13,7 +13,7 @@ namespace Bit.Api.Models
throw new ArgumentNullException(nameof(user)); throw new ArgumentNullException(nameof(user));
} }
Id = user.Id; Id = user.Id.ToString();
Name = user.Name; Name = user.Name;
Email = user.Email; Email = user.Email;
MasterPasswordHint = string.IsNullOrWhiteSpace(user.MasterPasswordHint) ? null : user.MasterPasswordHint; MasterPasswordHint = string.IsNullOrWhiteSpace(user.MasterPasswordHint) ? null : user.MasterPasswordHint;

View File

@ -1,26 +1,34 @@
using System; using System;
using Bit.Core.Domains; using Bit.Core.Domains;
using Newtonsoft.Json;
namespace Bit.Api.Models namespace Bit.Api.Models
{ {
public class SiteResponseModel : ResponseModel public class SiteResponseModel : ResponseModel
{ {
public SiteResponseModel(Site site) public SiteResponseModel(Cipher cipher)
: base("site") : base("site")
{ {
if(site == null) if(cipher == null)
{ {
throw new ArgumentNullException(nameof(site)); throw new ArgumentNullException(nameof(cipher));
} }
Id = site.Id; if(cipher.Type != Core.Enums.CipherType.Site)
FolderId = string.IsNullOrWhiteSpace(site.FolderId) ? null : site.FolderId; {
Name = site.Name; throw new ArgumentException(nameof(cipher.Type));
Uri = site.Uri; }
Username = site.Username;
Password = site.Password; var data = JsonConvert.DeserializeObject<CipherDataModel>(cipher.Data);
Notes = site.Notes;
RevisionDate = site.RevisionDate; Id = cipher.Id.ToString();
FolderId = cipher.FolderId?.ToString();
Name = data.Name;
Uri = data.Uri;
Username = data.Username;
Password = data.Password;
Notes = data.Notes;
RevisionDate = cipher.RevisionDate;
} }
public string Id { get; set; } public string Id { get; set; }

View File

@ -57,8 +57,6 @@ namespace Bit.Api
// Repositories // Repositories
services.AddSingleton<IUserRepository>(s => new Repos.UserRepository(globalSettings.SqlServer.ConnectionString)); services.AddSingleton<IUserRepository>(s => new Repos.UserRepository(globalSettings.SqlServer.ConnectionString));
services.AddSingleton<ISiteRepository>(s => new Repos.SiteRepository(globalSettings.SqlServer.ConnectionString));
services.AddSingleton<IFolderRepository>(s => new Repos.FolderRepository(globalSettings.SqlServer.ConnectionString));
services.AddSingleton<ICipherRepository>(s => new Repos.CipherRepository(globalSettings.SqlServer.ConnectionString)); services.AddSingleton<ICipherRepository>(s => new Repos.CipherRepository(globalSettings.SqlServer.ConnectionString));
// Context // Context

View File

@ -1,13 +1,21 @@
using System; using System;
using Bit.Core.Utilities;
namespace Bit.Core.Domains namespace Bit.Core.Domains
{ {
public abstract class Cipher : IDataObject public class Cipher : IDataObject<Guid>
{ {
public string Id { get; set; } public Guid Id { get; set; }
public string UserId { get; set; } public Guid UserId { get; set; }
public string Name { get; set; } public Guid? FolderId { get; set; }
public Enums.CipherType Type { get; set; }
public string Data { get; set; }
public DateTime CreationDate { get; internal set; } = DateTime.UtcNow; public DateTime CreationDate { get; internal set; } = DateTime.UtcNow;
public DateTime RevisionDate { get; internal set; } = DateTime.UtcNow; public DateTime RevisionDate { get; internal set; } = DateTime.UtcNow;
public void SetNewId()
{
Id = CoreHelpers.GenerateComb();
}
} }
} }

View File

@ -1,6 +0,0 @@
namespace Bit.Core.Domains
{
public class Folder : Cipher, IDataObject
{
}
}

View File

@ -1,13 +0,0 @@
using Bit.Core.Enums;
namespace Bit.Core.Domains
{
public class Site : Cipher, IDataObject
{
public string FolderId { get; set; }
public string Uri { get; set; }
public string Username { get; set; }
public string Password { get; set; }
public string Notes { get; set; }
}
}

View File

@ -1,11 +1,12 @@
using System; using System;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Utilities;
namespace Bit.Core.Domains namespace Bit.Core.Domains
{ {
public class User : IDataObject public class User : IDataObject<Guid>
{ {
public string Id { get; set; } public Guid Id { get; set; }
public string Name { get; set; } public string Name { get; set; }
public string Email { get; set; } public string Email { get; set; }
public bool EmailVerified { get; set; } public bool EmailVerified { get; set; }
@ -18,5 +19,10 @@ namespace Bit.Core.Domains
public string AuthenticatorKey { get; set; } public string AuthenticatorKey { get; set; }
public DateTime CreationDate { get; internal set; } = DateTime.UtcNow; public DateTime CreationDate { get; internal set; } = DateTime.UtcNow;
public DateTime RevisionDate { get; internal set; } = DateTime.UtcNow; public DateTime RevisionDate { get; internal set; } = DateTime.UtcNow;
public void SetNewId()
{
Id = CoreHelpers.GenerateComb();
}
} }
} }

View File

@ -1,6 +1,6 @@
namespace Bit.Core.Enums namespace Bit.Core.Enums
{ {
public enum CipherType public enum CipherType : short
{ {
Folder = 0, Folder = 0,
Site = 1 Site = 1

View File

@ -1,7 +1,10 @@
namespace Bit.Core using System;
namespace Bit.Core
{ {
public interface IDataObject public interface IDataObject<T> where T : IEquatable<T>
{ {
string Id { get; set; } T Id { get; set; }
void SetNewId();
} }
} }

View File

@ -26,7 +26,7 @@ namespace Bit.Core.Identity
var signInManager = context.HttpContext.RequestServices.GetRequiredService<JwtBearerSignInManager>(); var signInManager = context.HttpContext.RequestServices.GetRequiredService<JwtBearerSignInManager>();
var userId = userManager.GetUserId(context.Ticket.Principal); var userId = userManager.GetUserId(context.Ticket.Principal);
var user = await userRepository.GetByIdAsync(userId); var user = await userRepository.GetByIdAsync(new Guid(userId));
// validate security token // validate security token
if(!await signInManager.ValidateSecurityStampAsync(user, context.Ticket.Principal)) if(!await signInManager.ValidateSecurityStampAsync(user, context.Ticket.Principal))

View File

@ -55,12 +55,14 @@ namespace Bit.Core.Identity
public async Task<User> FindByIdAsync(string userId, CancellationToken cancellationToken = default(CancellationToken)) public async Task<User> FindByIdAsync(string userId, CancellationToken cancellationToken = default(CancellationToken))
{ {
if(_currentContext?.User != null && _currentContext.User.Id == userId) var id = new Guid(userId);
if(_currentContext?.User != null && _currentContext.User.Id == id)
{ {
return _currentContext.User; return _currentContext.User;
} }
return await _userRepository.GetByIdAsync(userId); return await _userRepository.GetByIdAsync(id);
} }
public async Task<User> FindByNameAsync(string normalizedUserName, CancellationToken cancellationToken = default(CancellationToken)) public async Task<User> FindByNameAsync(string normalizedUserName, CancellationToken cancellationToken = default(CancellationToken))
@ -100,7 +102,7 @@ namespace Bit.Core.Identity
public Task<string> GetUserIdAsync(User user, CancellationToken cancellationToken = default(CancellationToken)) public Task<string> GetUserIdAsync(User user, CancellationToken cancellationToken = default(CancellationToken))
{ {
return Task.FromResult(user.Id); return Task.FromResult(user.Id.ToString());
} }
public Task<string> GetUserNameAsync(User user, CancellationToken cancellationToken = default(CancellationToken)) public Task<string> GetUserNameAsync(User user, CancellationToken cancellationToken = default(CancellationToken))

View File

@ -1,12 +1,16 @@
using System.Collections.Generic; using System;
using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using Bit.Core.Domains; using Bit.Core.Domains;
namespace Bit.Core.Repositories namespace Bit.Core.Repositories
{ {
public interface ICipherRepository public interface ICipherRepository : IRepository<Cipher, Guid>
{ {
Task UpdateUserEmailPasswordAndCiphersAsync(User user, IEnumerable<dynamic> ciphers); Task<Cipher> GetByIdAsync(Guid id, Guid userId);
Task CreateAsync(IEnumerable<dynamic> ciphers); Task<ICollection<Cipher>> GetManyByUserIdAsync(Guid userId);
Task<ICollection<Cipher>> GetManyByTypeAndUserIdAsync(Enums.CipherType type, Guid userId);
Task UpdateUserEmailPasswordAndCiphersAsync(User user, IEnumerable<Cipher> ciphers);
Task CreateAsync(IEnumerable<Cipher> ciphers);
} }
} }

View File

@ -1,13 +0,0 @@
using System;
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);
}
}

View File

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

View File

@ -1,13 +0,0 @@
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);
}
}

View File

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

View File

@ -4,8 +4,6 @@ namespace Bit.Core.Repositories.SqlServer
{ {
public abstract class BaseRepository public abstract class BaseRepository
{ {
private static readonly long _baseDateTicks = new DateTime(1900, 1, 1).Ticks;
public BaseRepository(string connectionString) public BaseRepository(string connectionString)
{ {
if(string.IsNullOrWhiteSpace(connectionString)) if(string.IsNullOrWhiteSpace(connectionString))
@ -17,36 +15,5 @@ namespace Bit.Core.Repositories.SqlServer
} }
protected string ConnectionString { get; private set; } protected string ConnectionString { get; private set; }
/// <summary>
/// Generate sequential Guid for Sql Server.
/// ref: https://github.com/nhibernate/nhibernate-core/blob/master/src/NHibernate/Id/GuidCombGenerator.cs
/// </summary>
/// <returns>A comb Guid.</returns>
protected Guid GenerateComb()
{
var guidArray = Guid.NewGuid().ToByteArray();
var now = DateTime.UtcNow;
// Get the days and milliseconds which will be used to build the byte string
var days = new TimeSpan(now.Ticks - _baseDateTicks);
var msecs = now.TimeOfDay;
// Convert to a byte array
// Note that SQL Server is accurate to 1/300th of a millisecond so we divide by 3.333333
var daysArray = BitConverter.GetBytes(days.Days);
var msecsArray = BitConverter.GetBytes((long)(msecs.TotalMilliseconds / 3.333333));
// Reverse the bytes to match SQL Servers ordering
Array.Reverse(daysArray);
Array.Reverse(msecsArray);
// Copy the bytes into the guid
Array.Copy(daysArray, daysArray.Length - 2, guidArray, guidArray.Length - 6, 2);
Array.Copy(msecsArray, msecsArray.Length - 4, guidArray, guidArray.Length - 4, 4);
return new Guid(guidArray);
}
} }
} }

View File

@ -3,23 +3,63 @@ using System.Linq;
using System.Collections.Generic; using System.Collections.Generic;
using System.Data.SqlClient; using System.Data.SqlClient;
using System.Threading.Tasks; using System.Threading.Tasks;
using Bit.Core.Repositories.SqlServer.Models;
using DataTableProxy; using DataTableProxy;
using Bit.Core.Domains; using Bit.Core.Domains;
using System.Data; using System.Data;
using Dapper;
namespace Bit.Core.Repositories.SqlServer namespace Bit.Core.Repositories.SqlServer
{ {
public class CipherRepository : BaseRepository, ICipherRepository public class CipherRepository : Repository<Cipher, Guid>, ICipherRepository
{ {
public CipherRepository(string connectionString) public CipherRepository(string connectionString)
: base(connectionString) : base(connectionString)
{ } { }
public Task UpdateUserEmailPasswordAndCiphersAsync(User user, IEnumerable<dynamic> ciphers) public async Task<Cipher> GetByIdAsync(Guid id, Guid userId)
{ {
var cleanedCiphers = ciphers.Where(c => c is Cipher); var cipher = await GetByIdAsync(id);
if(cleanedCiphers.Count() == 0) if(cipher == null || cipher.UserId != userId)
{
return null;
}
return cipher;
}
public async Task<ICollection<Cipher>> GetManyByUserIdAsync(Guid userId)
{
using(var connection = new SqlConnection(ConnectionString))
{
var results = await connection.QueryAsync<Cipher>(
$"[{Schema}].[{Table}_ReadByUserId]",
new { UserId = userId },
commandType: CommandType.StoredProcedure);
return results.ToList();
}
}
public async Task<ICollection<Cipher>> GetManyByTypeAndUserIdAsync(Enums.CipherType type, Guid userId)
{
using(var connection = new SqlConnection(ConnectionString))
{
var results = await connection.QueryAsync<Cipher>(
$"[{Schema}].[{Table}_ReadByTypeUserId]",
new
{
Type = type,
UserId = userId
},
commandType: CommandType.StoredProcedure);
return results.ToList();
}
}
public Task UpdateUserEmailPasswordAndCiphersAsync(User user, IEnumerable<Cipher> ciphers)
{
if(ciphers.Count() == 0)
{ {
return Task.FromResult(0); return Task.FromResult(0);
} }
@ -37,7 +77,7 @@ namespace Bit.Core.Repositories.SqlServer
using(var cmd = new SqlCommand("[dbo].[User_UpdateEmailPassword]", connection, transaction)) using(var cmd = new SqlCommand("[dbo].[User_UpdateEmailPassword]", connection, transaction))
{ {
cmd.CommandType = CommandType.StoredProcedure; cmd.CommandType = CommandType.StoredProcedure;
cmd.Parameters.Add("@Id", SqlDbType.UniqueIdentifier).Value = new Guid(user.Id); cmd.Parameters.Add("@Id", SqlDbType.UniqueIdentifier).Value = user.Id;
cmd.Parameters.Add("@Email", SqlDbType.NVarChar).Value = user.Email; cmd.Parameters.Add("@Email", SqlDbType.NVarChar).Value = user.Email;
cmd.Parameters.Add("@EmailVerified", SqlDbType.NVarChar).Value = user.EmailVerified; cmd.Parameters.Add("@EmailVerified", SqlDbType.NVarChar).Value = user.EmailVerified;
cmd.Parameters.Add("@MasterPassword", SqlDbType.NVarChar).Value = user.MasterPassword; cmd.Parameters.Add("@MasterPassword", SqlDbType.NVarChar).Value = user.MasterPassword;
@ -50,12 +90,8 @@ namespace Bit.Core.Repositories.SqlServer
var sqlCreateTemp = @" var sqlCreateTemp = @"
SELECT TOP 0 * SELECT TOP 0 *
INTO #TempFolder INTO #TempCipher
FROM [dbo].[Folder] FROM [dbo].[Cipher]";
SELECT TOP 0 *
INTO #TempSite
FROM [dbo].[Site]";
using(var cmd = new SqlCommand(sqlCreateTemp, connection, transaction)) using(var cmd = new SqlCommand(sqlCreateTemp, connection, transaction))
{ {
@ -66,25 +102,9 @@ namespace Bit.Core.Repositories.SqlServer
using(var bulkCopy = new SqlBulkCopy(connection, SqlBulkCopyOptions.KeepIdentity, transaction)) using(var bulkCopy = new SqlBulkCopy(connection, SqlBulkCopyOptions.KeepIdentity, transaction))
{ {
bulkCopy.DestinationTableName = "#TempFolder"; bulkCopy.DestinationTableName = "#TempCipher";
var dataTable = cleanedCiphers
.Where(c => c is Folder)
.Select(c => new FolderTableModel(c as Folder))
.ToTable(new ClassMapping<FolderTableModel>().AddAllPropertiesAsColumns());
bulkCopy.WriteToServer(dataTable);
}
using(var bulkCopy = new SqlBulkCopy(connection, SqlBulkCopyOptions.KeepIdentity, transaction))
{
bulkCopy.DestinationTableName = "#TempSite";
var dataTable = cleanedCiphers
.Where(c => c is Site)
.Select(c => new SiteTableModel(c as Site))
.ToTable(new ClassMapping<SiteTableModel>().AddAllPropertiesAsColumns());
var dataTable = ciphers.ToTable(new ClassMapping<Cipher>().AddAllPropertiesAsColumns());
bulkCopy.WriteToServer(dataTable); bulkCopy.WriteToServer(dataTable);
} }
@ -92,44 +112,26 @@ namespace Bit.Core.Repositories.SqlServer
var sqlUpdate = @" var sqlUpdate = @"
UPDATE UPDATE
[dbo].[Folder] [dbo].[Cipher]
SET
-- Do not update [UserId]
[Name] = TF.[Name],
-- Do not update [CreationDate]
[RevisionDate] = TF.[RevisionDate]
FROM
[dbo].[Folder] F
INNER JOIN
#TempFolder TF ON F.Id = TF.Id
WHERE
F.[UserId] = @UserId
UPDATE
[dbo].[Site]
SET SET
-- Do not update [UserId] -- Do not update [UserId]
-- Do not update [FolderId] -- Do not update [FolderId]
[Name] = TS.[Name], -- Do not update [Type]
[Uri] = TS.[Uri], [Data] = TC.[Data],
[Username] = TS.[Username],
[Password] = TS.[Password],
[Notes] = TS.[Notes],
-- Do not update [CreationDate] -- Do not update [CreationDate]
[RevisionDate] = TS.[RevisionDate] [RevisionDate] = TC.[RevisionDate]
FROM FROM
[dbo].[Site] S [dbo].[Cipher] C
INNER JOIN INNER JOIN
#TempSite TS ON S.Id = TS.Id #TempCipher TC ON C.Id = TC.Id
WHERE WHERE
S.[UserId] = @UserId C.[UserId] = @UserId
DROP TABLE #TempFolder DROP TABLE #TempCipher";
DROP TABLE #TempSite";
using(var cmd = new SqlCommand(sqlUpdate, connection, transaction)) using(var cmd = new SqlCommand(sqlUpdate, connection, transaction))
{ {
cmd.Parameters.Add("@UserId", SqlDbType.UniqueIdentifier).Value = new Guid(user.Id); cmd.Parameters.Add("@UserId", SqlDbType.UniqueIdentifier).Value = user.Id;
cmd.ExecuteNonQuery(); cmd.ExecuteNonQuery();
} }
@ -146,18 +148,17 @@ namespace Bit.Core.Repositories.SqlServer
return Task.FromResult(0); return Task.FromResult(0);
} }
public Task CreateAsync(IEnumerable<dynamic> ciphers) public Task CreateAsync(IEnumerable<Cipher> ciphers)
{ {
var cleanedCiphers = ciphers.Where(c => c is Cipher); if(ciphers.Count() == 0)
if(cleanedCiphers.Count() == 0)
{ {
return Task.FromResult(0); return Task.FromResult(0);
} }
// Generate new Ids for these new ciphers // Generate new Ids for these new ciphers
foreach(var cipher in cleanedCiphers) foreach(var cipher in ciphers)
{ {
cipher.Id = GenerateComb().ToString(); cipher.SetNewId();
} }
using(var connection = new SqlConnection(ConnectionString)) using(var connection = new SqlConnection(ConnectionString))
@ -168,27 +169,10 @@ namespace Bit.Core.Repositories.SqlServer
{ {
try try
{ {
using(var bulkCopy = new SqlBulkCopy(connection, SqlBulkCopyOptions.KeepIdentity, transaction)) using(var bulkCopy = new SqlBulkCopy(connection, SqlBulkCopyOptions.KeepIdentity | SqlBulkCopyOptions.FireTriggers, transaction))
{ {
bulkCopy.DestinationTableName = "[dbo].[Folder]"; bulkCopy.DestinationTableName = "[dbo].[Cipher]";
var dataTable = ciphers.ToTable(new ClassMapping<Cipher>().AddAllPropertiesAsColumns());
var dataTable = cleanedCiphers
.Where(c => c is Folder)
.Select(c => new FolderTableModel(c as Folder))
.ToTable(new ClassMapping<FolderTableModel>().AddAllPropertiesAsColumns());
bulkCopy.WriteToServer(dataTable);
}
using(var bulkCopy = new SqlBulkCopy(connection, SqlBulkCopyOptions.KeepIdentity, transaction))
{
bulkCopy.DestinationTableName = "[dbo].[Site]";
var dataTable = cleanedCiphers
.Where(c => c is Site)
.Select(c => new SiteTableModel(c as Site))
.ToTable(new ClassMapping<SiteTableModel>().AddAllPropertiesAsColumns());
bulkCopy.WriteToServer(dataTable); bulkCopy.WriteToServer(dataTable);
} }

View File

@ -1,43 +0,0 @@
using System;
using System.Collections.Generic;
using System.Data;
using System.Data.SqlClient;
using System.Linq;
using System.Threading.Tasks;
using Bit.Core.Domains;
using Bit.Core.Repositories.SqlServer.Models;
using Dapper;
namespace Bit.Core.Repositories.SqlServer
{
public class FolderRepository : Repository<Folder, FolderTableModel>, IFolderRepository
{
public FolderRepository(string connectionString)
: base(connectionString)
{ }
public async Task<Folder> GetByIdAsync(string id, string userId)
{
var folder = await GetByIdAsync(id);
if(folder == null || folder.UserId != userId)
{
return null;
}
return folder;
}
public async Task<ICollection<Folder>> GetManyByUserIdAsync(string userId)
{
using(var connection = new SqlConnection(ConnectionString))
{
var results = await connection.QueryAsync<FolderTableModel>(
$"[{Schema}].[{Table}_ReadByUserId]",
new { UserId = new Guid(userId) },
commandType: CommandType.StoredProcedure);
return results.Select(f => f.ToDomain()).ToList();
}
}
}
}

View File

@ -1,37 +0,0 @@
using System;
using Bit.Core.Domains;
namespace Bit.Core.Repositories.SqlServer.Models
{
public class FolderTableModel : ITableModel<Folder>
{
public FolderTableModel() { }
public FolderTableModel(Folder folder)
{
Id = new Guid(folder.Id);
UserId = new Guid(folder.UserId);
Name = folder.Name;
CreationDate = folder.CreationDate;
RevisionDate = folder.RevisionDate;
}
public Guid Id { get; set; }
public Guid UserId { get; set; }
public string Name { get; set; }
public DateTime CreationDate { get; set; }
public DateTime RevisionDate { get; set; }
public Folder ToDomain()
{
return new Folder
{
Id = Id.ToString(),
UserId = UserId.ToString(),
Name = Name,
CreationDate = CreationDate,
RevisionDate = RevisionDate
};
}
}
}

View File

@ -1,7 +0,0 @@
namespace Bit.Core.Repositories.SqlServer.Models
{
public interface ITableModel<T>
{
T ToDomain();
}
}

View File

@ -1,52 +0,0 @@
using System;
using Bit.Core.Domains;
namespace Bit.Core.Repositories.SqlServer.Models
{
public class SiteTableModel : ITableModel<Site>
{
public SiteTableModel() { }
public SiteTableModel(Site site)
{
Id = new Guid(site.Id);
UserId = new Guid(site.UserId);
FolderId = string.IsNullOrWhiteSpace(site.FolderId) ? (Guid?)null : new Guid(site.FolderId);
Name = site.Name;
Uri = site.Uri;
Username = site.Username;
Password = site.Password;
Notes = site.Notes;
CreationDate = site.CreationDate;
RevisionDate = site.RevisionDate;
}
public Guid Id { get; set; }
public Guid UserId { get; set; }
public Guid? FolderId { get; set; }
public string Name { get; set; }
public string Uri { get; set; }
public string Username { get; set; }
public string Password { get; set; }
public string Notes { get; set; }
public DateTime CreationDate { get; set; }
public DateTime RevisionDate { get; set; }
public Site ToDomain()
{
return new Site
{
Id = Id.ToString(),
UserId = UserId.ToString(),
FolderId = FolderId.ToString(),
Name = Name,
Uri = Uri,
Username = Username,
Password = Password,
Notes = Notes,
CreationDate = CreationDate,
RevisionDate = RevisionDate
};
}
}
}

View File

@ -1,62 +0,0 @@
using System;
using Bit.Core.Domains;
using Bit.Core.Enums;
namespace Bit.Core.Repositories.SqlServer.Models
{
public class UserTableModel : ITableModel<User>
{
public UserTableModel() { }
public UserTableModel(User user)
{
Id = new Guid(user.Id);
Name = user.Name;
Email = user.Email;
EmailVerified = user.EmailVerified;
MasterPassword = user.MasterPassword;
MasterPasswordHint = user.MasterPasswordHint;
Culture = user.Culture;
SecurityStamp = user.SecurityStamp;
TwoFactorEnabled = user.TwoFactorEnabled;
TwoFactorProvider = user.TwoFactorProvider;
AuthenticatorKey = user.AuthenticatorKey;
CreationDate = user.CreationDate;
RevisionDate = user.RevisionDate;
}
public Guid Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
public bool EmailVerified { get; set; }
public string MasterPassword { get; set; }
public string MasterPasswordHint { get; set; }
public string Culture { get; set; }
public string SecurityStamp { get; set; }
public bool TwoFactorEnabled { get; set; }
public TwoFactorProvider? TwoFactorProvider { get; set; }
public string AuthenticatorKey { get; set; }
public DateTime CreationDate { get; set; }
public DateTime RevisionDate { get; set; }
public User ToDomain()
{
return new User
{
Id = Id.ToString(),
Name = Name,
Email = Email,
EmailVerified = EmailVerified,
MasterPassword = MasterPassword,
MasterPasswordHint = MasterPasswordHint,
Culture = Culture,
SecurityStamp = SecurityStamp,
TwoFactorEnabled = TwoFactorEnabled,
TwoFactorProvider = TwoFactorProvider,
AuthenticatorKey = AuthenticatorKey,
CreationDate = CreationDate,
RevisionDate = RevisionDate
};
}
}
}

View File

@ -3,12 +3,11 @@ using System.Data;
using System.Data.SqlClient; using System.Data.SqlClient;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Bit.Core.Repositories.SqlServer.Models;
using Dapper; using Dapper;
namespace Bit.Core.Repositories.SqlServer namespace Bit.Core.Repositories.SqlServer
{ {
public abstract class Repository<T, TModel> : BaseRepository, IRepository<T> where T : IDataObject where TModel : ITableModel<T> public abstract class Repository<T, TId> : BaseRepository, IRepository<T, TId> where TId : IEquatable<TId> where T : class, IDataObject<TId>
{ {
public Repository(string connectionString, string schema = null, string table = null) public Repository(string connectionString, string schema = null, string table = null)
: base(connectionString) : base(connectionString)
@ -27,55 +26,45 @@ namespace Bit.Core.Repositories.SqlServer
protected string Schema { get; private set; } = "dbo"; protected string Schema { get; private set; } = "dbo";
protected string Table { get; private set; } = typeof(T).Name; protected string Table { get; private set; } = typeof(T).Name;
public virtual async Task<T> GetByIdAsync(string id) public virtual async Task<T> GetByIdAsync(TId id)
{ {
using(var connection = new SqlConnection(ConnectionString)) using(var connection = new SqlConnection(ConnectionString))
{ {
var results = await connection.QueryAsync<TModel>( var results = await connection.QueryAsync<T>(
$"[{Schema}].[{Table}_ReadById]", $"[{Schema}].[{Table}_ReadById]",
new { Id = new Guid(id) }, new { Id = id },
commandType: CommandType.StoredProcedure); commandType: CommandType.StoredProcedure);
var model = results.FirstOrDefault(); return results.SingleOrDefault();
if(model == null)
{
return default(T);
}
return model.ToDomain();
} }
} }
public virtual async Task CreateAsync(T obj) public virtual async Task CreateAsync(T obj)
{ {
obj.Id = GenerateComb().ToString(); obj.SetNewId();
var tableModel = (TModel)Activator.CreateInstance(typeof(TModel), obj);
using(var connection = new SqlConnection(ConnectionString)) using(var connection = new SqlConnection(ConnectionString))
{ {
var results = await connection.ExecuteAsync( var results = await connection.ExecuteAsync(
$"[{Schema}].[{Table}_Create]", $"[{Schema}].[{Table}_Create]",
tableModel, obj,
commandType: CommandType.StoredProcedure); commandType: CommandType.StoredProcedure);
} }
} }
public virtual async Task ReplaceAsync(T obj) public virtual async Task ReplaceAsync(T obj)
{ {
var tableModel = (TModel)Activator.CreateInstance(typeof(TModel), obj);
using(var connection = new SqlConnection(ConnectionString)) using(var connection = new SqlConnection(ConnectionString))
{ {
var results = await connection.ExecuteAsync( var results = await connection.ExecuteAsync(
$"[{Schema}].[{Table}_Update]", $"[{Schema}].[{Table}_Update]",
tableModel, obj,
commandType: CommandType.StoredProcedure); commandType: CommandType.StoredProcedure);
} }
} }
public virtual async Task UpsertAsync(T obj) public virtual async Task UpsertAsync(T obj)
{ {
if(string.IsNullOrWhiteSpace(obj.Id) || obj.Id == "0" || obj.Id == Guid.Empty.ToString()) if(obj.Id.Equals(default(TId)))
{ {
await CreateAsync(obj); await CreateAsync(obj);
} }
@ -90,13 +79,13 @@ namespace Bit.Core.Repositories.SqlServer
await DeleteByIdAsync(obj.Id); await DeleteByIdAsync(obj.Id);
} }
public virtual async Task DeleteByIdAsync(string id) public virtual async Task DeleteByIdAsync(TId id)
{ {
using(var connection = new SqlConnection(ConnectionString)) using(var connection = new SqlConnection(ConnectionString))
{ {
await connection.ExecuteAsync( await connection.ExecuteAsync(
$"[{Schema}].[{Table}_DeleteById]", $"[{Schema}].[{Table}_DeleteById]",
new { Id = new Guid(id) }, new { Id = id },
commandType: CommandType.StoredProcedure); commandType: CommandType.StoredProcedure);
} }
} }

View File

@ -1,43 +0,0 @@
using System;
using System.Collections.Generic;
using System.Data;
using System.Data.SqlClient;
using System.Linq;
using System.Threading.Tasks;
using Bit.Core.Domains;
using Bit.Core.Repositories.SqlServer.Models;
using Dapper;
namespace Bit.Core.Repositories.SqlServer
{
public class SiteRepository : Repository<Site, SiteTableModel>, ISiteRepository
{
public SiteRepository(string connectionString)
: base(connectionString)
{ }
public async Task<Site> GetByIdAsync(string id, string userId)
{
var site = await GetByIdAsync(id);
if(site == null || site.UserId != userId)
{
return null;
}
return site;
}
public async Task<ICollection<Site>> GetManyByUserIdAsync(string userId)
{
using(var connection = new SqlConnection(ConnectionString))
{
var results = await connection.QueryAsync<SiteTableModel>(
$"[{Schema}].[{Table}_ReadByUserId]",
new { UserId = new Guid(userId) },
commandType: CommandType.StoredProcedure);
return results.Select(s => s.ToDomain()).ToList();
}
}
}
}

View File

@ -1,14 +1,14 @@
using System.Data; using System;
using System.Data;
using System.Data.SqlClient; using System.Data.SqlClient;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Bit.Core.Domains; using Bit.Core.Domains;
using Bit.Core.Repositories.SqlServer.Models;
using Dapper; using Dapper;
namespace Bit.Core.Repositories.SqlServer namespace Bit.Core.Repositories.SqlServer
{ {
public class UserRepository : Repository<User, UserTableModel>, IUserRepository public class UserRepository : Repository<User, Guid>, IUserRepository
{ {
public UserRepository(string connectionString) public UserRepository(string connectionString)
: base(connectionString) : base(connectionString)
@ -18,18 +18,12 @@ namespace Bit.Core.Repositories.SqlServer
{ {
using(var connection = new SqlConnection(ConnectionString)) using(var connection = new SqlConnection(ConnectionString))
{ {
var results = await connection.QueryAsync<UserTableModel>( var results = await connection.QueryAsync<User>(
$"[{Schema}].[{Table}_ReadByEmail]", $"[{Schema}].[{Table}_ReadByEmail]",
new { Email = email }, new { Email = email },
commandType: CommandType.StoredProcedure); commandType: CommandType.StoredProcedure);
var model = results.FirstOrDefault(); return results.SingleOrDefault();
if(model == null)
{
return null;
}
return model.ToDomain();
} }
} }
} }

View File

@ -9,46 +9,43 @@ namespace Bit.Core.Services
{ {
public class CipherService : ICipherService public class CipherService : ICipherService
{ {
private readonly IFolderRepository _folderRepository;
private readonly ICipherRepository _cipherRepository; private readonly ICipherRepository _cipherRepository;
public CipherService( public CipherService(
IFolderRepository folderRepository,
ICipherRepository cipherRepository) ICipherRepository cipherRepository)
{ {
_folderRepository = folderRepository;
_cipherRepository = cipherRepository; _cipherRepository = cipherRepository;
} }
public async Task ImportCiphersAsync( public async Task ImportCiphersAsync(
List<Folder> folders, List<Cipher> folders,
List<Site> sites, List<Cipher> ciphers,
IEnumerable<KeyValuePair<int, int>> siteRelationships) IEnumerable<KeyValuePair<int, int>> folderRelationships)
{ {
// create all the folders // create all the folders
var folderTasks = new List<Task>(); var folderTasks = new List<Task>();
foreach(var folder in folders) foreach(var folder in folders)
{ {
folderTasks.Add(_folderRepository.CreateAsync(folder)); folderTasks.Add(_cipherRepository.CreateAsync(folder));
} }
await Task.WhenAll(folderTasks); await Task.WhenAll(folderTasks);
// associate the newly created folders to the sites // associate the newly created folders to the ciphers
foreach(var relationship in siteRelationships) foreach(var relationship in folderRelationships)
{ {
var site = sites.ElementAtOrDefault(relationship.Key); var cipher = ciphers.ElementAtOrDefault(relationship.Key);
var folder = folders.ElementAtOrDefault(relationship.Value); var folder = folders.ElementAtOrDefault(relationship.Value);
if(site == null || folder == null) if(cipher == null || folder == null)
{ {
continue; continue;
} }
site.FolderId = folder.Id; cipher.FolderId = folder.Id;
} }
// create all the sites // create all the ciphers
await _cipherRepository.CreateAsync(sites); await _cipherRepository.CreateAsync(ciphers);
} }
} }
} }

View File

@ -6,6 +6,6 @@ namespace Bit.Core.Services
{ {
public interface ICipherService public interface ICipherService
{ {
Task ImportCiphersAsync(List<Folder> folders, List<Site> sites, IEnumerable<KeyValuePair<int, int>> siteRelationships); Task ImportCiphersAsync(List<Cipher> folders, List<Cipher> ciphers, IEnumerable<KeyValuePair<int, int>> folderRelationships);
} }
} }

View File

@ -8,13 +8,13 @@ namespace Bit.Core.Services
{ {
public interface IUserService public interface IUserService
{ {
Task<User> GetUserByIdAsync(string userId); Task<User> GetUserByIdAsync(Guid userId);
Task SaveUserAsync(User user); Task SaveUserAsync(User user);
Task<IdentityResult> RegisterUserAsync(User user, string masterPassword); Task<IdentityResult> RegisterUserAsync(User user, string masterPassword);
Task SendMasterPasswordHintAsync(string email); Task SendMasterPasswordHintAsync(string email);
Task InitiateEmailChangeAsync(User user, string newEmail); Task InitiateEmailChangeAsync(User user, string newEmail);
Task<IdentityResult> ChangeEmailAsync(User user, string masterPassword, string newEmail, string newMasterPassword, string token, IEnumerable<dynamic> ciphers); Task<IdentityResult> ChangeEmailAsync(User user, string masterPassword, string newEmail, string newMasterPassword, string token, IEnumerable<Cipher> ciphers);
Task<IdentityResult> ChangePasswordAsync(User user, string currentMasterPasswordHash, string newMasterPasswordHash, IEnumerable<dynamic> ciphers); Task<IdentityResult> ChangePasswordAsync(User user, string currentMasterPasswordHash, string newMasterPasswordHash, IEnumerable<Cipher> ciphers);
Task<IdentityResult> RefreshSecurityStampAsync(User user, string masterPasswordHash); Task<IdentityResult> RefreshSecurityStampAsync(User user, string masterPasswordHash);
Task GetTwoFactorAsync(User user, Enums.TwoFactorProvider provider); Task GetTwoFactorAsync(User user, Enums.TwoFactorProvider provider);
Task<IdentityResult> DeleteAsync(User user); Task<IdentityResult> DeleteAsync(User user);

View File

@ -1,7 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
@ -57,14 +56,14 @@ namespace Bit.Core.Services
_passwordValidators = passwordValidators; _passwordValidators = passwordValidators;
} }
public async Task<User> GetUserByIdAsync(string userId) public async Task<User> GetUserByIdAsync(Guid userId)
{ {
return await _userRepository.GetByIdAsync(userId); return await _userRepository.GetByIdAsync(userId);
} }
public async Task SaveUserAsync(User user) public async Task SaveUserAsync(User user)
{ {
if(string.IsNullOrWhiteSpace(user.Id)) if(user.Id == default(Guid))
{ {
throw new ApplicationException("Use register method to create a new user."); throw new ApplicationException("Use register method to create a new user.");
} }
@ -114,7 +113,7 @@ namespace Bit.Core.Services
await _mailService.SendChangeEmailEmailAsync(newEmail, token); await _mailService.SendChangeEmailEmailAsync(newEmail, token);
} }
public async Task<IdentityResult> ChangeEmailAsync(User user, string masterPassword, string newEmail, string newMasterPassword, string token, IEnumerable<dynamic> ciphers) public async Task<IdentityResult> ChangeEmailAsync(User user, string masterPassword, string newEmail, string newMasterPassword, string token, IEnumerable<Cipher> ciphers)
{ {
var verifyPasswordResult = _passwordHasher.VerifyHashedPassword(user, user.MasterPassword, masterPassword); var verifyPasswordResult = _passwordHasher.VerifyHashedPassword(user, user.MasterPassword, masterPassword);
if(verifyPasswordResult == PasswordVerificationResult.Failed) if(verifyPasswordResult == PasswordVerificationResult.Failed)
@ -151,7 +150,7 @@ namespace Bit.Core.Services
throw new NotImplementedException(); throw new NotImplementedException();
} }
public async Task<IdentityResult> ChangePasswordAsync(User user, string masterPassword, string newMasterPassword, IEnumerable<dynamic> ciphers) public async Task<IdentityResult> ChangePasswordAsync(User user, string masterPassword, string newMasterPassword, IEnumerable<Cipher> ciphers)
{ {
if(user == null) if(user == null)
{ {

View File

@ -0,0 +1,40 @@
using System;
namespace Bit.Core.Utilities
{
public static class CoreHelpers
{
private static readonly long _baseDateTicks = new DateTime(1900, 1, 1).Ticks;
/// <summary>
/// Generate sequential Guid for Sql Server.
/// ref: https://github.com/nhibernate/nhibernate-core/blob/master/src/NHibernate/Id/GuidCombGenerator.cs
/// </summary>
/// <returns>A comb Guid.</returns>
public static Guid GenerateComb()
{
var guidArray = Guid.NewGuid().ToByteArray();
var now = DateTime.UtcNow;
// Get the days and milliseconds which will be used to build the byte string
var days = new TimeSpan(now.Ticks - _baseDateTicks);
var msecs = now.TimeOfDay;
// Convert to a byte array
// Note that SQL Server is accurate to 1/300th of a millisecond so we divide by 3.333333
var daysArray = BitConverter.GetBytes(days.Days);
var msecsArray = BitConverter.GetBytes((long)(msecs.TotalMilliseconds / 3.333333));
// Reverse the bytes to match SQL Servers ordering
Array.Reverse(daysArray);
Array.Reverse(msecsArray);
// Copy the bytes into the guid
Array.Copy(daysArray, daysArray.Length - 2, guidArray, guidArray.Length - 6, 2);
Array.Copy(msecsArray, msecsArray.Length - 4, guidArray, guidArray.Length - 4, 4);
return new Guid(guidArray);
}
}
}

View File

@ -76,7 +76,7 @@
<Build Include="dbo\Stored Procedures\Cipher_ReadById.sql" /> <Build Include="dbo\Stored Procedures\Cipher_ReadById.sql" />
<Build Include="dbo\Stored Procedures\Cipher_Update.sql" /> <Build Include="dbo\Stored Procedures\Cipher_Update.sql" />
<Build Include="dbo\Stored Procedures\History_ReadById.sql" /> <Build Include="dbo\Stored Procedures\History_ReadById.sql" />
<Build Include="dbo\Stored Procedures\History_Create.sql" /> <Build Include="dbo\Stored Procedures\Cipher_ReadByTypeUserId.sql" />
<Build Include="dbo\Stored Procedures\User_ReadById.sql" /> <Build Include="dbo\Stored Procedures\User_ReadById.sql" />
<Build Include="dbo\Stored Procedures\User_ReadByEmail.sql" /> <Build Include="dbo\Stored Procedures\User_ReadByEmail.sql" />
<Build Include="dbo\Stored Procedures\User_DeleteById.sql" /> <Build Include="dbo\Stored Procedures\User_DeleteById.sql" />

View File

@ -8,6 +8,8 @@
@RevisionDate DATETIME2(7) @RevisionDate DATETIME2(7)
AS AS
BEGIN BEGIN
SET NOCOUNT ON
INSERT INTO [dbo].[Cipher] INSERT INTO [dbo].[Cipher]
( (
[Id], [Id],

View File

@ -2,6 +2,8 @@
@Id UNIQUEIDENTIFIER @Id UNIQUEIDENTIFIER
AS AS
BEGIN BEGIN
SET NOCOUNT ON
BEGIN TRANSACTION Cipher_DeleteById BEGIN TRANSACTION Cipher_DeleteById
UPDATE UPDATE

View File

@ -2,6 +2,8 @@
@Id UNIQUEIDENTIFIER @Id UNIQUEIDENTIFIER
AS AS
BEGIN BEGIN
SET NOCOUNT ON
SELECT SELECT
* *
FROM FROM

View File

@ -0,0 +1,15 @@
CREATE PROCEDURE [dbo].[Cipher_ReadByTypeUserId]
@Type TINYINT,
@UserId UNIQUEIDENTIFIER
AS
BEGIN
SET NOCOUNT ON
SELECT
*
FROM
[dbo].[CipherView]
WHERE
[Type] = @Type
AND [UserId] = @UserId
END

View File

@ -2,6 +2,8 @@
@UserId UNIQUEIDENTIFIER @UserId UNIQUEIDENTIFIER
AS AS
BEGIN BEGIN
SET NOCOUNT ON
SELECT SELECT
* *
FROM FROM

View File

@ -8,6 +8,8 @@
@RevisionDate DATETIME2(7) @RevisionDate DATETIME2(7)
AS AS
BEGIN BEGIN
SET NOCOUNT ON
UPDATE UPDATE
[dbo].[Cipher] [dbo].[Cipher]
SET SET

View File

@ -1,22 +0,0 @@
CREATE PROCEDURE [dbo].[History_Create]
@UserId UNIQUEIDENTIFIER,
@CipherId UNIQUEIDENTIFIER,
@Event TINYINT,
@Date DATETIME2(7)
AS
BEGIN
INSERT INTO [dbo].[History]
(
[UserId],
[CipherId],
[Event],
[Date]
)
VALUES
(
@UserId,
@CipherId,
@Event,
@Date
)
END

View File

@ -2,6 +2,8 @@
@Id BIGINT @Id BIGINT
AS AS
BEGIN BEGIN
SET NOCOUNT ON
SELECT SELECT
* *
FROM FROM

View File

@ -14,6 +14,8 @@
@RevisionDate DATETIME2(7) @RevisionDate DATETIME2(7)
AS AS
BEGIN BEGIN
SET NOCOUNT ON
INSERT INTO [dbo].[User] INSERT INTO [dbo].[User]
( (
[Id], [Id],

View File

@ -2,6 +2,8 @@
@Id UNIQUEIDENTIFIER @Id UNIQUEIDENTIFIER
AS AS
BEGIN BEGIN
SET NOCOUNT ON
BEGIN TRANSACTION User_DeleteById BEGIN TRANSACTION User_DeleteById
DELETE DELETE

View File

@ -2,6 +2,8 @@
@Email NVARCHAR(50) @Email NVARCHAR(50)
AS AS
BEGIN BEGIN
SET NOCOUNT ON
SELECT SELECT
* *
FROM FROM

View File

@ -2,6 +2,8 @@
@Id UNIQUEIDENTIFIER @Id UNIQUEIDENTIFIER
AS AS
BEGIN BEGIN
SET NOCOUNT ON
SELECT SELECT
* *
FROM FROM

View File

@ -14,6 +14,8 @@
@RevisionDate DATETIME2(7) @RevisionDate DATETIME2(7)
AS AS
BEGIN BEGIN
SET NOCOUNT ON
UPDATE UPDATE
[dbo].[User] [dbo].[User]
SET SET

View File

@ -7,6 +7,8 @@
@RevisionDate DATETIME2(7) @RevisionDate DATETIME2(7)
AS AS
BEGIN BEGIN
SET NOCOUNT ON
UPDATE UPDATE
[dbo].[User] [dbo].[User]
SET SET

View File

@ -22,40 +22,75 @@ CREATE TRIGGER [dbo].[Cipher_Inserted]
ON [dbo].[Cipher] AFTER INSERT ON [dbo].[Cipher] AFTER INSERT
AS AS
BEGIN BEGIN
SET NOCOUNT ON DECLARE @Count INT = (SELECT COUNT(1) FROM INSERTED)
IF @Count = 0 RETURN
DECLARE @UserId UNIQUEIDENTIFIER = (SELECT [UserId] FROM INSERTED)
DECLARE @CipherId UNIQUEIDENTIFIER = (SELECT [Id] FROM INSERTED)
DECLARE @Date DATETIME2(7) = (SELECT [CreationDate] FROM INSERTED)
EXEC [dbo].[History_Create] @UserId, @CipherId, 0 /* Insert */, @Date
END
SET NOCOUNT ON
INSERT INTO [dbo].[History]
(
[UserId],
[CipherId],
[Event],
[Date]
)
SELECT
[UserId],
[Id],
0, --Insert
[CreationDate]
FROM
INSERTED
END
GO GO
CREATE TRIGGER [dbo].[Cipher_Updated] CREATE TRIGGER [dbo].[Cipher_Updated]
ON [dbo].[Cipher] AFTER UPDATE ON [dbo].[Cipher] AFTER UPDATE
AS AS
BEGIN BEGIN
SET NOCOUNT ON DECLARE @Count INT = (SELECT COUNT(1) FROM INSERTED)
IF @Count = 0 RETURN
DECLARE @UserId UNIQUEIDENTIFIER = (SELECT [UserId] FROM INSERTED)
DECLARE @CipherId UNIQUEIDENTIFIER = (SELECT [Id] FROM INSERTED)
DECLARE @Date DATETIME2(7) = (SELECT [RevisionDate] FROM INSERTED)
EXEC [dbo].[History_Create] @UserId, @CipherId, 1 /* Update */, @Date
END
SET NOCOUNT ON
INSERT INTO [dbo].[History]
(
[UserId],
[CipherId],
[Event],
[Date]
)
SELECT
[UserId],
[Id],
1, --Update
[RevisionDate]
FROM
INSERTED
END
GO GO
CREATE TRIGGER [dbo].[Cipher_Deleted] CREATE TRIGGER [dbo].[Cipher_Deleted]
ON [dbo].[Cipher] AFTER DELETE ON [dbo].[Cipher] AFTER DELETE
AS AS
BEGIN BEGIN
DECLARE @Count INT = (SELECT COUNT(1) FROM DELETED)
IF @Count = 0 RETURN
SET NOCOUNT ON SET NOCOUNT ON
DECLARE @UserId UNIQUEIDENTIFIER = (SELECT [UserId] FROM DELETED) INSERT INTO [dbo].[History]
DECLARE @CipherId UNIQUEIDENTIFIER = (SELECT [Id] FROM DELETED) (
[UserId],
EXEC [dbo].[History_Create] @UserId, @CipherId, 2 /* Delete */, GETUTCDATE [CipherId],
[Event],
[Date]
)
SELECT
[UserId],
[Id],
2, --Delete
GETUTCDATE()
FROM
DELETED
END END

View File

@ -4,8 +4,6 @@
[CipherId] UNIQUEIDENTIFIER NOT NULL, [CipherId] UNIQUEIDENTIFIER NOT NULL,
[Event] TINYINT NOT NULL, [Event] TINYINT NOT NULL,
[Date] DATETIME2 (7) NOT NULL, [Date] DATETIME2 (7) NOT NULL,
CONSTRAINT [PK_CipherHistory] PRIMARY KEY CLUSTERED ([Id] ASC), CONSTRAINT [PK_CipherHistory] PRIMARY KEY CLUSTERED ([Id] ASC)
CONSTRAINT [FK_CipherHistory_Cipher] FOREIGN KEY ([CipherId]) REFERENCES [dbo].[Cipher] ([Id]),
CONSTRAINT [FK_CipherHistory_User] FOREIGN KEY ([UserId]) REFERENCES [dbo].[User] ([Id])
); );