1
0
mirror of https://github.com/bitwarden/server.git synced 2025-06-30 07:36:14 -05:00

[PM-10600] Push notification creation to affected clients (#4923)

* PM-10600: Notification push notification

* PM-10600: Sending to specific client types for relay push notifications

* PM-10600: Sending to specific client types for other clients

* PM-10600: Send push notification on notification creation

* PM-10600: Explicit group names

* PM-10600: Id typos

* PM-10600: Revert global push notifications

* PM-10600: Added DeviceType claim

* PM-10600: Sent to organization typo

* PM-10600: UT coverage

* PM-10600: Small refactor, UTs coverage

* PM-10600: UTs coverage

* PM-10600: Startup fix

* PM-10600: Test fix

* PM-10600: Required attribute, organization group for push notification fix

* PM-10600: UT coverage

* PM-10600: Fix Mobile devices not registering to organization push notifications

We only register devices for organization push notifications when the organization is being created. This does not work, since we have a use case (Notification Center) of delivering notifications to all users of organization. This fixes it, by adding the organization id tag when device registers for push notifications.

* PM-10600: Unit Test coverage for NotificationHubPushRegistrationService

Fixed IFeatureService substitute mocking for Android tests.
Added user part of organization test with organizationId tags expectation.

* PM-10600: Unit Tests fix to NotificationHubPushRegistrationService after merge conflict

* PM-10600: Organization push notifications not sending to mobile device from self-hosted.

Self-hosted instance uses relay to register the mobile device against Bitwarden Cloud Api. Only the self-hosted server knows client's organization membership, which means it needs to pass in the organization id's information to the relay. Similarly, for Bitwarden Cloud, the organizaton id will come directly from the server.

* PM-10600: Fix self-hosted organization notification not being received by mobile device.

When mobile device registers on self-hosted through the relay, every single id, like user id, device id and now organization id needs to be prefixed with the installation id. This have been missing in the PushController that handles this for organization id.

* PM-10600: Broken NotificationsController integration test

Device type is now part of JWT access token, so the notification center results in the integration test are now scoped to client type web and all.

* PM-10600: Merge conflicts fix

* merge conflict fix
This commit is contained in:
Maciej Zieniuk
2025-02-12 16:46:30 +01:00
committed by GitHub
parent 9f5134e070
commit ae9bb427a1
37 changed files with 1187 additions and 335 deletions

View File

@ -43,7 +43,7 @@ public class PushController : Controller
{
CheckUsage();
await _pushRegistrationService.CreateOrUpdateRegistrationAsync(model.PushToken, Prefix(model.DeviceId),
Prefix(model.UserId), Prefix(model.Identifier), model.Type);
Prefix(model.UserId), Prefix(model.Identifier), model.Type, model.OrganizationIds.Select(Prefix));
}
[HttpPost("delete")]
@ -79,12 +79,12 @@ public class PushController : Controller
if (!string.IsNullOrWhiteSpace(model.UserId))
{
await _pushNotificationService.SendPayloadToUserAsync(Prefix(model.UserId),
model.Type.Value, model.Payload, Prefix(model.Identifier), Prefix(model.DeviceId));
model.Type, model.Payload, Prefix(model.Identifier), Prefix(model.DeviceId), model.ClientType);
}
else if (!string.IsNullOrWhiteSpace(model.OrganizationId))
{
await _pushNotificationService.SendPayloadToOrganizationAsync(Prefix(model.OrganizationId),
model.Type.Value, model.Payload, Prefix(model.Identifier), Prefix(model.DeviceId));
model.Type, model.Payload, Prefix(model.Identifier), Prefix(model.DeviceId), model.ClientType);
}
}

View File

@ -169,6 +169,11 @@ public class CurrentContext : ICurrentContext
DeviceIdentifier = GetClaimValue(claimsDict, Claims.Device);
if (Enum.TryParse(GetClaimValue(claimsDict, Claims.DeviceType), out DeviceType deviceType))
{
DeviceType = deviceType;
}
Organizations = GetOrganizations(claimsDict, orgApi);
Providers = GetProviders(claimsDict);

View File

@ -27,4 +27,6 @@ public enum PushType : byte
SyncOrganizations = 17,
SyncOrganizationStatusChanged = 18,
SyncOrganizationCollectionSettingChanged = 19,
SyncNotification = 20,
}

View File

@ -6,6 +6,7 @@ public static class Claims
public const string SecurityStamp = "sstamp";
public const string Premium = "premium";
public const string Device = "device";
public const string DeviceType = "devicetype";
public const string OrganizationOwner = "orgowner";
public const string OrganizationAdmin = "orgadmin";

View File

@ -15,4 +15,5 @@ public class PushRegistrationRequestModel
public DeviceType Type { get; set; }
[Required]
public string Identifier { get; set; }
public IEnumerable<string> OrganizationIds { get; set; }
}

View File

