1
0
mirror of https://github.com/bitwarden/server.git synced 2025-01-10 20:07:56 +01:00

notification hub services

This commit is contained in:
Kyle Spearrin 2017-05-26 22:52:50 -04:00
parent 887fe4fc05
commit 5af974d541
6 changed files with 298 additions and 11 deletions

View File

@ -8,5 +8,7 @@ namespace Bit.Core.Services
{
Task CreateOrUpdateRegistrationAsync(Device device);
Task DeleteRegistrationAsync(Guid deviceId);
Task AddUserRegistrationOrganizationAsync(Guid userId, Guid organizationId);
Task DeleteUserRegistrationOrganizationAsync(Guid userId, Guid organizationId);
}
}

View File

@ -0,0 +1,183 @@
using System;
using System.Threading.Tasks;
using Bit.Core.Models.Table;
using Microsoft.Azure.NotificationHubs;
using Bit.Core.Enums;
using Newtonsoft.Json;
using System.Collections.Generic;
using Microsoft.AspNetCore.Http;
namespace Bit.Core.Services
{
public class NotificationHubPushNotificationService : IPushNotificationService
{
private readonly NotificationHubClient _client;
private readonly IHttpContextAccessor _httpContextAccessor;
public NotificationHubPushNotificationService(
GlobalSettings globalSettings,
IHttpContextAccessor httpContextAccessor)
{
_client = NotificationHubClient.CreateClientFromConnectionString(globalSettings.NotificationHub.ConnectionString,
globalSettings.NotificationHub.HubName);
_httpContextAccessor = httpContextAccessor;
}
public async Task PushSyncCipherCreateAsync(Cipher cipher)
{
await PushCipherAsync(cipher, PushType.SyncCipherCreate);
}
public async Task PushSyncCipherUpdateAsync(Cipher cipher)
{
await PushCipherAsync(cipher, PushType.SyncCipherUpdate);
}
public async Task PushSyncCipherDeleteAsync(Cipher cipher)
{
await PushCipherAsync(cipher, PushType.SyncLoginDelete);
}
private async Task PushCipherAsync(Cipher cipher, PushType type)
{
if(cipher.OrganizationId.HasValue)
{
// We cannot send org pushes since access logic is much more complicated than just the fact that they belong
// to the organization. Potentially we could blindly send to just users that have the access all permission
// device registration needs to be more granular to handle that appropriately. A more brute force approach could
// me to send "full sync" push to all org users, but that has the potential to DDOS the API in bursts.
// await SendPayloadToOrganizationAsync(cipher.OrganizationId.Value, type, message, true);
}
else if(cipher.UserId.HasValue)
{
var message = new SyncCipherPushNotification
{
Id = cipher.Id,
UserId = cipher.UserId,
OrganizationId = cipher.OrganizationId,
RevisionDate = cipher.RevisionDate,
};
await SendPayloadToUserAsync(cipher.UserId.Value, type, message, true);
}
}
public async Task PushSyncFolderCreateAsync(Folder folder)
{
await PushFolderAsync(folder, PushType.SyncFolderCreate);
}
public async Task PushSyncFolderUpdateAsync(Folder folder)
{
await PushFolderAsync(folder, PushType.SyncFolderUpdate);
}
public async Task PushSyncFolderDeleteAsync(Folder folder)
{
await PushFolderAsync(folder, PushType.SyncFolderDelete);
}
private async Task PushFolderAsync(Folder folder, PushType type)
{
var message = new SyncFolderPushNotification
{
Id = folder.Id,
UserId = folder.UserId,
RevisionDate = folder.RevisionDate
};
await SendPayloadToUserAsync(folder.UserId, type, message, true);
}
public async Task PushSyncCiphersAsync(Guid userId)
{
await PushSyncUserAsync(userId, PushType.SyncCiphers);
}
public async Task PushSyncVaultAsync(Guid userId)
{
await PushSyncUserAsync(userId, PushType.SyncVault);
}
public async Task PushSyncOrgKeysAsync(Guid userId)
{
await PushSyncUserAsync(userId, PushType.SyncOrgKeys);
}
public async Task PushSyncSettingsAsync(Guid userId)
{
await PushSyncUserAsync(userId, PushType.SyncSettings);
}
private async Task PushSyncUserAsync(Guid userId, PushType type)
{
var message = new SyncUserPushNotification
{
UserId = userId,
Date = DateTime.UtcNow
};
await SendPayloadToUserAsync(userId, type, message, false);
}
private async Task SendPayloadToUserAsync(Guid userId, PushType type, object payload, bool excludeCurrentContext)
{
var tag = BuildTag($"template:payload_userId:{userId}", excludeCurrentContext);
await SendPayloadAsync(tag, type, payload);
}
private async Task SendPayloadToOrganizationAsync(Guid orgId, PushType type, object payload, bool excludeCurrentContext)
{
var tag = BuildTag($"template:payload && organizationId:{orgId}", excludeCurrentContext);
await SendPayloadAsync(tag, type, payload);
}
private string BuildTag(string tag, bool excludeCurrentContext)
{
if(excludeCurrentContext)
{
var currentContext = _httpContextAccessor?.HttpContext?.
RequestServices.GetService(typeof(CurrentContext)) as CurrentContext;
if(!string.IsNullOrWhiteSpace(currentContext?.DeviceIdentifier))
{
tag += $" && !deviceIdentifier:{currentContext.DeviceIdentifier}";
}
}
return $"({tag})";
}
private async Task SendPayloadAsync(string tag, PushType type, object payload)
{
await _client.SendTemplateNotificationAsync(
new Dictionary<string, string>
{
{ "type", ((byte)type).ToString() },
{ "payload", JsonConvert.SerializeObject(payload) }
}, tag);
}
private class SyncCipherPushNotification
{
public Guid Id { get; set; }
public Guid? UserId { get; set; }
public Guid? OrganizationId { get; set; }
public DateTime RevisionDate { get; set; }
}
private class SyncFolderPushNotification
{
public Guid Id { get; set; }
public Guid UserId { get; set; }
public DateTime RevisionDate { get; set; }
}
private class SyncUserPushNotification
{
public Guid UserId { get; set; }
public DateTime Date { get; set; }
}
}
}

