From 5af974d541a8c12cc292fc327ccb4585c25bb81e Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 26 May 2017 22:52:50 -0400 Subject: [PATCH] notification hub services --- src/Core/Services/IPushRegistrationService.cs | 2 + .../NotificationHubPushNotificationService.cs | 183 ++++++++++++++++++ .../NotificationHubPushRegistrationService.cs | 90 ++++++++- .../Implementations/OrganizationService.cs | 22 ++- .../NoopPushRegistrationService.cs | 10 + .../Utilities/ServiceCollectionExtensions.cs | 2 +- 6 files changed, 298 insertions(+), 11 deletions(-) create mode 100644 src/Core/Services/Implementations/NotificationHubPushNotificationService.cs diff --git a/src/Core/Services/IPushRegistrationService.cs b/src/Core/Services/IPushRegistrationService.cs index c0755fae23..38282bccf0 100644 --- a/src/Core/Services/IPushRegistrationService.cs +++ b/src/Core/Services/IPushRegistrationService.cs @@ -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); } } diff --git a/src/Core/Services/Implementations/NotificationHubPushNotificationService.cs b/src/Core/Services/Implementations/NotificationHubPushNotificationService.cs new file mode 100644 index 0000000000..c509259705 --- /dev/null +++ b/src/Core/Services/Implementations/NotificationHubPushNotificationService.cs @@ -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 + { + { "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; } + } + } +} diff --git a/src/Core/Services/Implementations/NotificationHubPushRegistrationService.cs b/src/Core/Services/Implementations/NotificationHubPushRegistrationService.cs index 1010636d4f..0387157e7d 100644 --- a/src/Core/Services/Implementations/NotificationHubPushRegistrationService.cs +++ b/src/Core/Services/Implementations/NotificationHubPushRegistrationService.cs @@ -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() }; installation.Tags = new List { - "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 + { + 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 { operation }); + } + } } } diff --git a/src/Core/Services/Implementations/OrganizationService.cs b/src/Core/Services/Implementations/OrganizationService.cs index 961c36885f..826cbae0e6 100644 --- a/src/Core/Services/Implementations/OrganizationService.cs +++ b/src/Core/Services/Implementations/OrganizationService.cs @@ -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 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, 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, diff --git a/src/Core/Services/NoopImplementations/NoopPushRegistrationService.cs b/src/Core/Services/NoopImplementations/NoopPushRegistrationService.cs index c46677137f..79f85e8bcd 100644 --- a/src/Core/Services/NoopImplementations/NoopPushRegistrationService.cs +++ b/src/Core/Services/NoopImplementations/NoopPushRegistrationService.cs @@ -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); + } } } diff --git a/src/Core/Utilities/ServiceCollectionExtensions.cs b/src/Core/Utilities/ServiceCollectionExtensions.cs index ff955f71ba..2c7f0945c3 100644 --- a/src/Core/Utilities/ServiceCollectionExtensions.cs +++ b/src/Core/Utilities/ServiceCollectionExtensions.cs @@ -50,7 +50,7 @@ namespace Bit.Core.Utilities public static void AddDefaultServices(this IServiceCollection services) { services.AddSingleton(); - services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); }