1
0
mirror of https://github.com/bitwarden/mobile.git synced 2024-11-04 08:50:18 +01:00

add collection syncing

This commit is contained in:
Kyle Spearrin 2017-11-24 16:11:40 -05:00
parent 3b44ede67e
commit c9ceb09906
19 changed files with 302 additions and 21 deletions

View File

@ -226,6 +226,8 @@ namespace Bit.Android
container.RegisterSingleton<ISettingsApiRepository, SettingsApiRepository>(); container.RegisterSingleton<ISettingsApiRepository, SettingsApiRepository>();
container.RegisterSingleton<ITwoFactorApiRepository, TwoFactorApiRepository>(); container.RegisterSingleton<ITwoFactorApiRepository, TwoFactorApiRepository>();
container.RegisterSingleton<ISyncApiRepository, SyncApiRepository>(); container.RegisterSingleton<ISyncApiRepository, SyncApiRepository>();
container.RegisterSingleton<ICollectionRepository, CollectionRepository>();
container.RegisterSingleton<ICipherCollectionRepository, CipherCollectionRepository>();
// Other // Other
container.RegisterSingleton(CrossSettings.Current); container.RegisterSingleton(CrossSettings.Current);

View File

@ -0,0 +1,14 @@
using System.Threading.Tasks;
using Bit.App.Models.Data;
using System.Collections.Generic;
namespace Bit.App.Abstractions
{
public interface ICipherCollectionRepository
{
Task<IEnumerable<CipherCollectionData>> GetAllByUserIdAsync(string userId);
Task InsertAsync(CipherCollectionData obj);
Task DeleteAsync(CipherCollectionData obj);
Task DeleteByUserIdAsync(string userId);
}
}

View File

@ -0,0 +1,11 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Bit.App.Models.Data;
namespace Bit.App.Abstractions
{
public interface ICollectionRepository : IRepository<CollectionData, string>
{
Task<IEnumerable<CollectionData>> GetAllByUserIdAsync(string userId);
}
}

View File

@ -36,6 +36,8 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<Compile Include="Abstractions\Repositories\IAttachmentRepository.cs" /> <Compile Include="Abstractions\Repositories\IAttachmentRepository.cs" />
<Compile Include="Abstractions\Repositories\ICipherCollectionRepository.cs" />
<Compile Include="Abstractions\Repositories\ICollectionRepository.cs" />
<Compile Include="Abstractions\Repositories\ISyncApiRepository.cs" /> <Compile Include="Abstractions\Repositories\ISyncApiRepository.cs" />
<Compile Include="Abstractions\Repositories\ITwoFactorApiRepository.cs" /> <Compile Include="Abstractions\Repositories\ITwoFactorApiRepository.cs" />
<Compile Include="Abstractions\Repositories\ISettingsApiRepository.cs" /> <Compile Include="Abstractions\Repositories\ISettingsApiRepository.cs" />
@ -107,6 +109,7 @@
<Compile Include="Models\Api\FieldDataModel.cs" /> <Compile Include="Models\Api\FieldDataModel.cs" />
<Compile Include="Models\Api\CardDataModel.cs" /> <Compile Include="Models\Api\CardDataModel.cs" />
<Compile Include="Models\Api\IdentityDataModel.cs" /> <Compile Include="Models\Api\IdentityDataModel.cs" />
<Compile Include="Models\Api\Response\CollectionResponse.cs" />
<Compile Include="Models\Api\SecureNoteDataModel.cs" /> <Compile Include="Models\Api\SecureNoteDataModel.cs" />
<Compile Include="Models\Api\Request\DeviceTokenRequest.cs" /> <Compile Include="Models\Api\Request\DeviceTokenRequest.cs" />
<Compile Include="Models\Api\Request\FolderRequest.cs" /> <Compile Include="Models\Api\Request\FolderRequest.cs" />
@ -134,7 +137,10 @@
<Compile Include="Models\CipherString.cs" /> <Compile Include="Models\CipherString.cs" />
<Compile Include="Models\Data\AttachmentData.cs" /> <Compile Include="Models\Data\AttachmentData.cs" />
<Compile Include="Models\Attachment.cs" /> <Compile Include="Models\Attachment.cs" />
<Compile Include="Models\Data\CipherCollectionData.cs" />
<Compile Include="Models\Data\CollectionData.cs" />
<Compile Include="Models\Field.cs" /> <Compile Include="Models\Field.cs" />
<Compile Include="Models\Collection.cs" />
<Compile Include="Models\Identity.cs" /> <Compile Include="Models\Identity.cs" />
<Compile Include="Models\Login.cs" /> <Compile Include="Models\Login.cs" />
<Compile Include="Models\Page\VaultAttachmentsPageModel.cs" /> <Compile Include="Models\Page\VaultAttachmentsPageModel.cs" />
@ -186,6 +192,9 @@
<Compile Include="Properties\AssemblyInfo.cs" /> <Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Abstractions\Repositories\ICipherRepository.cs" /> <Compile Include="Abstractions\Repositories\ICipherRepository.cs" />
<Compile Include="Repositories\AttachmentRepository.cs" /> <Compile Include="Repositories\AttachmentRepository.cs" />
<Compile Include="Repositories\BaseRepository.cs" />
<Compile Include="Repositories\CipherCollectionRepository.cs" />
<Compile Include="Repositories\CollectionRepository.cs" />
<Compile Include="Repositories\SyncApiRepository.cs" /> <Compile Include="Repositories\SyncApiRepository.cs" />
<Compile Include="Repositories\TwoFactorApiRepository.cs" /> <Compile Include="Repositories\TwoFactorApiRepository.cs" />
<Compile Include="Repositories\SettingsApiRepository.cs" /> <Compile Include="Repositories\SettingsApiRepository.cs" />