View File

@ -1,20 +1,25 @@
using System;
using System.Linq;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.Azure.NotificationHubs;
using Bit.Core.Models.Table;
using Bit.Core.Repositories;
namespace Bit.Core.Services
{
public class NotificationHubPushRegistrationService : IPushRegistrationService
{
private readonly NotificationHubClient _client;
private readonly IDeviceRepository _deviceRepository;
public NotificationHubPushRegistrationService(GlobalSettings globalSettings)
public NotificationHubPushRegistrationService(
GlobalSettings globalSettings,
IDeviceRepository deviceRepository)
{
_client = NotificationHubClient.CreateClientFromConnectionString(globalSettings.NotificationHub.ConnectionString,
globalSettings.NotificationHub.HubName);
_deviceRepository = deviceRepository;
}
public async Task CreateOrUpdateRegistrationAsync(Device device)
@ -27,40 +32,115 @@ namespace Bit.Core.Services
var installation = new Installation
{
InstallationId = device.Id.ToString(),
PushChannel = device.PushToken
PushChannel = device.PushToken,
Templates = new Dictionary<string, InstallationTemplate>()
};
installation.Tags = new List<string>
{
"userId:" + device.UserId.ToString()
$"userId:{device.UserId}"
};
if(!string.IsNullOrWhiteSpace(device.Identifier))
{
installation.Tags.Add("identifier:" + device.Identifier);
installation.Tags.Add("deviceIdentifier:" + device.Identifier);
}
string payloadTemplate = null, messageTemplate = null, badgeMessageTemplate = null;
switch(device.Type)
{
case Enums.DeviceType.Android:
payloadTemplate = "{\"data\":{\"type\":\"#(type)\",\"payload\":\"$(payload)\"}}";
messageTemplate = "{\"data\":{\"type\":\"#(type)\"}," +
"\"notification\":{\"title\":\"$(title)\",\"body\":\"$(message)\"}}";
installation.Platform = NotificationPlatform.Gcm;
break;
case Enums.DeviceType.iOS:
payloadTemplate = "{\"data\":{\"type\":\"#(type)\",\"payload\":\"$(payload)\"}," +
"\"aps\":{\"alert\":null,\"badge\":null,\"content-available\":1}}";
messageTemplate = "{\"data\":{\"type\":\"#(type)\"}," +
"\"aps\":{\"alert\":\"$(message)\",\"badge\":null,\"content-available\":1}}";
badgeMessageTemplate = "{\"data\":{\"type\":\"#(type)\"}," +
"\"aps\":{\"alert\":\"$(message)\",\"badge\":\"#(badge)\",\"content-available\":1}}";
installation.Platform = NotificationPlatform.Apns;
break;
case Enums.DeviceType.AndroidAmazon:
payloadTemplate = "{\"data\":{\"type\":\"#(type)\",\"payload\":\"$(payload)\"}}";
messageTemplate = "{\"data\":{\"type\":\"#(type)\",\"message\":\"$(message)\"}}";
installation.Platform = NotificationPlatform.Adm;
break;
default:
break;
}
BuildInstallationTemplate(installation, "payload", payloadTemplate, device.UserId, device.Identifier);
BuildInstallationTemplate(installation, "message", messageTemplate, device.UserId, device.Identifier);
BuildInstallationTemplate(installation, "badgeMessage", badgeMessageTemplate ?? messageTemplate, device.UserId,
device.Identifier);
await _client.CreateOrUpdateInstallationAsync(installation);
}
private void BuildInstallationTemplate(Installation installation, string templateId, string templateBody,
Guid userId, string deviceIdentifier)
{
if(templateBody == null)
{
return;
}
var fullTemplateId = $"template:{templateId}";
var template = new InstallationTemplate
{
Body = templateBody,
Tags = new List<string>
{
fullTemplateId,
$"{fullTemplateId}_userId:{userId}"
}
};
if(!string.IsNullOrWhiteSpace(deviceIdentifier))
{
template.Tags.Add($"{fullTemplateId}_deviceIdentifier:{deviceIdentifier}");
}
installation.Templates.Add(fullTemplateId, template);
}
public async Task DeleteRegistrationAsync(Guid deviceId)
{
await _client.DeleteInstallationAsync(deviceId.ToString());
}
public async Task AddUserRegistrationOrganizationAsync(Guid userId, Guid organizationId)
{
await PatchTagsForUserDevicesAsync(userId, UpdateOperationType.Add, $"organizationId:{organizationId}");
}
public async Task DeleteUserRegistrationOrganizationAsync(Guid userId, Guid organizationId)
{
await PatchTagsForUserDevicesAsync(userId, UpdateOperationType.Remove, $"organizationId:{organizationId}");
}
private async Task PatchTagsForUserDevicesAsync(Guid userId, UpdateOperationType op, string tag)
{
var devices = await _deviceRepository.GetManyByUserIdAsync(userId);
var operation = new PartialUpdateOperation
{
Operation = op,
Path = "/tags",
Value = tag
};
foreach(var device in devices)
{
await _client.PatchInstallationAsync(device.Id.ToString(), new List<PartialUpdateOperation> { operation });
}
}
}
}