@ -1,18 +1,18 @@
using System.ComponentModel.DataAnnotations;
#nullable enable
using System.ComponentModel.DataAnnotations;
using Bit.Core.Enums;
namespace Bit.Core.Models.Api;
public class PushSendRequestModel : IValidatableObject
{
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; }
[Required]
public object Payload { get; set; }
public string? UserId { get; set; }
public string? OrganizationId { get; set; }
public string? DeviceId { get; set; }
public string? Identifier { get; set; }
public required PushType Type { get; set; }
public required object Payload { get; set; }
public ClientType? ClientType { get; set; }
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{

View File

@ -45,6 +45,15 @@ public class SyncSendPushNotification
public DateTime RevisionDate { get; set; }
}
public class SyncNotificationPushNotification
{
public Guid Id { get; set; }
public Guid? UserId { get; set; }
public Guid? OrganizationId { get; set; }
public ClientType ClientType { get; set; }
public DateTime RevisionDate { get; set; }
}
public class AuthRequestPushNotification
{
public Guid UserId { get; set; }

View File

@ -4,6 +4,7 @@ using Bit.Core.NotificationCenter.Authorization;
using Bit.Core.NotificationCenter.Commands.Interfaces;
using Bit.Core.NotificationCenter.Entities;
using Bit.Core.NotificationCenter.Repositories;
using Bit.Core.Platform.Push;
using Bit.Core.Utilities;
using Microsoft.AspNetCore.Authorization;
@ -14,14 +15,17 @@ public class CreateNotificationCommand : ICreateNotificationCommand
private readonly ICurrentContext _currentContext;
private readonly IAuthorizationService _authorizationService;
private readonly INotificationRepository _notificationRepository;
private readonly IPushNotificationService _pushNotificationService;
public CreateNotificationCommand(ICurrentContext currentContext,
IAuthorizationService authorizationService,
INotificationRepository notificationRepository)
INotificationRepository notificationRepository,
IPushNotificationService pushNotificationService)
{
_currentContext = currentContext;
_authorizationService = authorizationService;
_notificationRepository = notificationRepository;
_pushNotificationService = pushNotificationService;
}
public async Task<Notification> CreateAsync(Notification notification)
@ -31,6 +35,10 @@ public class CreateNotificationCommand : ICreateNotificationCommand
await _authorizationService.AuthorizeOrThrowAsync(_currentContext.HttpContext.User, notification,
NotificationOperations.Create);
return await _notificationRepository.CreateAsync(notification);
var newNotification = await _notificationRepository.CreateAsync(notification);
await _pushNotificationService.PushSyncNotificationAsync(newNotification);
return newNotification;
}
}

View File

@ -4,6 +4,6 @@ namespace Bit.Core.NotificationHub;
public interface INotificationHubPool
{
NotificationHubClient ClientFor(Guid comb);
INotificationHubClient ClientFor(Guid comb);
INotificationHubProxy AllClients { get; }
}

View File

@ -43,7 +43,7 @@ public class NotificationHubPool : INotificationHubPool
/// <param name="comb"></param>
/// <returns></returns>
/// <exception cref="InvalidOperationException">Thrown when no notification hub is found for a given comb.</exception>
public NotificationHubClient ClientFor(Guid comb)
public INotificationHubClient ClientFor(Guid comb)
{
var possibleConnections = _connections.Where(c => c.RegistrationEnabled(comb)).ToArray();
if (possibleConnections.Length == 0)

View File

@ -12,6 +12,7 @@ using Bit.Core.Tools.Entities;
using Bit.Core.Vault.Entities;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Notification = Bit.Core.NotificationCenter.Entities.Notification;
namespace Bit.Core.NotificationHub;
@ -135,11 +136,7 @@ public class NotificationHubPushNotificationService : IPushNotificationService
private async Task PushUserAsync(Guid userId, PushType type, bool excludeCurrentContext = false)
{
var message = new UserPushNotification
{
UserId = userId,
Date = DateTime.UtcNow
};
var message = new UserPushNotification { UserId = userId, Date = DateTime.UtcNow };
await SendPayloadToUserAsync(userId, type, message, excludeCurrentContext);
}
@ -184,31 +181,54 @@ public class NotificationHubPushNotificationService : IPushNotificationService
await PushAuthRequestAsync(authRequest, PushType.AuthRequestResponse);
}
public async Task PushSyncNotificationAsync(Notification notification)
{
var message = new SyncNotificationPushNotification
{
Id = notification.Id,
UserId = notification.UserId,
OrganizationId = notification.OrganizationId,
ClientType = notification.ClientType,
RevisionDate = notification.RevisionDate
};
if (notification.UserId.HasValue)
{
await SendPayloadToUserAsync(notification.UserId.Value, PushType.SyncNotification, message, true,
notification.ClientType);
}
else if (notification.OrganizationId.HasValue)
{
await SendPayloadToOrganizationAsync(notification.OrganizationId.Value, PushType.SyncNotification, message,
true, notification.ClientType);
}
}
private async Task PushAuthRequestAsync(AuthRequest authRequest, PushType type)
{
var message = new AuthRequestPushNotification
{
Id = authRequest.Id,
UserId = authRequest.UserId
};
var message = new AuthRequestPushNotification { Id = authRequest.Id, UserId = authRequest.UserId };
await SendPayloadToUserAsync(authRequest.UserId, type, message, true);
}
private async Task SendPayloadToUserAsync(Guid userId, PushType type, object payload, bool excludeCurrentContext)
private async Task SendPayloadToUserAsync(Guid userId, PushType type, object payload, bool excludeCurrentContext,
ClientType? clientType = null)
{
await SendPayloadToUserAsync(userId.ToString(), type, payload, GetContextIdentifier(excludeCurrentContext));
await SendPayloadToUserAsync(userId.ToString(), type, payload, GetContextIdentifier(excludeCurrentContext),
clientType: clientType);
}
private async Task SendPayloadToOrganizationAsync(Guid orgId, PushType type, object payload, bool excludeCurrentContext)
private async Task SendPayloadToOrganizationAsync(Guid orgId, PushType type, object payload,
bool excludeCurrentContext, ClientType? clientType = null)
{
await SendPayloadToUserAsync(orgId.ToString(), type, payload, GetContextIdentifier(excludeCurrentContext));
await SendPayloadToOrganizationAsync(orgId.ToString(), type, payload,
GetContextIdentifier(excludeCurrentContext), clientType: clientType);
}
public async Task SendPayloadToUserAsync(string userId, PushType type, object payload, string identifier,
string deviceId = null)
string deviceId = null, ClientType? clientType = null)
{
var tag = BuildTag($"template:payload_userId:{SanitizeTagInput(userId)}", identifier);
var tag = BuildTag($"template:payload_userId:{SanitizeTagInput(userId)}", identifier, clientType);
await SendPayloadAsync(tag, type, payload);
if (InstallationDeviceEntity.IsInstallationDeviceId(deviceId))
{
@ -217,9 +237,9 @@ public class NotificationHubPushNotificationService : IPushNotificationService
}
public async Task SendPayloadToOrganizationAsync(string orgId, PushType type, object payload, string identifier,
string deviceId = null)
string deviceId = null, ClientType? clientType = null)
{
var tag = BuildTag($"template:payload && organizationId:{SanitizeTagInput(orgId)}", identifier);
var tag = BuildTag($"template:payload && organizationId:{SanitizeTagInput(orgId)}", identifier, clientType);
await SendPayloadAsync(tag, type, payload);
if (InstallationDeviceEntity.IsInstallationDeviceId(deviceId))
{
@ -259,18 +279,23 @@ public class NotificationHubPushNotificationService : IPushNotificationService
return null;
}
var currentContext = _httpContextAccessor?.HttpContext?.
RequestServices.GetService(typeof(ICurrentContext)) as ICurrentContext;
var currentContext =
_httpContextAccessor?.HttpContext?.RequestServices.GetService(typeof(ICurrentContext)) as ICurrentContext;
return currentContext?.DeviceIdentifier;
}
private string BuildTag(string tag, string identifier)
private string BuildTag(string tag, string identifier, ClientType? clientType)
{
if (!string.IsNullOrWhiteSpace(identifier))
{
tag += $" && !deviceIdentifier:{SanitizeTagInput(identifier)}";
}
if (clientType.HasValue && clientType.Value != ClientType.All)
{
tag += $" && clientType:{clientType}";
}
return $"({tag})";
}
@ -279,8 +304,7 @@ public class NotificationHubPushNotificationService : IPushNotificationService
var results = await _notificationHubPool.AllClients.SendTemplateNotificationAsync(
new Dictionary<string, string>
{
{ "type", ((byte)type).ToString() },
{ "payload", JsonSerializer.Serialize(payload) }
{ "type", ((byte)type).ToString() }, { "payload", JsonSerializer.Serialize(payload) }
}, tag);
if (_enableTracing)
@ -291,7 +315,9 @@ public class NotificationHubPushNotificationService : IPushNotificationService
{
continue;
}
_logger.LogInformation("Azure Notification Hub Tracking ID: {Id} | {Type} push notification with {Success} successes and {Failure} failures with a payload of {@Payload} and result of {@Results}",
_logger.LogInformation(
"Azure Notification Hub Tracking ID: {Id} | {Type} push notification with {Success} successes and {Failure} failures with a payload of {@Payload} and result of {@Results}",
outcome.TrackingId, type, outcome.Success, outcome.Failure, payload, outcome.Results);
}
}