View File

@ -17,6 +17,7 @@ namespace Bit.App.Models.Api
public bool OrganizationUseTotp { get; set; } public bool OrganizationUseTotp { get; set; }
public JObject Data { get; set; } public JObject Data { get; set; }
public IEnumerable<AttachmentResponse> Attachments { get; set; } public IEnumerable<AttachmentResponse> Attachments { get; set; }
public IEnumerable<string> CollectionIds { get; set; }
public DateTime RevisionDate { get; set; } public DateTime RevisionDate { get; set; }
} }
} }

View File

@ -0,0 +1,9 @@
namespace Bit.App.Models.Api
{
public class CollectionResponse
{
public string Id { get; set; }
public string Name { get; set; }
public string OrganizationId { get; set; }
}
}

View File

@ -6,6 +6,7 @@ namespace Bit.App.Models.Api
{ {
public ProfileResponse Profile { get; set; } public ProfileResponse Profile { get; set; }
public IEnumerable<FolderResponse> Folders { get; set; } public IEnumerable<FolderResponse> Folders { get; set; }
public IEnumerable<CollectionResponse> Collections { get; set; }
public IEnumerable<CipherResponse> Ciphers { get; set; } public IEnumerable<CipherResponse> Ciphers { get; set; }
public DomainsResponse Domains { get; set; } public DomainsResponse Domains { get; set; }
} }

View File

@ -0,0 +1,29 @@
using Bit.App.Models.Data;
using Bit.App.Models.Api;
namespace Bit.App.Models
{
public class Collection
{
public Collection()
{ }
public Collection(CollectionData data)
{
Id = data.Id;
OrganizationId = data.OrganizationId;
Name = data.Name != null ? new CipherString(data.Name) : null;
}
public Collection(CollectionResponse response)
{
Id = response.Id;
OrganizationId = response.OrganizationId;
Name = response.Name != null ? new CipherString(response.Name) : null;
}
public string Id { get; set; }
public string OrganizationId { get; set; }
public CipherString Name { get; set; }
}
}

View File

