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:
parent
887fe4fc05
commit
5af974d541
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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; }
|
||||
}
|
||||
}
|
||||
}
|
@ -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 });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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>();
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user