1
0
mirror of https://github.com/bitwarden/server.git synced 2025-04-06 05:28:15 -05:00

record installation devices

This commit is contained in:
Kyle Spearrin 2019-03-19 00:39:03 -04:00
parent 11fafa4a92
commit 01a293cf76
15 changed files with 255 additions and 36 deletions

View File

@ -76,12 +76,12 @@ namespace Bit.Api.Controllers
if(!string.IsNullOrWhiteSpace(model.UserId))
{
await _pushNotificationService.SendPayloadToUserAsync(Prefix(model.UserId),
model.Type.Value, model.Payload, Prefix(model.Identifier));
model.Type.Value, model.Payload, Prefix(model.Identifier), Prefix(model.DeviceId));
}
else if(!string.IsNullOrWhiteSpace(model.OrganizationId))
{
await _pushNotificationService.SendPayloadToOrganizationAsync(Prefix(model.OrganizationId),
model.Type.Value, model.Payload, Prefix(model.Identifier));
model.Type.Value, model.Payload, Prefix(model.Identifier), Prefix(model.DeviceId));
}
}

View File

@ -8,6 +8,7 @@ namespace Bit.Core.Models.Api
{
public string UserId { get; set; }
public string OrganizationId { get; set; }
public string DeviceId { get; set; }
public string Identifier { get; set; }
[Required]
public PushType? Type { get; set; }

View File

@ -0,0 +1,36 @@
using System;
using Microsoft.WindowsAzure.Storage.Table;
namespace Bit.Core.Models.Data
{
public class InstallationDeviceEntity : TableEntity
{
public InstallationDeviceEntity() { }
public InstallationDeviceEntity(Guid installationId, Guid deviceId)
{
PartitionKey = installationId.ToString();
RowKey = deviceId.ToString();
}
public InstallationDeviceEntity(string prefixedDeviceId)
{
var parts = prefixedDeviceId.Split("_");
if(parts.Length < 2)
{
throw new ArgumentException("Not enough parts.");
}
if(!Guid.TryParse(parts[0], out var installationId) || !Guid.TryParse(parts[1], out var deviceId))
{
throw new ArgumentException("Could not parse parts.");
}
PartitionKey = parts[0];
RowKey = parts[1];
}
public static bool IsInstallationDeviceId(string deviceId)
{
return deviceId != null && deviceId.Length == 73 && deviceId[36] == '_';
}
}
}

View File

@ -0,0 +1,13 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Bit.Core.Models.Data;
namespace Bit.Core.Repositories
{
public interface IInstallationDeviceRepository
{
Task UpsertAsync(InstallationDeviceEntity entity);
Task UpsertManyAsync(IList<InstallationDeviceEntity> entities);
Task DeleteAsync(InstallationDeviceEntity entity);
}
}

View File

@ -0,0 +1,24 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Bit.Core.Models.Data;
namespace Bit.Core.Repositories.Noop
{
public class InstallationDeviceRepository : IInstallationDeviceRepository
{
public Task UpsertAsync(InstallationDeviceEntity entity)
{
return Task.FromResult(0);
}
public Task UpsertManyAsync(IList<InstallationDeviceEntity> entities)
{
return Task.FromResult(0);
}
public Task DeleteAsync(InstallationDeviceEntity entity)
{
return Task.FromResult(0);
}
}
}

View File

@ -0,0 +1,79 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Bit.Core.Models.Data;
using Microsoft.WindowsAzure.Storage;
using Microsoft.WindowsAzure.Storage.Table;
namespace Bit.Core.Repositories.TableStorage
{
public class InstallationDeviceRepository : IInstallationDeviceRepository
{
private readonly CloudTable _table;
public InstallationDeviceRepository(GlobalSettings globalSettings)
: this(globalSettings.Events.ConnectionString)
{ }
public InstallationDeviceRepository(string storageConnectionString)
{
var storageAccount = CloudStorageAccount.Parse(storageConnectionString);
var tableClient = storageAccount.CreateCloudTableClient();
_table = tableClient.GetTableReference("installationdevice");
}
public async Task UpsertAsync(InstallationDeviceEntity entity)
{
await _table.ExecuteAsync(TableOperation.InsertOrReplace(entity));
}
public async Task UpsertManyAsync(IList<InstallationDeviceEntity> entities)
{
if(!entities?.Any() ?? true)
{
return;
}
if(entities.Count == 1)
{
await UpsertAsync(entities.First());
return;
}
var entityGroups = entities.GroupBy(ent => ent.PartitionKey);
foreach(var group in entityGroups)
{
var groupEntities = group.ToList();
if(groupEntities.Count == 1)
{
await UpsertAsync(groupEntities.First());
continue;
}
// A batch insert can only contain 100 entities at a time
var iterations = groupEntities.Count / 100;
for(var i = 0; i <= iterations; i++)
{
var batch = new TableBatchOperation();
var batchEntities = groupEntities.Skip(i * 100).Take(100);
if(!batchEntities.Any())
{
break;
}
foreach(var entity in batchEntities)
{
batch.InsertOrReplace(entity);
}
await _table.ExecuteBatchAsync(batch);
}
}
}
public async Task DeleteAsync(InstallationDeviceEntity entity)
{
await _table.ExecuteAsync(TableOperation.Delete(entity));
}
}
}

View File

@ -19,7 +19,8 @@ namespace Bit.Core.Services
Task PushSyncOrgKeysAsync(Guid userId);
Task PushSyncSettingsAsync(Guid userId);
Task PushLogOutAsync(Guid userId);
Task SendPayloadToUserAsync(string userId, PushType type, object payload, string identifier);
Task SendPayloadToOrganizationAsync(string orgId, PushType type, object payload, string identifier);
Task SendPayloadToUserAsync(string userId, PushType type, object payload, string identifier, string deviceId = null);
Task SendPayloadToOrganizationAsync(string orgId, PushType type, object payload, string identifier,
string deviceId = null);
}
}