@ -0,0 +1,18 @@
using SQLite;
namespace Bit.App.Models.Data
{
[Table("CipherCollection")]
public class CipherCollectionData
{
[PrimaryKey]
[AutoIncrement]
public int Id { get; set; }
[Indexed]
public string UserId { get; set; }
[Indexed]
public string CipherId { get; set; }
[Indexed]
public string CollectionId { get; set; }
}
}

View File

@ -0,0 +1,36 @@
using SQLite;
using Bit.App.Abstractions;
using Bit.App.Models.Api;
namespace Bit.App.Models.Data
{
[Table("Collection")]
public class CollectionData : IDataObject<string>
{
public CollectionData()
{ }
public CollectionData(Collection collection, string userId)
{
Id = collection.Id;
UserId = userId;
Name = collection.Name?.EncryptedString;
OrganizationId = collection.OrganizationId;
}
public CollectionData(CollectionResponse collection, string userId)
{
Id = collection.Id;
UserId = userId;
Name = collection.Name;
OrganizationId = collection.OrganizationId;
}
[PrimaryKey]
public string Id { get; set; }
[Indexed]
public string UserId { get; set; }
public string Name { get; set; }
public string OrganizationId { get; set; }
}
}

View File

@ -32,10 +32,5 @@ namespace Bit.App.Models.Data
public string UserId { get; set; } public string UserId { get; set; }
public string Name { get; set; } public string Name { get; set; }
public DateTime RevisionDateTime { get; set; } = DateTime.UtcNow; public DateTime RevisionDateTime { get; set; } = DateTime.UtcNow;
public Folder ToFolder()
{
return new Folder(this);
}
} }
} }

View File

@ -0,0 +1,15 @@
using Bit.App.Abstractions;
using SQLite;
namespace Bit.App.Repositories
{
public abstract class BaseRepository
{
public BaseRepository(ISqlService sqlService)
{
Connection = sqlService.GetConnection();
}
protected SQLiteConnection Connection { get; private set; }
}
}

View File

@ -0,0 +1,40 @@
using System.Threading.Tasks;
using Bit.App.Abstractions;
using Bit.App.Models.Data;
using System.Collections.Generic;
using System.Linq;
namespace Bit.App.Repositories
{
public class CipherCollectionRepository : BaseRepository, ICipherCollectionRepository
{
public CipherCollectionRepository(ISqlService sqlService)
: base(sqlService)
{ }
public Task<IEnumerable<CipherCollectionData>> GetAllByUserIdAsync(string userId)
{
var cipherCollections = Connection.Table<CipherCollectionData>().Where(f => f.UserId == userId)
.Cast<CipherCollectionData>();
return Task.FromResult(cipherCollections);
}
public virtual Task InsertAsync(CipherCollectionData obj)
{
Connection.Insert(obj);
return Task.FromResult(0);
}
public virtual Task DeleteAsync(CipherCollectionData obj)
{
Connection.Delete<CipherCollectionData>(obj.Id);
return Task.FromResult(0);
}
public virtual Task DeleteByUserIdAsync(string userId)
{
Connection.Execute("DELETE FROM CipherCollection WHERE UserId = ?", userId);
return Task.FromResult(0);
}
}
}

View File

@ -0,0 +1,21 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Bit.App.Abstractions;
using Bit.App.Models.Data;
namespace Bit.App.Repositories
{
public class CollectionRepository : Repository<CollectionData, string>, ICollectionRepository
{
public CollectionRepository(ISqlService sqlService)
: base(sqlService)
{ }
public Task<IEnumerable<CollectionData>> GetAllByUserIdAsync(string userId)
{
var folders = Connection.Table<CollectionData>().Where(f => f.UserId == userId).Cast<CollectionData>();
return Task.FromResult(folders);
}
}
}

View File