View File

@ -24,7 +24,8 @@ namespace Bit.Core.Services
private readonly IGroupRepository _groupRepository;
private readonly IDataProtector _dataProtector;
private readonly IMailService _mailService;
private readonly IPushNotificationService _pushService;
private readonly IPushNotificationService _pushNotificationService;
private readonly IPushRegistrationService _pushRegistrationService;
public OrganizationService(
IOrganizationRepository organizationRepository,
@ -34,7 +35,8 @@ namespace Bit.Core.Services
IGroupRepository groupRepository,
IDataProtectionProvider dataProtectionProvider,
IMailService mailService,
IPushNotificationService pushService)
IPushNotificationService pushNotificationService,
IPushRegistrationService pushRegistrationService)
{
_organizationRepository = organizationRepository;
_organizationUserRepository = organizationUserRepository;
@ -43,7 +45,8 @@ namespace Bit.Core.Services
_groupRepository = groupRepository;
_dataProtector = dataProtectionProvider.CreateProtector("OrganizationServiceDataProtector");
_mailService = mailService;
_pushService = pushService;
_pushNotificationService = pushNotificationService;
_pushRegistrationService = pushRegistrationService;
}
public async Task<OrganizationBilling> GetBillingAsync(Organization organization)
{
@ -609,7 +612,9 @@ namespace Bit.Core.Services
await _organizationUserRepository.CreateAsync(orgUser);
// push
await _pushService.PushSyncOrgKeysAsync(signup.Owner.Id);
await _pushRegistrationService.AddUserRegistrationOrganizationAsync(orgUser.UserId.Value,
organization.Id);
await _pushNotificationService.PushSyncOrgKeysAsync(signup.Owner.Id);
return new Tuple<Organization, OrganizationUser>(organization, orgUser);
}
@ -868,7 +873,8 @@ namespace Bit.Core.Services
}
// push
await _pushService.PushSyncOrgKeysAsync(orgUser.UserId.Value);
await _pushRegistrationService.AddUserRegistrationOrganizationAsync(orgUser.UserId.Value, organizationId);
await _pushNotificationService.PushSyncOrgKeysAsync(orgUser.UserId.Value);
return orgUser;
}
@ -915,6 +921,9 @@ namespace Bit.Core.Services
}
await _organizationUserRepository.DeleteAsync(orgUser);
// push
await _pushRegistrationService.DeleteUserRegistrationOrganizationAsync(orgUser.UserId.Value, organizationId);
}
public async Task DeleteUserAsync(Guid organizationId, Guid userId)
@ -932,6 +941,9 @@ namespace Bit.Core.Services
}
await _organizationUserRepository.DeleteAsync(orgUser);
// push
await _pushRegistrationService.DeleteUserRegistrationOrganizationAsync(orgUser.UserId.Value, organizationId);
}
public async Task ImportAsync(Guid organizationId,

View File

@ -6,6 +6,11 @@ namespace Bit.Core.Services
{
public class NoopPushRegistrationService : IPushRegistrationService
{
public Task AddUserRegistrationOrganizationAsync(Guid userId, Guid organizationId)
{
return Task.FromResult(0);
}
public Task CreateOrUpdateRegistrationAsync(Device device)
{
return Task.FromResult(0);
@ -15,5 +20,10 @@ namespace Bit.Core.Services
{
return Task.FromResult(0);
}
public Task DeleteUserRegistrationOrganizationAsync(Guid userId, Guid organizationId)
{
return Task.FromResult(0);
}
}
}

View File

@ -50,7 +50,7 @@ namespace Bit.Core.Utilities
public static void AddDefaultServices(this IServiceCollection services)
{
services.AddSingleton<IMailService, SendGridMailService>();
services.AddSingleton<IPushNotificationService, PushSharpPushNotificationService>();
services.AddSingleton<IPushNotificationService, NotificationHubPushNotificationService>();
services.AddSingleton<IBlockIpService, AzureQueueBlockIpService>();
services.AddSingleton<IPushRegistrationService, NotificationHubPushRegistrationService>();
}