View File

@ -2,36 +2,26 @@
using Bit.Core.Models.Data;
using Bit.Core.Platform.Push;
using Bit.Core.Repositories;
using Bit.Core.Settings;
using Bit.Core.Utilities;
using Microsoft.Azure.NotificationHubs;
using Microsoft.Extensions.Logging;
namespace Bit.Core.NotificationHub;
public class NotificationHubPushRegistrationService : IPushRegistrationService
{
private readonly IInstallationDeviceRepository _installationDeviceRepository;
private readonly GlobalSettings _globalSettings;
private readonly INotificationHubPool _notificationHubPool;
private readonly IServiceProvider _serviceProvider;
private readonly ILogger<NotificationHubPushRegistrationService> _logger;
public NotificationHubPushRegistrationService(
IInstallationDeviceRepository installationDeviceRepository,
GlobalSettings globalSettings,
INotificationHubPool notificationHubPool,
IServiceProvider serviceProvider,
ILogger<NotificationHubPushRegistrationService> logger)
INotificationHubPool notificationHubPool)
{
_installationDeviceRepository = installationDeviceRepository;
_globalSettings = globalSettings;
_notificationHubPool = notificationHubPool;
_serviceProvider = serviceProvider;
_logger = logger;
}
public async Task CreateOrUpdateRegistrationAsync(string pushToken, string deviceId, string userId,
string identifier, DeviceType type)
string identifier, DeviceType type, IEnumerable<string> organizationIds)
{
if (string.IsNullOrWhiteSpace(pushToken))
{
@ -45,16 +35,21 @@ public class NotificationHubPushRegistrationService : IPushRegistrationService
Templates = new Dictionary<string, InstallationTemplate>()
};
installation.Tags = new List<string>
{
$"userId:{userId}"
};
var clientType = DeviceTypes.ToClientType(type);
installation.Tags = new List<string> { $"userId:{userId}", $"clientType:{clientType}" };
if (!string.IsNullOrWhiteSpace(identifier))
{
installation.Tags.Add("deviceIdentifier:" + identifier);
}
var organizationIdsList = organizationIds.ToList();
foreach (var organizationId in organizationIdsList)
{
installation.Tags.Add($"organizationId:{organizationId}");
}
string payloadTemplate = null, messageTemplate = null, badgeMessageTemplate = null;
switch (type)
{
@ -84,10 +79,12 @@ public class NotificationHubPushRegistrationService : IPushRegistrationService
break;
}
BuildInstallationTemplate(installation, "payload", payloadTemplate, userId, identifier);
BuildInstallationTemplate(installation, "message", messageTemplate, userId, identifier);
BuildInstallationTemplate(installation, "payload", payloadTemplate, userId, identifier, clientType,
organizationIdsList);
BuildInstallationTemplate(installation, "message", messageTemplate, userId, identifier, clientType,
organizationIdsList);
BuildInstallationTemplate(installation, "badgeMessage", badgeMessageTemplate ?? messageTemplate,
userId, identifier);
userId, identifier, clientType, organizationIdsList);
await ClientFor(GetComb(deviceId)).CreateOrUpdateInstallationAsync(installation);
if (InstallationDeviceEntity.IsInstallationDeviceId(deviceId))
@ -97,7 +94,7 @@ public class NotificationHubPushRegistrationService : IPushRegistrationService
}
private void BuildInstallationTemplate(Installation installation, string templateId, string templateBody,
string userId, string identifier)
string userId, string identifier, ClientType clientType, List<string> organizationIds)
{
if (templateBody == null)
{
@ -111,8 +108,7 @@ public class NotificationHubPushRegistrationService : IPushRegistrationService
Body = templateBody,
Tags = new List<string>
{
fullTemplateId,
$"{fullTemplateId}_userId:{userId}"
fullTemplateId, $"{fullTemplateId}_userId:{userId}", $"clientType:{clientType}"
}
};
@ -121,6 +117,11 @@ public class NotificationHubPushRegistrationService : IPushRegistrationService
template.Tags.Add($"{fullTemplateId}_deviceIdentifier:{identifier}");
}
foreach (var organizationId in organizationIds)
{
template.Tags.Add($"organizationId:{organizationId}");
}
installation.Templates.Add(fullTemplateId, template);
}
@ -197,7 +198,7 @@ public class NotificationHubPushRegistrationService : IPushRegistrationService
}
}
private NotificationHubClient ClientFor(Guid deviceId)
private INotificationHubClient ClientFor(Guid deviceId)
{
return _notificationHubPool.ClientFor(deviceId);
}

