1
0
mirror of https://github.com/bitwarden/server synced 2025-12-26 21:23:39 +00:00

[PM-15084] Push global notification creation to affected clients (#5079)

* 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

* PM-10600: Push notification with full notification center content.

Notification Center push notification now includes all the fields.

* PM-10564: Push notification updates to other clients

Cherry-picked and squashed commits:
d9711b6031 6e69c8a0ce 01c814595e 3885885d5f 1285a7e994 fcf346985f 28ff53c293 57804ae27c 1c9339b686

* PM-15084: Push global notification creation to affected clients

Cherry-picked and squashed commits:
ed5051e0eb 181f3e4ae6 49fe7c93fd a8efb45a63 7b4122c837 d21d4a67b3 186a09bb92 1531f564b5

* PM-15084: Log warning when invalid notification push notification sent

* explicit Guid default value

* push notification tests in wrong namespace

* Installation push notification not received for on global notification center message

* wrong merge conflict

* wrong merge conflict

* installation id type Guid in push registration request
This commit is contained in:
Maciej Zieniuk
2025-02-20 15:35:48 +01:00
committed by GitHub
parent 228ce3b2e9
commit 9f4aa1ab2b
30 changed files with 963 additions and 163 deletions

View File

@@ -10,6 +10,7 @@ using Bit.Core.Models.Data;
using Bit.Core.NotificationCenter.Entities;
using Bit.Core.Platform.Push;
using Bit.Core.Repositories;
using Bit.Core.Settings;
using Bit.Core.Tools.Entities;
using Bit.Core.Vault.Entities;
using Microsoft.AspNetCore.Http;
@@ -18,6 +19,11 @@ using Notification = Bit.Core.NotificationCenter.Entities.Notification;
namespace Bit.Core.NotificationHub;
/// <summary>
/// Sends mobile push notifications to the Azure Notification Hub.
/// Used by Cloud-Hosted environments.
/// Received by Firebase for Android or APNS for iOS.
/// </summary>
public class NotificationHubPushNotificationService : IPushNotificationService
{
private readonly IInstallationDeviceRepository _installationDeviceRepository;
@@ -25,17 +31,25 @@ public class NotificationHubPushNotificationService : IPushNotificationService
private readonly bool _enableTracing = false;
private readonly INotificationHubPool _notificationHubPool;
private readonly ILogger _logger;
private readonly IGlobalSettings _globalSettings;
public NotificationHubPushNotificationService(
IInstallationDeviceRepository installationDeviceRepository,
INotificationHubPool notificationHubPool,
IHttpContextAccessor httpContextAccessor,
ILogger<NotificationsApiPushNotificationService> logger)
ILogger<NotificationHubPushNotificationService> logger,
IGlobalSettings globalSettings)
{
_installationDeviceRepository = installationDeviceRepository;
_httpContextAccessor = httpContextAccessor;
_notificationHubPool = notificationHubPool;
_logger = logger;
_globalSettings = globalSettings;
if (globalSettings.Installation.Id == Guid.Empty)
{
logger.LogWarning("Installation ID is not set. Push notifications for installations will not work.");
}
}
public async Task PushSyncCipherCreateAsync(Cipher cipher, IEnumerable<Guid> collectionIds)
@@ -185,6 +199,10 @@ public class NotificationHubPushNotificationService : IPushNotificationService
public async Task PushNotificationAsync(Notification notification)
{
Guid? installationId = notification.Global && _globalSettings.Installation.Id != Guid.Empty
? _globalSettings.Installation.Id
: null;
var message = new NotificationPushNotification
{
Id = notification.Id,
@@ -193,26 +211,49 @@ public class NotificationHubPushNotificationService : IPushNotificationService
ClientType = notification.ClientType,
UserId = notification.UserId,
OrganizationId = notification.OrganizationId,
InstallationId = installationId,
Title = notification.Title,
Body = notification.Body,
CreationDate = notification.CreationDate,
RevisionDate = notification.RevisionDate
};
if (notification.UserId.HasValue)
if (notification.Global)
{
await SendPayloadToUserAsync(notification.UserId.Value, PushType.SyncNotification, message, true,
if (installationId.HasValue)
{
await SendPayloadToInstallationAsync(installationId.Value, PushType.Notification, message, true,
notification.ClientType);
}
else
{
_logger.LogWarning(
"Invalid global notification id {NotificationId} push notification. No installation id provided.",
notification.Id);
}
}
else if (notification.UserId.HasValue)
{
await SendPayloadToUserAsync(notification.UserId.Value, PushType.Notification, message, true,
notification.ClientType);
}
else if (notification.OrganizationId.HasValue)
{
await SendPayloadToOrganizationAsync(notification.OrganizationId.Value, PushType.SyncNotification, message,
await SendPayloadToOrganizationAsync(notification.OrganizationId.Value, PushType.Notification, message,
true, notification.ClientType);
}
else
{
_logger.LogWarning("Invalid notification id {NotificationId} push notification", notification.Id);
}
}
public async Task PushNotificationStatusAsync(Notification notification, NotificationStatus notificationStatus)
{
Guid? installationId = notification.Global && _globalSettings.Installation.Id != Guid.Empty
? _globalSettings.Installation.Id
: null;
var message = new NotificationPushNotification
{
Id = notification.Id,
@@ -221,6 +262,7 @@ public class NotificationHubPushNotificationService : IPushNotificationService
ClientType = notification.ClientType,
UserId = notification.UserId,
OrganizationId = notification.OrganizationId,
InstallationId = installationId,
Title = notification.Title,
Body = notification.Body,
CreationDate = notification.CreationDate,
@@ -229,15 +271,33 @@ public class NotificationHubPushNotificationService : IPushNotificationService
DeletedDate = notificationStatus.DeletedDate
};
if (notification.UserId.HasValue)
if (notification.Global)
{
await SendPayloadToUserAsync(notification.UserId.Value, PushType.SyncNotificationStatus, message, true,
if (installationId.HasValue)
{
await SendPayloadToInstallationAsync(installationId.Value, PushType.NotificationStatus, message, true,
notification.ClientType);
}
else
{
_logger.LogWarning(
"Invalid global notification status id {NotificationId} push notification. No installation id provided.",
notification.Id);
}
}
else if (notification.UserId.HasValue)
{
await SendPayloadToUserAsync(notification.UserId.Value, PushType.NotificationStatus, message, true,
notification.ClientType);
}
else if (notification.OrganizationId.HasValue)
{
await SendPayloadToOrganizationAsync(notification.OrganizationId.Value, PushType.SyncNotificationStatus, message,
true, notification.ClientType);
await SendPayloadToOrganizationAsync(notification.OrganizationId.Value, PushType.NotificationStatus,
message, true, notification.ClientType);
}
else
{
_logger.LogWarning("Invalid notification status id {NotificationId} push notification", notification.Id);
}
}
@@ -248,6 +308,13 @@ public class NotificationHubPushNotificationService : IPushNotificationService
await SendPayloadToUserAsync(authRequest.UserId, type, message, true);
}
private async Task SendPayloadToInstallationAsync(Guid installationId, PushType type, object payload,
bool excludeCurrentContext, ClientType? clientType = null)
{
await SendPayloadToInstallationAsync(installationId.ToString(), type, payload,
GetContextIdentifier(excludeCurrentContext), clientType: clientType);
}
private async Task SendPayloadToUserAsync(Guid userId, PushType type, object payload, bool excludeCurrentContext,
ClientType? clientType = null)
{
@@ -262,6 +329,17 @@ public class NotificationHubPushNotificationService : IPushNotificationService
GetContextIdentifier(excludeCurrentContext), clientType: clientType);
}
public async Task SendPayloadToInstallationAsync(string installationId, PushType type, object payload,
string? identifier, string? deviceId = null, ClientType? clientType = null)
{
var tag = BuildTag($"template:payload && installationId:{installationId}", identifier, clientType);
await SendPayloadAsync(tag, type, payload);
if (InstallationDeviceEntity.IsInstallationDeviceId(deviceId))
{
await _installationDeviceRepository.UpsertAsync(new InstallationDeviceEntity(deviceId));
}
}
public async Task SendPayloadToUserAsync(string userId, PushType type, object payload, string? identifier,
string? deviceId = null, ClientType? clientType = null)
{

View File

@@ -21,7 +21,7 @@ public class NotificationHubPushRegistrationService : IPushRegistrationService
}
public async Task CreateOrUpdateRegistrationAsync(string pushToken, string deviceId, string userId,
string identifier, DeviceType type, IEnumerable<string> organizationIds)
string identifier, DeviceType type, IEnumerable<string> organizationIds, Guid installationId)
{
if (string.IsNullOrWhiteSpace(pushToken))
{
@@ -50,6 +50,11 @@ public class NotificationHubPushRegistrationService : IPushRegistrationService
installation.Tags.Add($"organizationId:{organizationId}");
}
if (installationId != Guid.Empty)
{
installation.Tags.Add($"installationId:{installationId}");
}
string payloadTemplate = null, messageTemplate = null, badgeMessageTemplate = null;
switch (type)
{
@@ -80,11 +85,11 @@ public class NotificationHubPushRegistrationService : IPushRegistrationService
}
BuildInstallationTemplate(installation, "payload", payloadTemplate, userId, identifier, clientType,
organizationIdsList);
organizationIdsList, installationId);
BuildInstallationTemplate(installation, "message", messageTemplate, userId, identifier, clientType,
organizationIdsList);
organizationIdsList, installationId);
BuildInstallationTemplate(installation, "badgeMessage", badgeMessageTemplate ?? messageTemplate,
userId, identifier, clientType, organizationIdsList);
userId, identifier, clientType, organizationIdsList, installationId);
await ClientFor(GetComb(deviceId)).CreateOrUpdateInstallationAsync(installation);
if (InstallationDeviceEntity.IsInstallationDeviceId(deviceId))
@@ -94,7 +99,7 @@ public class NotificationHubPushRegistrationService : IPushRegistrationService
}
private void BuildInstallationTemplate(Installation installation, string templateId, string templateBody,
string userId, string identifier, ClientType clientType, List<string> organizationIds)
string userId, string identifier, ClientType clientType, List<string> organizationIds, Guid installationId)
{
if (templateBody == null)
{
@@ -122,6 +127,11 @@ public class NotificationHubPushRegistrationService : IPushRegistrationService
template.Tags.Add($"organizationId:{organizationId}");
}
if (installationId != Guid.Empty)
{
template.Tags.Add($"installationId:{installationId}");
}
installation.Templates.Add(fullTemplateId, template);
}