@ -3,20 +3,16 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Bit.App.Abstractions; using Bit.App.Abstractions;
using SQLite;
namespace Bit.App.Repositories namespace Bit.App.Repositories
{ {
public abstract class Repository<T, TId> : IRepository<T, TId> public abstract class Repository<T, TId> : BaseRepository, IRepository<T, TId>
where TId : IEquatable<TId> where TId : IEquatable<TId>
where T : class, IDataObject<TId>, new() where T : class, IDataObject<TId>, new()
{ {
public Repository(ISqlService sqlService) public Repository(ISqlService sqlService)
{ : base(sqlService)
Connection = sqlService.GetConnection(); { }
}
protected SQLiteConnection Connection { get; private set; }
public virtual Task<T> GetByIdAsync(TId id) public virtual Task<T> GetByIdAsync(TId id)
{ {
@ -39,6 +35,7 @@ namespace Bit.App.Repositories
Connection.Update(obj); Connection.Update(obj);
return Task.FromResult(0); return Task.FromResult(0);
} }
public virtual Task UpsertAsync(T obj) public virtual Task UpsertAsync(T obj)
{ {
Connection.InsertOrReplace(obj); Connection.InsertOrReplace(obj);

View File

@ -17,7 +17,9 @@ namespace Bit.App.Services
public void CreateTables() public void CreateTables()
{ {
_connection.CreateTable<FolderData>(); _connection.CreateTable<FolderData>();
_connection.CreateTable<CollectionData>();
_connection.CreateTable<CipherData>(); _connection.CreateTable<CipherData>();
_connection.CreateTable<CipherCollectionData>();
_connection.CreateTable<AttachmentData>(); _connection.CreateTable<AttachmentData>();
_connection.CreateTable<SettingsData>(); _connection.CreateTable<SettingsData>();
} }

View File

@ -20,6 +20,8 @@ namespace Bit.App.Services
private readonly ISettingsApiRepository _settingsApiRepository; private readonly ISettingsApiRepository _settingsApiRepository;
private readonly ISyncApiRepository _syncApiRepository; private readonly ISyncApiRepository _syncApiRepository;
private readonly IFolderRepository _folderRepository; private readonly IFolderRepository _folderRepository;
private readonly ICollectionRepository _collectionRepository;
private readonly ICipherCollectionRepository _cipherCollectionRepository;
private readonly ICipherService _cipherService; private readonly ICipherService _cipherService;
private readonly IAttachmentRepository _attachmentRepository; private readonly IAttachmentRepository _attachmentRepository;
private readonly ISettingsRepository _settingsRepository; private readonly ISettingsRepository _settingsRepository;
@ -35,6 +37,8 @@ namespace Bit.App.Services
ISettingsApiRepository settingsApiRepository, ISettingsApiRepository settingsApiRepository,
ISyncApiRepository syncApiRepository, ISyncApiRepository syncApiRepository,
IFolderRepository folderRepository, IFolderRepository folderRepository,
ICollectionRepository collectionRepository,
ICipherCollectionRepository cipherCollectionRepository,
ICipherService cipherService, ICipherService cipherService,
IAttachmentRepository attachmentRepository, IAttachmentRepository attachmentRepository,
ISettingsRepository settingsRepository, ISettingsRepository settingsRepository,
@ -49,6 +53,8 @@ namespace Bit.App.Services
_settingsApiRepository = settingsApiRepository; _settingsApiRepository = settingsApiRepository;
_syncApiRepository = syncApiRepository; _syncApiRepository = syncApiRepository;
_folderRepository = folderRepository; _folderRepository = folderRepository;
_collectionRepository = collectionRepository;
_cipherCollectionRepository = cipherCollectionRepository;
_cipherService = cipherService; _cipherService = cipherService;
_attachmentRepository = attachmentRepository; _attachmentRepository = attachmentRepository;
_settingsRepository = settingsRepository; _settingsRepository = settingsRepository;
@ -258,9 +264,9 @@ namespace Bit.App.Services
var now = DateTime.UtcNow; var now = DateTime.UtcNow;
var syncResponse = await _syncApiRepository.Get(); var syncResponse = await _syncApiRepository.Get();
if(!CheckSuccess(syncResponse, if(!CheckSuccess(syncResponse,
!string.IsNullOrWhiteSpace(_appSettingsService.SecurityStamp) && !string.IsNullOrWhiteSpace(_appSettingsService.SecurityStamp) &&
syncResponse.Result?.Profile != null && syncResponse.Result?.Profile != null &&
_appSettingsService.SecurityStamp != syncResponse.Result.Profile.SecurityStamp)) _appSettingsService.SecurityStamp != syncResponse.Result.Profile.SecurityStamp))
{ {
return false; return false;
@ -268,15 +274,17 @@ namespace Bit.App.Services
var ciphersDict = syncResponse.Result.Ciphers.ToDictionary(s => s.Id); var ciphersDict = syncResponse.Result.Ciphers.ToDictionary(s => s.Id);
var foldersDict = syncResponse.Result.Folders.ToDictionary(f => f.Id); var foldersDict = syncResponse.Result.Folders.ToDictionary(f => f.Id);
var collectionsDict = syncResponse.Result.Collections?.ToDictionary(c => c.Id);
var cipherTask = SyncCiphersAsync(ciphersDict); var cipherTask = SyncCiphersAsync(ciphersDict);
var folderTask = SyncFoldersAsync(foldersDict); var folderTask = SyncFoldersAsync(foldersDict);
var collectionsTask = SyncCollectionsAsync(collectionsDict);
var domainsTask = SyncDomainsAsync(syncResponse.Result.Domains); var domainsTask = SyncDomainsAsync(syncResponse.Result.Domains);
var profileTask = SyncProfileKeysAsync(syncResponse.Result.Profile); var profileTask = SyncProfileKeysAsync(syncResponse.Result.Profile);
await Task.WhenAll(cipherTask, folderTask, domainsTask, profileTask).ConfigureAwait(false); await Task.WhenAll(cipherTask, folderTask, collectionsTask, domainsTask, profileTask).ConfigureAwait(false);
if(folderTask.Exception != null || cipherTask.Exception != null || domainsTask.Exception != null || if(folderTask.Exception != null || cipherTask.Exception != null || collectionsTask.Exception != null ||
profileTask.Exception != null) domainsTask.Exception != null || profileTask.Exception != null)
{ {
SyncCompleted(false); SyncCompleted(false);
return false; return false;
@ -349,7 +357,48 @@ namespace Bit.App.Services
} }
} }
private async Task SyncCiphersAsync(IDictionary<string, CipherResponse> serviceCiphers) private async Task SyncCollectionsAsync(IDictionary<string, CollectionResponse> serverCollections)
{
if(!_authService.IsAuthenticated)
{
return;
}
var localCollections = (await _collectionRepository.GetAllByUserIdAsync(_authService.UserId)
.ConfigureAwait(false))
.GroupBy(f => f.Id)
.Select(f => f.First())
.ToDictionary(f => f.Id);
if(serverCollections != null)
{
foreach(var serverCollection in serverCollections)
{
if(!_authService.IsAuthenticated)
{
return;
}
try
{
var data = new CollectionData(serverCollection.Value, _authService.UserId);
await _collectionRepository.UpsertAsync(data).ConfigureAwait(false);
}
catch(SQLite.SQLiteException) { }
}
}
foreach(var collection in localCollections.Where(lc => !serverCollections.ContainsKey(lc.Key)))
{
try
{
await _collectionRepository.DeleteAsync(collection.Value.Id).ConfigureAwait(false);
}
catch(SQLite.SQLiteException) { }
}
}
private async Task SyncCiphersAsync(IDictionary<string, CipherResponse> serverCiphers)
{ {
if(!_authService.IsAuthenticated) if(!_authService.IsAuthenticated)
{ {
@ -367,16 +416,29 @@ namespace Bit.App.Services
.GroupBy(a => a.LoginId) .GroupBy(a => a.LoginId)
.ToDictionary(g => g.Key); .ToDictionary(g => g.Key);
foreach(var serverCipher in serviceCiphers) var cipherCollections = new List<CipherCollectionData>();
foreach(var serverCipher in serverCiphers)
{ {
if(!_authService.IsAuthenticated) if(!_authService.IsAuthenticated)
{ {
return; return;
} }
var collectionForThisCipher = serverCipher.Value.CollectionIds?.Select(cid => new CipherCollectionData
{
CipherId = serverCipher.Value.Id,
CollectionId = cid,
UserId = _authService.UserId
}).ToList();
if(collectionForThisCipher != null && collectionForThisCipher.Any())
{
cipherCollections.AddRange(collectionForThisCipher);
}
try try
{ {
var localCipher = localCiphers.ContainsKey(serverCipher.Value.Id) ? var localCipher = localCiphers.ContainsKey(serverCipher.Value.Id) ?
localCiphers[serverCipher.Value.Id] : null; localCiphers[serverCipher.Value.Id] : null;
var data = new CipherData(serverCipher.Value, _authService.UserId); var data = new CipherData(serverCipher.Value, _authService.UserId);
@ -404,7 +466,7 @@ namespace Bit.App.Services
catch(SQLite.SQLiteException) { } catch(SQLite.SQLiteException) { }
} }
foreach(var cipher in localCiphers.Where(local => !serviceCiphers.ContainsKey(local.Key))) foreach(var cipher in localCiphers.Where(local => !serverCiphers.ContainsKey(local.Key)))
{ {
try try
{ {
@ -412,6 +474,21 @@ namespace Bit.App.Services
} }
catch(SQLite.SQLiteException) { } catch(SQLite.SQLiteException) { }
} }
try
{
await _cipherCollectionRepository.DeleteByUserIdAsync(_authService.UserId).ConfigureAwait(false);
}
catch(SQLite.SQLiteException) { }
foreach(var cipherCollection in cipherCollections)
{
try
{
await _cipherCollectionRepository.InsertAsync(cipherCollection).ConfigureAwait(false);
}
catch(SQLite.SQLiteException) { }
}
} }
private async Task SyncDomainsAsync(DomainsResponse serverDomains) private async Task SyncDomainsAsync(DomainsResponse serverDomains)

View File

@ -301,6 +301,8 @@ namespace Bit.iOS.Extension
container.RegisterSingleton<IAccountsApiRepository, AccountsApiRepository>(); container.RegisterSingleton<IAccountsApiRepository, AccountsApiRepository>();
container.RegisterSingleton<ICipherApiRepository, CipherApiRepository>(); container.RegisterSingleton<ICipherApiRepository, CipherApiRepository>();
container.RegisterSingleton<ISyncApiRepository, SyncApiRepository>(); container.RegisterSingleton<ISyncApiRepository, SyncApiRepository>();
container.RegisterSingleton<ICollectionRepository, CollectionRepository>();
container.RegisterSingleton<ICipherCollectionRepository, CipherCollectionRepository>();
// Other // Other
container.RegisterSingleton(CrossConnectivity.Current); container.RegisterSingleton(CrossConnectivity.Current);

View File

@ -285,6 +285,8 @@ namespace Bit.iOS
container.RegisterSingleton<ISettingsApiRepository, SettingsApiRepository>(); container.RegisterSingleton<ISettingsApiRepository, SettingsApiRepository>();
container.RegisterSingleton<ITwoFactorApiRepository, TwoFactorApiRepository>(); container.RegisterSingleton<ITwoFactorApiRepository, TwoFactorApiRepository>();
container.RegisterSingleton<ISyncApiRepository, SyncApiRepository>(); container.RegisterSingleton<ISyncApiRepository, SyncApiRepository>();
container.RegisterSingleton<ICollectionRepository, CollectionRepository>();
container.RegisterSingleton<ICipherCollectionRepository, CipherCollectionRepository>();
// Other // Other
container.RegisterSingleton(CrossConnectivity.Current); container.RegisterSingleton(CrossConnectivity.Current);