View File

@ -5,26 +5,25 @@ using Bit.Core.Auth.Entities;
using Bit.Core.Context;
using Bit.Core.Enums;
using Bit.Core.Models;
using Bit.Core.Settings;
using Bit.Core.NotificationCenter.Entities;
using Bit.Core.Tools.Entities;
using Bit.Core.Utilities;
using Bit.Core.Vault.Entities;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
namespace Bit.Core.Platform.Push.Internal;
public class AzureQueuePushNotificationService : IPushNotificationService
{
private readonly QueueClient _queueClient;
private readonly GlobalSettings _globalSettings;
private readonly IHttpContextAccessor _httpContextAccessor;
public AzureQueuePushNotificationService(
GlobalSettings globalSettings,
[FromKeyedServices("notifications")] QueueClient queueClient,
IHttpContextAccessor httpContextAccessor)
{
_queueClient = new QueueClient(globalSettings.Notifications.ConnectionString, "notifications");
_globalSettings = globalSettings;
_queueClient = queueClient;
_httpContextAccessor = httpContextAccessor;
}
@ -129,11 +128,7 @@ public class AzureQueuePushNotificationService : IPushNotificationService
private async Task PushUserAsync(Guid userId, PushType type, bool excludeCurrentContext = false)
{
var message = new UserPushNotification
{
UserId = userId,
Date = DateTime.UtcNow
};
var message = new UserPushNotification { UserId = userId, Date = DateTime.UtcNow };
await SendMessageAsync(type, message, excludeCurrentContext);
}
@ -150,11 +145,7 @@ public class AzureQueuePushNotificationService : IPushNotificationService
private async Task PushAuthRequestAsync(AuthRequest authRequest, PushType type)
{
var message = new AuthRequestPushNotification
{
Id = authRequest.Id,
UserId = authRequest.UserId
};
var message = new AuthRequestPushNotification { Id = authRequest.Id, UserId = authRequest.UserId };
await SendMessageAsync(type, message, true);
}
@ -174,6 +165,20 @@ public class AzureQueuePushNotificationService : IPushNotificationService
await PushSendAsync(send, PushType.SyncSendDelete);
}
public async Task PushSyncNotificationAsync(Notification notification)
{
var message = new SyncNotificationPushNotification
{
Id = notification.Id,
UserId = notification.UserId,
OrganizationId = notification.OrganizationId,
ClientType = notification.ClientType,
RevisionDate = notification.RevisionDate
};
await SendMessageAsync(PushType.SyncNotification, message, true);
}
private async Task PushSendAsync(Send send, PushType type)
{
if (send.UserId.HasValue)
@ -204,20 +209,20 @@ public class AzureQueuePushNotificationService : IPushNotificationService
return null;
}
var currentContext = _httpContextAccessor?.HttpContext?.
RequestServices.GetService(typeof(ICurrentContext)) as ICurrentContext;
var currentContext =
_httpContextAccessor?.HttpContext?.RequestServices.GetService(typeof(ICurrentContext)) as ICurrentContext;
return currentContext?.DeviceIdentifier;
}
public Task SendPayloadToUserAsync(string userId, PushType type, object payload, string identifier,
string deviceId = null)
string deviceId = null, ClientType? clientType = null)
{
// Noop
return Task.FromResult(0);
}
public Task SendPayloadToOrganizationAsync(string orgId, PushType type, object payload, string identifier,
string deviceId = null)
string deviceId = null, ClientType? clientType = null)
{
// Noop
return Task.FromResult(0);

View File

@ -1,6 +1,7 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.Auth.Entities;
using Bit.Core.Enums;
using Bit.Core.NotificationCenter.Entities;
using Bit.Core.Tools.Entities;
using Bit.Core.Vault.Entities;
@ -23,11 +24,13 @@ public interface IPushNotificationService
Task PushSyncSendCreateAsync(Send send);
Task PushSyncSendUpdateAsync(Send send);
Task PushSyncSendDeleteAsync(Send send);
Task PushSyncNotificationAsync(Notification notification);
Task PushAuthRequestAsync(AuthRequest authRequest);
Task PushAuthRequestResponseAsync(AuthRequest authRequest);
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);
Task PushSyncOrganizationStatusAsync(Organization organization);
Task PushSyncOrganizationCollectionManagementSettingsAsync(Organization organization);
Task SendPayloadToUserAsync(string userId, PushType type, object payload, string identifier,
string deviceId = null, ClientType? clientType = null);
Task SendPayloadToOrganizationAsync(string orgId, PushType type, object payload, string identifier,
string deviceId = null, ClientType? clientType = null);
}