View File

@ -141,7 +141,7 @@ namespace Bit.Core.Services
private async Task SendMessageAsync<T>(PushType type, T payload, bool excludeCurrentContext)
{
var contextId = GetContextIdentifier(excludeCurrentContext);
var message = JsonConvert.SerializeObject(new PushNotificationData<T>(type, payload, contextId),
var message = JsonConvert.SerializeObject(new PushNotificationData<T>(type, payload, contextId),
_jsonSettings);
var queueMessage = new CloudQueueMessage(message);
await _queue.AddMessageAsync(queueMessage);
@ -159,13 +159,15 @@ namespace Bit.Core.Services
return currentContext?.DeviceIdentifier;
}
public Task SendPayloadToUserAsync(string userId, PushType type, object payload, string identifier)
public Task SendPayloadToUserAsync(string userId, PushType type, object payload, string identifier,
string deviceId = null)
{
// Noop
return Task.FromResult(0);
}
public Task SendPayloadToOrganizationAsync(string orgId, PushType type, object payload, string identifier)
public Task SendPayloadToOrganizationAsync(string orgId, PushType type, object payload, string identifier,
string deviceId = null)
{
// Noop
return Task.FromResult(0);

View File

@ -6,14 +6,19 @@ using System.Collections.Generic;
using Microsoft.AspNetCore.Http;
using Bit.Core.Utilities;
using Microsoft.Extensions.Logging;
using Bit.Core.Repositories;
namespace Bit.Core.Services
{
public class MultiServicePushNotificationService : IPushNotificationService
{
private readonly List<IPushNotificationService> _services = new List<IPushNotificationService>();
private readonly IDeviceRepository _deviceRepository;
private readonly IInstallationDeviceRepository _installationDeviceRepository;
public MultiServicePushNotificationService(
IDeviceRepository deviceRepository,
IInstallationDeviceRepository installationDeviceRepository,
GlobalSettings globalSettings,
IHttpContextAccessor httpContextAccessor,
ILogger<RelayPushNotificationService> relayLogger,
@ -25,7 +30,8 @@ namespace Bit.Core.Services
globalSettings.Installation?.Id != null &&
CoreHelpers.SettingHasValue(globalSettings.Installation?.Key))
{
_services.Add(new RelayPushNotificationService(globalSettings, httpContextAccessor, relayLogger));
_services.Add(new RelayPushNotificationService(_deviceRepository, globalSettings,
httpContextAccessor, relayLogger));
}
if(CoreHelpers.SettingHasValue(globalSettings.InternalIdentityKey) &&
CoreHelpers.SettingHasValue(globalSettings.BaseServiceUri.InternalNotifications))
@ -38,13 +44,17 @@ namespace Bit.Core.Services
{
if(CoreHelpers.SettingHasValue(globalSettings.NotificationHub.ConnectionString))
{
_services.Add(new NotificationHubPushNotificationService(globalSettings, httpContextAccessor));
_services.Add(new NotificationHubPushNotificationService(_installationDeviceRepository,
globalSettings, httpContextAccessor));
}
if(CoreHelpers.SettingHasValue(globalSettings.Notifications?.ConnectionString))
{
_services.Add(new AzureQueuePushNotificationService(globalSettings, httpContextAccessor));
}
}
_deviceRepository = deviceRepository;
_installationDeviceRepository = installationDeviceRepository;
}
public Task PushSyncCipherCreateAsync(Cipher cipher, IEnumerable<Guid> collectionIds)
@ -113,15 +123,17 @@ namespace Bit.Core.Services
return Task.FromResult(0);
}
public Task SendPayloadToUserAsync(string userId, PushType type, object payload, string identifier)
public Task SendPayloadToUserAsync(string userId, PushType type, object payload, string identifier,
string deviceId = null)
{
PushToServices((s) => s.SendPayloadToUserAsync(userId, type, payload, identifier));
PushToServices((s) => s.SendPayloadToUserAsync(userId, type, payload, identifier, deviceId));
return Task.FromResult(0);
}
public Task SendPayloadToOrganizationAsync(string orgId, PushType type, object payload, string identifier)
public Task SendPayloadToOrganizationAsync(string orgId, PushType type, object payload, string identifier,
string deviceId = null)
{
PushToServices((s) => s.SendPayloadToOrganizationAsync(orgId, type, payload, identifier));
PushToServices((s) => s.SendPayloadToOrganizationAsync(orgId, type, payload, identifier, deviceId));
return Task.FromResult(0);
}

View File

@ -7,20 +7,25 @@ using Newtonsoft.Json;
using System.Collections.Generic;
using Microsoft.AspNetCore.Http;
using Bit.Core.Models;
using Bit.Core.Models.Data;
using Bit.Core.Repositories;
namespace Bit.Core.Services
{
public class NotificationHubPushNotificationService : IPushNotificationService
{
private readonly IInstallationDeviceRepository _installationDeviceRepository;
private readonly GlobalSettings _globalSettings;
private readonly IHttpContextAccessor _httpContextAccessor;
private NotificationHubClient _client = null;
public NotificationHubPushNotificationService(
IInstallationDeviceRepository installationDeviceRepository,
GlobalSettings globalSettings,
IHttpContextAccessor httpContextAccessor)
{
_installationDeviceRepository = installationDeviceRepository;
_globalSettings = globalSettings;
_httpContextAccessor = httpContextAccessor;
_client = NotificationHubClient.CreateClientFromConnectionString(
@ -141,16 +146,26 @@ namespace Bit.Core.Services
await SendPayloadToUserAsync(orgId.ToString(), type, payload, GetContextIdentifier(excludeCurrentContext));
}
public async Task SendPayloadToUserAsync(string userId, PushType type, object payload, string identifier)
public async Task SendPayloadToUserAsync(string userId, PushType type, object payload, string identifier,
string deviceId = null)
{
var tag = BuildTag($"template:payload_userId:{userId}", identifier);
await SendPayloadAsync(tag, type, payload);
if(InstallationDeviceEntity.IsInstallationDeviceId(deviceId))
{
await _installationDeviceRepository.UpsertAsync(new InstallationDeviceEntity(deviceId));
}
}
public async Task SendPayloadToOrganizationAsync(string orgId, PushType type, object payload, string identifier)
public async Task SendPayloadToOrganizationAsync(string orgId, PushType type, object payload, string identifier,
string deviceId = null)
{
var tag = BuildTag($"template:payload && organizationId:{orgId}", identifier);
await SendPayloadAsync(tag, type, payload);
if(InstallationDeviceEntity.IsInstallationDeviceId(deviceId))
{
await _installationDeviceRepository.UpsertAsync(new InstallationDeviceEntity(deviceId));
}
}
private string GetContextIdentifier(bool excludeCurrentContext)

View File

@ -4,18 +4,23 @@ using Microsoft.Azure.NotificationHubs;
using Bit.Core.Enums;
using System.Linq;
using System;
using Bit.Core.Models.Data;
using Bit.Core.Repositories;
namespace Bit.Core.Services
{
public class NotificationHubPushRegistrationService : IPushRegistrationService
{
private readonly IInstallationDeviceRepository _installationDeviceRepository;
private readonly GlobalSettings _globalSettings;
private NotificationHubClient _client = null;
public NotificationHubPushRegistrationService(
IInstallationDeviceRepository installationDeviceRepository,
GlobalSettings globalSettings)
{
_installationDeviceRepository = installationDeviceRepository;
_globalSettings = globalSettings;
_client = NotificationHubClient.CreateClientFromConnectionString(
_globalSettings.NotificationHub.ConnectionString,
@ -83,6 +88,10 @@ namespace Bit.Core.Services
userId, identifier);
await _client.CreateOrUpdateInstallationAsync(installation);
if(InstallationDeviceEntity.IsInstallationDeviceId(deviceId))
{
await _installationDeviceRepository.UpsertAsync(new InstallationDeviceEntity(deviceId));
}
}
private void BuildInstallationTemplate(Installation installation, string templateId, string templateBody,
@ -118,6 +127,10 @@ namespace Bit.Core.Services
try
{
await _client.DeleteInstallationAsync(deviceId);
if(InstallationDeviceEntity.IsInstallationDeviceId(deviceId))
{
await _installationDeviceRepository.DeleteAsync(new InstallationDeviceEntity(deviceId));
}
}
catch(Exception e)
{
@ -131,12 +144,22 @@ namespace Bit.Core.Services
public async Task AddUserRegistrationOrganizationAsync(IEnumerable<string> deviceIds, string organizationId)
{
await PatchTagsForUserDevicesAsync(deviceIds, UpdateOperationType.Add, $"organizationId:{organizationId}");
if(deviceIds.Any() && InstallationDeviceEntity.IsInstallationDeviceId(deviceIds.First()))
{
var entities = deviceIds.Select(e => new InstallationDeviceEntity(e));
await _installationDeviceRepository.UpsertManyAsync(entities.ToList());
}
}
public async Task DeleteUserRegistrationOrganizationAsync(IEnumerable<string> deviceIds, string organizationId)
{
await PatchTagsForUserDevicesAsync(deviceIds, UpdateOperationType.Remove,
$"organizationId:{organizationId}");
if(deviceIds.Any() && InstallationDeviceEntity.IsInstallationDeviceId(deviceIds.First()))
{
var entities = deviceIds.Select(e => new InstallationDeviceEntity(e));
await _installationDeviceRepository.UpsertManyAsync(entities.ToList());
}
}
private async Task PatchTagsForUserDevicesAsync(IEnumerable<string> deviceIds, UpdateOperationType op,

View File

@ -161,13 +161,15 @@ namespace Bit.Core.Services
return currentContext?.DeviceIdentifier;
}
public Task SendPayloadToUserAsync(string userId, PushType type, object payload, string identifier)
public Task SendPayloadToUserAsync(string userId, PushType type, object payload, string identifier,
string deviceId = null)
{
// Noop
return Task.FromResult(0);
}
public Task SendPayloadToOrganizationAsync(string orgId, PushType type, object payload, string identifier)
public Task SendPayloadToOrganizationAsync(string orgId, PushType type, object payload, string identifier,
string deviceId = null)
{
// Noop
return Task.FromResult(0);

View File

@ -8,15 +8,18 @@ using System.Net.Http;
using Bit.Core.Models.Api;
using Microsoft.Extensions.Logging;
using System.Collections.Generic;
using Bit.Core.Repositories;
namespace Bit.Core.Services
{
public class RelayPushNotificationService : BaseIdentityClientService, IPushNotificationService
{
private readonly IDeviceRepository _deviceRepository;
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly ILogger<RelayPushNotificationService> _logger;
public RelayPushNotificationService(
IDeviceRepository deviceRepository,
GlobalSettings globalSettings,
IHttpContextAccessor httpContextAccessor,
ILogger<RelayPushNotificationService> logger)
@ -28,6 +31,7 @@ namespace Bit.Core.Services
globalSettings.Installation.Key,
logger)
{
_deviceRepository = deviceRepository;
_httpContextAccessor = httpContextAccessor;
_logger = logger;
}
@ -143,12 +147,8 @@ namespace Bit.Core.Services
Type = type,
Payload = payload
};
if(excludeCurrentContext)
{
ExcludeCurrentContext(request);
}
await AddCurrentContextAsync(request, excludeCurrentContext);
await SendAsync(HttpMethod.Post, "push/send", request);
}
@ -161,30 +161,36 @@ namespace Bit.Core.Services
Payload = payload
};
if(excludeCurrentContext)
{
ExcludeCurrentContext(request);
}
await AddCurrentContextAsync(request, excludeCurrentContext);
await SendAsync(HttpMethod.Post, "push/send", request);
}
private void ExcludeCurrentContext(PushSendRequestModel request)
private async Task AddCurrentContextAsync(PushSendRequestModel request, bool addIdentifier)
{
var currentContext = _httpContextAccessor?.HttpContext?.
RequestServices.GetService(typeof(CurrentContext)) as CurrentContext;
RequestServices.GetService(typeof(CurrentContext)) as CurrentContext;
if(!string.IsNullOrWhiteSpace(currentContext?.DeviceIdentifier))
{
request.Identifier = currentContext.DeviceIdentifier;
var device = await _deviceRepository.GetByIdentifierAsync(currentContext.DeviceIdentifier);
if(device != null)
{
request.DeviceId = device.Id.ToString();
}
if(addIdentifier)
{
request.Identifier = currentContext.DeviceIdentifier;
}
}
}
public Task SendPayloadToUserAsync(string userId, PushType type, object payload, string identifier)
public Task SendPayloadToUserAsync(string userId, PushType type, object payload, string identifier,
string deviceId = null)
{
throw new NotImplementedException();
}
public Task SendPayloadToOrganizationAsync(string orgId, PushType type, object payload, string identifier)
public Task SendPayloadToOrganizationAsync(string orgId, PushType type, object payload, string identifier,
string deviceId = null)
{
throw new NotImplementedException();
}

View File

@ -63,12 +63,14 @@ namespace Bit.Core.Services
return Task.FromResult(0);
}
public Task SendPayloadToOrganizationAsync(string orgId, PushType type, object payload, string identifier)
public Task SendPayloadToOrganizationAsync(string orgId, PushType type, object payload, string identifier,
string deviceId = null)
{
return Task.FromResult(0);
}
public Task SendPayloadToUserAsync(string userId, PushType type, object payload, string identifier)
public Task SendPayloadToUserAsync(string userId, PushType type, object payload, string identifier,
string deviceId = null)
{
return Task.FromResult(0);
}

View File

@ -20,6 +20,7 @@ using System;
using System.IO;
using SqlServerRepos = Bit.Core.Repositories.SqlServer;
using PostgreSqlRepos = Bit.Core.Repositories.PostgreSql;
using NoopRepos = Bit.Core.Repositories.Noop;
using System.Threading.Tasks;
using TableStorageRepos = Bit.Core.Repositories.TableStorage;
using Microsoft.Extensions.DependencyInjection.Extensions;
@ -58,10 +59,12 @@ namespace Bit.Core.Utilities
if(globalSettings.SelfHosted)
{
services.AddSingleton<IEventRepository, SqlServerRepos.EventRepository>();
services.AddSingleton<IInstallationDeviceRepository, NoopRepos.InstallationDeviceRepository>();
}
else
{
services.AddSingleton<IEventRepository, TableStorageRepos.EventRepository>();
services.AddSingleton<IInstallationDeviceRepository, TableStorageRepos.InstallationDeviceRepository>();
}
}