View File

@ -5,7 +5,7 @@ namespace Bit.Core.Platform.Push;
public interface IPushRegistrationService
{
Task CreateOrUpdateRegistrationAsync(string pushToken, string deviceId, string userId,
string identifier, DeviceType type);
string identifier, DeviceType type, IEnumerable<string> organizationIds);
Task DeleteRegistrationAsync(string deviceId);
Task AddUserRegistrationOrganizationAsync(IEnumerable<string> deviceIds, string organizationId);
Task DeleteUserRegistrationOrganizationAsync(IEnumerable<string> deviceIds, string organizationId);

View File

@ -1,6 +1,7 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.Auth.Entities;
using Bit.Core.Enums;
using Bit.Core.NotificationCenter.Entities;
using Bit.Core.Settings;
using Bit.Core.Tools.Entities;
using Bit.Core.Vault.Entities;
@ -131,20 +132,6 @@ public class MultiServicePushNotificationService : IPushNotificationService
return Task.FromResult(0);
}
public Task SendPayloadToUserAsync(string userId, PushType type, object payload, string identifier,
string deviceId = null)
{
PushToServices((s) => s.SendPayloadToUserAsync(userId, type, payload, identifier, deviceId));
return Task.FromResult(0);
}
public Task SendPayloadToOrganizationAsync(string orgId, PushType type, object payload, string identifier,
string deviceId = null)
{
PushToServices((s) => s.SendPayloadToOrganizationAsync(orgId, type, payload, identifier, deviceId));
return Task.FromResult(0);
}
public Task PushSyncOrganizationStatusAsync(Organization organization)
{
PushToServices((s) => s.PushSyncOrganizationStatusAsync(organization));
@ -157,6 +144,26 @@ public class MultiServicePushNotificationService : IPushNotificationService
return Task.CompletedTask;
}
public Task PushSyncNotificationAsync(Notification notification)
{
PushToServices((s) => s.PushSyncNotificationAsync(notification));
return Task.CompletedTask;
}
public Task SendPayloadToUserAsync(string userId, PushType type, object payload, string identifier,
string deviceId = null, ClientType? clientType = null)
{
PushToServices((s) => s.SendPayloadToUserAsync(userId, type, payload, identifier, deviceId, clientType));
return Task.FromResult(0);
}
public Task SendPayloadToOrganizationAsync(string orgId, PushType type, object payload, string identifier,
string deviceId = null, ClientType? clientType = null)
{
PushToServices((s) => s.SendPayloadToOrganizationAsync(orgId, type, payload, identifier, deviceId, clientType));
return Task.FromResult(0);
}
private void PushToServices(Func<IPushNotificationService, Task> pushFunc)
{
if (_services != null)

View File

@ -1,6 +1,7 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.Auth.Entities;
using Bit.Core.Enums;
using Bit.Core.NotificationCenter.Entities;
using Bit.Core.Tools.Entities;
using Bit.Core.Vault.Entities;
@ -84,7 +85,7 @@ public class NoopPushNotificationService : IPushNotificationService
}
public Task SendPayloadToOrganizationAsync(string orgId, PushType type, object payload, string identifier,
string deviceId = null)
string deviceId = null, ClientType? clientType = null)
{
return Task.FromResult(0);
}
@ -107,8 +108,10 @@ public class NoopPushNotificationService : IPushNotificationService
}
public Task SendPayloadToUserAsync(string userId, PushType type, object payload, string identifier,
string deviceId = null)
string deviceId = null, ClientType? clientType = null)
{
return Task.FromResult(0);
}
public Task PushSyncNotificationAsync(Notification notification) => Task.CompletedTask;
}

View File

@ -10,7 +10,7 @@ public class NoopPushRegistrationService : IPushRegistrationService
}
public Task CreateOrUpdateRegistrationAsync(string pushToken, string deviceId, string userId,
string identifier, DeviceType type)
string identifier, DeviceType type, IEnumerable<string> organizationIds)
{
return Task.FromResult(0);
}

View File

@ -3,6 +3,7 @@ using Bit.Core.Auth.Entities;
using Bit.Core.Context;
using Bit.Core.Enums;
using Bit.Core.Models;
using Bit.Core.NotificationCenter.Entities;
using Bit.Core.Services;
using Bit.Core.Settings;
using Bit.Core.Tools.Entities;
@ -183,6 +184,20 @@ public class NotificationsApiPushNotificationService : BaseIdentityClientService
await PushSendAsync(send, PushType.SyncSendDelete);
}
public async Task PushSyncNotificationAsync(Notification notification)
{
var message = new SyncNotificationPushNotification
{
Id = notification.Id,
UserId = notification.UserId,
OrganizationId = notification.OrganizationId,
ClientType = notification.ClientType,
RevisionDate = notification.RevisionDate
};
await SendMessageAsync(PushType.SyncNotification, message, true);
}
private async Task PushSendAsync(Send send, PushType type)
{
if (send.UserId.HasValue)
@ -212,20 +227,20 @@ public class NotificationsApiPushNotificationService : BaseIdentityClientService
return null;
}
var currentContext = _httpContextAccessor?.HttpContext?.
RequestServices.GetService(typeof(ICurrentContext)) as ICurrentContext;
var currentContext =
_httpContextAccessor?.HttpContext?.RequestServices.GetService(typeof(ICurrentContext)) as ICurrentContext;
return currentContext?.DeviceIdentifier;
}
public Task SendPayloadToUserAsync(string userId, PushType type, object payload, string identifier,
string deviceId = null)
string deviceId = null, ClientType? clientType = null)
{
// Noop
return Task.FromResult(0);
}
public Task SendPayloadToOrganizationAsync(string orgId, PushType type, object payload, string identifier,
string deviceId = null)
string deviceId = null, ClientType? clientType = null)
{
// Noop
return Task.FromResult(0);

View File

@ -5,6 +5,7 @@ using Bit.Core.Enums;
using Bit.Core.IdentityServer;
using Bit.Core.Models;
using Bit.Core.Models.Api;
using Bit.Core.NotificationCenter.Entities;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Settings;
@ -138,11 +139,7 @@ public class RelayPushNotificationService : BaseIdentityClientService, IPushNoti
private async Task PushUserAsync(Guid userId, PushType type, bool excludeCurrentContext = false)
{
var message = new UserPushNotification
{
UserId = userId,
Date = DateTime.UtcNow
};
var message = new UserPushNotification { UserId = userId, Date = DateTime.UtcNow };
await SendPayloadToUserAsync(userId, type, message, excludeCurrentContext);
}
@ -189,69 +186,32 @@ public class RelayPushNotificationService : BaseIdentityClientService, IPushNoti
private async Task PushAuthRequestAsync(AuthRequest authRequest, PushType type)
{
var message = new AuthRequestPushNotification
{
Id = authRequest.Id,
UserId = authRequest.UserId
};
var message = new AuthRequestPushNotification { Id = authRequest.Id, UserId = authRequest.UserId };
await SendPayloadToUserAsync(authRequest.UserId, type, message, true);
}
private async Task SendPayloadToUserAsync(Guid userId, PushType type, object payload, bool excludeCurrentContext)
public async Task PushSyncNotificationAsync(Notification notification)
{
var request = new PushSendRequestModel
var message = new SyncNotificationPushNotification
{
UserId = userId.ToString(),
Type = type,
Payload = payload
Id = notification.Id,
UserId = notification.UserId,
OrganizationId = notification.OrganizationId,
ClientType = notification.ClientType,
RevisionDate = notification.RevisionDate
};
await AddCurrentContextAsync(request, excludeCurrentContext);
await SendAsync(HttpMethod.Post, "push/send", request);
}
private async Task SendPayloadToOrganizationAsync(Guid orgId, PushType type, object payload, bool excludeCurrentContext)
{
var request = new PushSendRequestModel
if (notification.UserId.HasValue)
{
OrganizationId = orgId.ToString(),
Type = type,
Payload = payload
};
await AddCurrentContextAsync(request, excludeCurrentContext);
await SendAsync(HttpMethod.Post, "push/send", request);
}
private async Task AddCurrentContextAsync(PushSendRequestModel request, bool addIdentifier)
{
var currentContext = _httpContextAccessor?.HttpContext?.
RequestServices.GetService(typeof(ICurrentContext)) as ICurrentContext;
if (!string.IsNullOrWhiteSpace(currentContext?.DeviceIdentifier))
{
var device = await _deviceRepository.GetByIdentifierAsync(currentContext.DeviceIdentifier);
if (device != null)
{
request.DeviceId = device.Id.ToString();
}
if (addIdentifier)
{
request.Identifier = currentContext.DeviceIdentifier;
}
await SendPayloadToUserAsync(notification.UserId.Value, PushType.SyncNotification, message, true,
notification.ClientType);
}
else if (notification.OrganizationId.HasValue)
{
await SendPayloadToOrganizationAsync(notification.OrganizationId.Value, PushType.SyncNotification, message,
true, notification.ClientType);
}
}
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,
string deviceId = null)
{
throw new NotImplementedException();
}
public async Task PushSyncOrganizationStatusAsync(Organization organization)
@ -278,4 +238,65 @@ public class RelayPushNotificationService : BaseIdentityClientService, IPushNoti
},
false
);
private async Task SendPayloadToUserAsync(Guid userId, PushType type, object payload, bool excludeCurrentContext,
ClientType? clientType = null)
{
var request = new PushSendRequestModel
{
UserId = userId.ToString(),
Type = type,
Payload = payload,
ClientType = clientType
};
await AddCurrentContextAsync(request, excludeCurrentContext);
await SendAsync(HttpMethod.Post, "push/send", request);
}
private async Task SendPayloadToOrganizationAsync(Guid orgId, PushType type, object payload,
bool excludeCurrentContext, ClientType? clientType = null)
{
var request = new PushSendRequestModel
{
OrganizationId = orgId.ToString(),
Type = type,
Payload = payload,
ClientType = clientType
};
await AddCurrentContextAsync(request, excludeCurrentContext);
await SendAsync(HttpMethod.Post, "push/send", request);
}
private async Task AddCurrentContextAsync(PushSendRequestModel request, bool addIdentifier)
{
var currentContext =
_httpContextAccessor?.HttpContext?.RequestServices.GetService(typeof(ICurrentContext)) as ICurrentContext;
if (!string.IsNullOrWhiteSpace(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,
string deviceId = null, ClientType? clientType = null)
{
throw new NotImplementedException();
}
public Task SendPayloadToOrganizationAsync(string orgId, PushType type, object payload, string identifier,
string deviceId = null, ClientType? clientType = null)
{
throw new NotImplementedException();
}
}

View File

@ -25,7 +25,7 @@ public class RelayPushRegistrationService : BaseIdentityClientService, IPushRegi
}
public async Task CreateOrUpdateRegistrationAsync(string pushToken, string deviceId, string userId,
string identifier, DeviceType type)
string identifier, DeviceType type, IEnumerable<string> organizationIds)
{
var requestModel = new PushRegistrationRequestModel
{
@ -33,7 +33,8 @@ public class RelayPushRegistrationService : BaseIdentityClientService, IPushRegi
Identifier = identifier,
PushToken = pushToken,
Type = type,
UserId = userId
UserId = userId,
OrganizationIds = organizationIds
};
await SendAsync(HttpMethod.Post, "push/register", requestModel);
}

View File

@ -1,6 +1,7 @@
using Bit.Core.Auth.Models.Api.Request;
using Bit.Core.Auth.Utilities;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Platform.Push;
using Bit.Core.Repositories;
@ -11,13 +12,16 @@ public class DeviceService : IDeviceService
{
private readonly IDeviceRepository _deviceRepository;
private readonly IPushRegistrationService _pushRegistrationService;
private readonly IOrganizationUserRepository _organizationUserRepository;
public DeviceService(
IDeviceRepository deviceRepository,
IPushRegistrationService pushRegistrationService)
IPushRegistrationService pushRegistrationService,
IOrganizationUserRepository organizationUserRepository)
{
_deviceRepository = deviceRepository;
_pushRegistrationService = pushRegistrationService;
_organizationUserRepository = organizationUserRepository;
}
public async Task SaveAsync(Device device)
@ -32,8 +36,13 @@ public class DeviceService : IDeviceService
await _deviceRepository.ReplaceAsync(device);
}
var organizationIdsString =
(await _organizationUserRepository.GetManyDetailsByUserAsync(device.UserId,
OrganizationUserStatusType.Confirmed))
.Select(ou => ou.OrganizationId.ToString());
await _pushRegistrationService.CreateOrUpdateRegistrationAsync(device.PushToken, device.Id.ToString(),
device.UserId.ToString(), device.Identifier, device.Type);
device.UserId.ToString(), device.Identifier, device.Type, organizationIdsString);
}
public async Task ClearTokenAsync(Device device)

View File

@ -18,6 +18,7 @@ public class ApiResources
Claims.SecurityStamp,
Claims.Premium,
Claims.Device,
Claims.DeviceType,
Claims.OrganizationOwner,
Claims.OrganizationAdmin,
Claims.OrganizationUser,

View File

@ -210,6 +210,7 @@ public abstract class BaseRequestValidator<T> where T : class
if (device != null)
{
claims.Add(new Claim(Claims.Device, device.Identifier));
claims.Add(new Claim(Claims.DeviceType, device.Type.ToString()));
}
var customResponse = new Dictionary<string, object>();

View File

@ -10,6 +10,8 @@ public static class HubHelpers
private static JsonSerializerOptions _deserializerOptions =
new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
private static readonly string _receiveMessageMethod = "ReceiveMessage";
public static async Task SendNotificationToHubAsync(
string notificationJson,
IHubContext<NotificationsHub> hubContext,
@ -18,7 +20,8 @@ public static class HubHelpers
CancellationToken cancellationToken = default(CancellationToken)
)
{
var notification = JsonSerializer.Deserialize<PushNotificationData<object>>(notificationJson, _deserializerOptions);
var notification =
JsonSerializer.Deserialize<PushNotificationData<object>>(notificationJson, _deserializerOptions);
logger.LogInformation("Sending notification: {NotificationType}", notification.Type);
switch (notification.Type)
{
@ -32,14 +35,15 @@ public static class HubHelpers
if (cipherNotification.Payload.UserId.HasValue)
{
await hubContext.Clients.User(cipherNotification.Payload.UserId.ToString())
.SendAsync("ReceiveMessage", cipherNotification, cancellationToken);
.SendAsync(_receiveMessageMethod, cipherNotification, cancellationToken);
}
else if (cipherNotification.Payload.OrganizationId.HasValue)
{
await hubContext.Clients.Group(
$"Organization_{cipherNotification.Payload.OrganizationId}")
.SendAsync("ReceiveMessage", cipherNotification, cancellationToken);
await hubContext.Clients
.Group(NotificationsHub.GetOrganizationGroup(cipherNotification.Payload.OrganizationId.Value))
.SendAsync(_receiveMessageMethod, cipherNotification, cancellationToken);
}
break;
case PushType.SyncFolderUpdate:
case PushType.SyncFolderCreate:
@ -48,7 +52,7 @@ public static class HubHelpers
JsonSerializer.Deserialize<PushNotificationData<SyncFolderPushNotification>>(
notificationJson, _deserializerOptions);
await hubContext.Clients.User(folderNotification.Payload.UserId.ToString())
.SendAsync("ReceiveMessage", folderNotification, cancellationToken);
.SendAsync(_receiveMessageMethod, folderNotification, cancellationToken);
break;
case PushType.SyncCiphers:
case PushType.SyncVault:
@ -60,30 +64,30 @@ public static class HubHelpers
JsonSerializer.Deserialize<PushNotificationData<UserPushNotification>>(
notificationJson, _deserializerOptions);
await hubContext.Clients.User(userNotification.Payload.UserId.ToString())
.SendAsync("ReceiveMessage", userNotification, cancellationToken);
.SendAsync(_receiveMessageMethod, userNotification, cancellationToken);
break;
case PushType.SyncSendCreate:
case PushType.SyncSendUpdate:
case PushType.SyncSendDelete:
var sendNotification =
JsonSerializer.Deserialize<PushNotificationData<SyncSendPushNotification>>(
notificationJson, _deserializerOptions);
notificationJson, _deserializerOptions);
await hubContext.Clients.User(sendNotification.Payload.UserId.ToString())
.SendAsync("ReceiveMessage", sendNotification, cancellationToken);
.SendAsync(_receiveMessageMethod, sendNotification, cancellationToken);
break;
case PushType.AuthRequestResponse:
var authRequestResponseNotification =
JsonSerializer.Deserialize<PushNotificationData<AuthRequestPushNotification>>(
notificationJson, _deserializerOptions);
notificationJson, _deserializerOptions);
await anonymousHubContext.Clients.Group(authRequestResponseNotification.Payload.Id.ToString())
.SendAsync("AuthRequestResponseRecieved", authRequestResponseNotification, cancellationToken);
break;
case PushType.AuthRequest:
var authRequestNotification =
JsonSerializer.Deserialize<PushNotificationData<AuthRequestPushNotification>>(
notificationJson, _deserializerOptions);
notificationJson, _deserializerOptions);
await hubContext.Clients.User(authRequestNotification.Payload.UserId.ToString())
.SendAsync("ReceiveMessage", authRequestNotification, cancellationToken);
.SendAsync(_receiveMessageMethod, authRequestNotification, cancellationToken);
break;
case PushType.SyncOrganizationStatusChanged:
var orgStatusNotification =
@ -99,6 +103,32 @@ public static class HubHelpers
await hubContext.Clients.Group($"Organization_{organizationCollectionSettingsChangedNotification.Payload.OrganizationId}")
.SendAsync("ReceiveMessage", organizationCollectionSettingsChangedNotification, cancellationToken);
break;
case PushType.SyncNotification:
var syncNotification =
JsonSerializer.Deserialize<PushNotificationData<SyncNotificationPushNotification>>(
notificationJson, _deserializerOptions);
if (syncNotification.Payload.UserId.HasValue)
{
if (syncNotification.Payload.ClientType == ClientType.All)
{
await hubContext.Clients.User(syncNotification.Payload.UserId.ToString())
.SendAsync(_receiveMessageMethod, syncNotification, cancellationToken);
}
else
{
await hubContext.Clients.Group(NotificationsHub.GetUserGroup(
syncNotification.Payload.UserId.Value, syncNotification.Payload.ClientType))
.SendAsync(_receiveMessageMethod, syncNotification, cancellationToken);
}
}
else if (syncNotification.Payload.OrganizationId.HasValue)
{
await hubContext.Clients.Group(NotificationsHub.GetOrganizationGroup(
syncNotification.Payload.OrganizationId.Value, syncNotification.Payload.ClientType))
.SendAsync(_receiveMessageMethod, syncNotification, cancellationToken);
}
break;
default:
break;
}

View File

@ -1,5 +1,7 @@
using Bit.Core.Context;
using Bit.Core.Enums;
using Bit.Core.Settings;
using Bit.Core.Utilities;
using Microsoft.AspNetCore.Authorization;
namespace Bit.Notifications;
@ -20,13 +22,25 @@ public class NotificationsHub : Microsoft.AspNetCore.SignalR.Hub
{
var currentContext = new CurrentContext(null, null);
await currentContext.BuildAsync(Context.User, _globalSettings);
var clientType = DeviceTypes.ToClientType(currentContext.DeviceType);
if (clientType != ClientType.All && currentContext.UserId.HasValue)
{
await Groups.AddToGroupAsync(Context.ConnectionId, GetUserGroup(currentContext.UserId.Value, clientType));
}
if (currentContext.Organizations != null)
{
foreach (var org in currentContext.Organizations)
{
await Groups.AddToGroupAsync(Context.ConnectionId, $"Organization_{org.Id}");
await Groups.AddToGroupAsync(Context.ConnectionId, GetOrganizationGroup(org.Id));
if (clientType != ClientType.All)
{
await Groups.AddToGroupAsync(Context.ConnectionId, GetOrganizationGroup(org.Id, clientType));
}
}
}
_connectionCounter.Increment();
await base.OnConnectedAsync();
}
@ -35,14 +49,39 @@ public class NotificationsHub : Microsoft.AspNetCore.SignalR.Hub
{
var currentContext = new CurrentContext(null, null);
await currentContext.BuildAsync(Context.User, _globalSettings);
var clientType = DeviceTypes.ToClientType(currentContext.DeviceType);
if (clientType != ClientType.All && currentContext.UserId.HasValue)
{
await Groups.RemoveFromGroupAsync(Context.ConnectionId,
GetUserGroup(currentContext.UserId.Value, clientType));
}
if (currentContext.Organizations != null)
{
foreach (var org in currentContext.Organizations)
{
await Groups.RemoveFromGroupAsync(Context.ConnectionId, $"Organization_{org.Id}");
await Groups.RemoveFromGroupAsync(Context.ConnectionId, GetOrganizationGroup(org.Id));
if (clientType != ClientType.All)
{
await Groups.RemoveFromGroupAsync(Context.ConnectionId, GetOrganizationGroup(org.Id, clientType));
}
}
}
_connectionCounter.Decrement();
await base.OnDisconnectedAsync(exception);
}
public static string GetUserGroup(Guid userId, ClientType clientType)
{
return $"UserClientType_{userId}_{clientType}";
}
public static string GetOrganizationGroup(Guid organizationId, ClientType? clientType = null)
{
return clientType is null or ClientType.All
? $"Organization_{organizationId}"
: $"OrganizationClientType_{organizationId}_{clientType}";
}
}

View File

@ -3,6 +3,7 @@ using System.Reflection;
using System.Security.Claims;
using System.Security.Cryptography.X509Certificates;
using AspNetCoreRateLimit;
using Azure.Storage.Queues;
using Bit.Core.AdminConsole.Models.Business.Tokenables;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
using Bit.Core.AdminConsole.Services;
@ -306,7 +307,10 @@ public static class ServiceCollectionExtensions
services.AddKeyedSingleton<IPushNotificationService, NotificationHubPushNotificationService>("implementation");
if (CoreHelpers.SettingHasValue(globalSettings.Notifications?.ConnectionString))
{
services.AddKeyedSingleton<IPushNotificationService, AzureQueuePushNotificationService>("implementation");
services.AddKeyedSingleton("notifications",
(_, _) => new QueueClient(globalSettings.Notifications.ConnectionString, "notifications"));
services.AddKeyedSingleton<IPushNotificationService, AzureQueuePushNotificationService>(
"implementation");
}
}