1
0
mirror of https://github.com/bitwarden/server synced 2026-01-02 08:33:48 +00:00

[PM-10564] Push notification updates to other clients (#5057)

* 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

* null check fix

* logging using template formatting
This commit is contained in:
Maciej Zieniuk
2025-02-13 14:23:33 +01:00
committed by GitHub
parent 0c482eea32
commit c3924bbf3b
22 changed files with 646 additions and 126 deletions

View File

@@ -1,4 +1,5 @@
using System.Text.Json;
#nullable enable
using System.Text.Json;
using Azure.Storage.Queues;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.Auth.Entities;
@@ -42,7 +43,7 @@ public class AzureQueuePushNotificationService : IPushNotificationService
await PushCipherAsync(cipher, PushType.SyncLoginDelete, null);
}
private async Task PushCipherAsync(Cipher cipher, PushType type, IEnumerable<Guid> collectionIds)
private async Task PushCipherAsync(Cipher cipher, PushType type, IEnumerable<Guid>? collectionIds)
{
if (cipher.OrganizationId.HasValue)
{
@@ -184,6 +185,27 @@ public class AzureQueuePushNotificationService : IPushNotificationService
await SendMessageAsync(PushType.SyncNotification, message, true);
}
public async Task PushNotificationStatusAsync(Notification notification, NotificationStatus notificationStatus)
{
var message = new NotificationPushNotification
{
Id = notification.Id,
Priority = notification.Priority,
Global = notification.Global,
ClientType = notification.ClientType,
UserId = notification.UserId,
OrganizationId = notification.OrganizationId,
Title = notification.Title,
Body = notification.Body,
CreationDate = notification.CreationDate,
RevisionDate = notification.RevisionDate,
ReadDate = notificationStatus.ReadDate,
DeletedDate = notificationStatus.DeletedDate
};
await SendMessageAsync(PushType.SyncNotificationStatus, message, true);
}
private async Task PushSendAsync(Send send, PushType type)
{
if (send.UserId.HasValue)
@@ -207,7 +229,7 @@ public class AzureQueuePushNotificationService : IPushNotificationService
await _queueClient.SendMessageAsync(message);
}
private string GetContextIdentifier(bool excludeCurrentContext)
private string? GetContextIdentifier(bool excludeCurrentContext)
{
if (!excludeCurrentContext)
{
@@ -219,15 +241,15 @@ public class AzureQueuePushNotificationService : IPushNotificationService
return currentContext?.DeviceIdentifier;
}
public Task SendPayloadToUserAsync(string userId, PushType type, object payload, string identifier,
string deviceId = null, ClientType? clientType = null)
public Task SendPayloadToUserAsync(string userId, PushType type, object payload, string? identifier,
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, ClientType? clientType = null)
public Task SendPayloadToOrganizationAsync(string orgId, PushType type, object payload, string? identifier,
string? deviceId = null, ClientType? clientType = null)
{
// Noop
return Task.FromResult(0);

View File

@@ -1,4 +1,5 @@
using Bit.Core.AdminConsole.Entities;
#nullable enable
using Bit.Core.AdminConsole.Entities;
using Bit.Core.Auth.Entities;
using Bit.Core.Enums;
using Bit.Core.NotificationCenter.Entities;
@@ -25,12 +26,13 @@ public interface IPushNotificationService
Task PushSyncSendUpdateAsync(Send send);
Task PushSyncSendDeleteAsync(Send send);
Task PushNotificationAsync(Notification notification);
Task PushNotificationStatusAsync(Notification notification, NotificationStatus notificationStatus);
Task PushAuthRequestAsync(AuthRequest authRequest);
Task PushAuthRequestResponseAsync(AuthRequest authRequest);
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);
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

@@ -1,4 +1,5 @@
using Bit.Core.AdminConsole.Entities;
#nullable enable
using Bit.Core.AdminConsole.Entities;
using Bit.Core.Auth.Entities;
using Bit.Core.Enums;
using Bit.Core.NotificationCenter.Entities;
@@ -24,7 +25,7 @@ public class MultiServicePushNotificationService : IPushNotificationService
_logger = logger;
_logger.LogInformation("Hub services: {Services}", _services.Count());
globalSettings?.NotificationHubPool?.NotificationHubs?.ForEach(hub =>
globalSettings.NotificationHubPool?.NotificationHubs?.ForEach(hub =>
{
_logger.LogInformation("HubName: {HubName}, EnableSendTracing: {EnableSendTracing}, RegistrationStartDate: {RegistrationStartDate}, RegistrationEndDate: {RegistrationEndDate}", hub.HubName, hub.EnableSendTracing, hub.RegistrationStartDate, hub.RegistrationEndDate);
});
@@ -150,15 +151,21 @@ public class MultiServicePushNotificationService : IPushNotificationService
return Task.CompletedTask;
}
public Task SendPayloadToUserAsync(string userId, PushType type, object payload, string identifier,
string deviceId = null, ClientType? clientType = null)
public Task PushNotificationStatusAsync(Notification notification, NotificationStatus notificationStatus)
{
PushToServices((s) => s.PushNotificationStatusAsync(notification, notificationStatus));
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)
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);
@@ -166,12 +173,16 @@ public class MultiServicePushNotificationService : IPushNotificationService
private void PushToServices(Func<IPushNotificationService, Task> pushFunc)
{
if (_services != null)
if (!_services.Any())
{
foreach (var service in _services)
{
pushFunc(service);
}
_logger.LogWarning("No services found to push notification");
return;
}
foreach (var service in _services)
{
_logger.LogDebug("Pushing notification to service {ServiceName}", service.GetType().Name);
pushFunc(service);
}
}
}

View File

@@ -1,4 +1,5 @@
using Bit.Core.AdminConsole.Entities;
#nullable enable
using Bit.Core.AdminConsole.Entities;
using Bit.Core.Auth.Entities;
using Bit.Core.Enums;
using Bit.Core.NotificationCenter.Entities;
@@ -84,8 +85,8 @@ public class NoopPushNotificationService : IPushNotificationService
return Task.FromResult(0);
}
public Task SendPayloadToOrganizationAsync(string orgId, PushType type, object payload, string identifier,
string deviceId = null, ClientType? clientType = null)
public Task SendPayloadToOrganizationAsync(string orgId, PushType type, object payload, string? identifier,
string? deviceId = null, ClientType? clientType = null)
{
return Task.FromResult(0);
}
@@ -107,11 +108,14 @@ public class NoopPushNotificationService : IPushNotificationService
return Task.FromResult(0);
}
public Task SendPayloadToUserAsync(string userId, PushType type, object payload, string identifier,
string deviceId = null, ClientType? clientType = null)
public Task SendPayloadToUserAsync(string userId, PushType type, object payload, string? identifier,
string? deviceId = null, ClientType? clientType = null)
{
return Task.FromResult(0);
}
public Task PushNotificationAsync(Notification notification) => Task.CompletedTask;
public Task PushNotificationStatusAsync(Notification notification, NotificationStatus notificationStatus) =>
Task.CompletedTask;
}

View File

@@ -1,4 +1,5 @@
using Bit.Core.AdminConsole.Entities;
#nullable enable
using Bit.Core.AdminConsole.Entities;
using Bit.Core.Auth.Entities;
using Bit.Core.Context;
using Bit.Core.Enums;
@@ -16,7 +17,6 @@ namespace Bit.Core.Platform.Push;
public class NotificationsApiPushNotificationService : BaseIdentityClientService, IPushNotificationService
{
private readonly GlobalSettings _globalSettings;
private readonly IHttpContextAccessor _httpContextAccessor;
public NotificationsApiPushNotificationService(
@@ -33,7 +33,6 @@ public class NotificationsApiPushNotificationService : BaseIdentityClientService
globalSettings.InternalIdentityKey,
logger)
{
_globalSettings = globalSettings;
_httpContextAccessor = httpContextAccessor;
}
@@ -52,7 +51,7 @@ public class NotificationsApiPushNotificationService : BaseIdentityClientService
await PushCipherAsync(cipher, PushType.SyncLoginDelete, null);
}
private async Task PushCipherAsync(Cipher cipher, PushType type, IEnumerable<Guid> collectionIds)
private async Task PushCipherAsync(Cipher cipher, PushType type, IEnumerable<Guid>? collectionIds)
{
if (cipher.OrganizationId.HasValue)
{
@@ -203,6 +202,27 @@ public class NotificationsApiPushNotificationService : BaseIdentityClientService
await SendMessageAsync(PushType.SyncNotification, message, true);
}
public async Task PushNotificationStatusAsync(Notification notification, NotificationStatus notificationStatus)
{
var message = new NotificationPushNotification
{
Id = notification.Id,
Priority = notification.Priority,
Global = notification.Global,
ClientType = notification.ClientType,
UserId = notification.UserId,
OrganizationId = notification.OrganizationId,
Title = notification.Title,
Body = notification.Body,
CreationDate = notification.CreationDate,
RevisionDate = notification.RevisionDate,
ReadDate = notificationStatus.ReadDate,
DeletedDate = notificationStatus.DeletedDate
};
await SendMessageAsync(PushType.SyncNotificationStatus, message, true);
}
private async Task PushSendAsync(Send send, PushType type)
{
if (send.UserId.HasValue)
@@ -225,7 +245,7 @@ public class NotificationsApiPushNotificationService : BaseIdentityClientService
await SendAsync(HttpMethod.Post, "send", request);
}
private string GetContextIdentifier(bool excludeCurrentContext)
private string? GetContextIdentifier(bool excludeCurrentContext)
{
if (!excludeCurrentContext)
{
@@ -233,19 +253,19 @@ public class NotificationsApiPushNotificationService : BaseIdentityClientService
}
var currentContext =
_httpContextAccessor?.HttpContext?.RequestServices.GetService(typeof(ICurrentContext)) as ICurrentContext;
_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, ClientType? clientType = null)
public Task SendPayloadToUserAsync(string userId, PushType type, object payload, string? identifier,
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, ClientType? clientType = null)
public Task SendPayloadToOrganizationAsync(string orgId, PushType type, object payload, string? identifier,
string? deviceId = null, ClientType? clientType = null)
{
// Noop
return Task.FromResult(0);

View File

@@ -1,4 +1,5 @@
using Bit.Core.AdminConsole.Entities;
#nullable enable
using Bit.Core.AdminConsole.Entities;
using Bit.Core.Auth.Entities;
using Bit.Core.Context;
using Bit.Core.Enums;
@@ -55,7 +56,7 @@ public class RelayPushNotificationService : BaseIdentityClientService, IPushNoti
await PushCipherAsync(cipher, PushType.SyncLoginDelete, null);
}
private async Task PushCipherAsync(Cipher cipher, PushType type, IEnumerable<Guid> collectionIds)
private async Task PushCipherAsync(Cipher cipher, PushType type, IEnumerable<Guid>? collectionIds)
{
if (cipher.OrganizationId.HasValue)
{
@@ -219,6 +220,36 @@ public class RelayPushNotificationService : BaseIdentityClientService, IPushNoti
}
}
public async Task PushNotificationStatusAsync(Notification notification, NotificationStatus notificationStatus)
{
var message = new NotificationPushNotification
{
Id = notification.Id,
Priority = notification.Priority,
Global = notification.Global,
ClientType = notification.ClientType,
UserId = notification.UserId,
OrganizationId = notification.OrganizationId,
Title = notification.Title,
Body = notification.Body,
CreationDate = notification.CreationDate,
RevisionDate = notification.RevisionDate,
ReadDate = notificationStatus.ReadDate,
DeletedDate = notificationStatus.DeletedDate
};
if (notification.UserId.HasValue)
{
await SendPayloadToUserAsync(notification.UserId.Value, PushType.SyncNotificationStatus, message, true,
notification.ClientType);
}
else if (notification.OrganizationId.HasValue)
{
await SendPayloadToOrganizationAsync(notification.OrganizationId.Value, PushType.SyncNotificationStatus, message,
true, notification.ClientType);
}
}
public async Task PushSyncOrganizationStatusAsync(Organization organization)
{
var message = new OrganizationStatusPushNotification
@@ -277,7 +308,7 @@ public class RelayPushNotificationService : BaseIdentityClientService, IPushNoti
private async Task AddCurrentContextAsync(PushSendRequestModel request, bool addIdentifier)
{
var currentContext =
_httpContextAccessor?.HttpContext?.RequestServices.GetService(typeof(ICurrentContext)) as ICurrentContext;
_httpContextAccessor.HttpContext?.RequestServices.GetService(typeof(ICurrentContext)) as ICurrentContext;
if (!string.IsNullOrWhiteSpace(currentContext?.DeviceIdentifier))
{
var device = await _deviceRepository.GetByIdentifierAsync(currentContext.DeviceIdentifier);
@@ -293,14 +324,14 @@ public class RelayPushNotificationService : BaseIdentityClientService, IPushNoti
}
}
public Task SendPayloadToUserAsync(string userId, PushType type, object payload, string identifier,
string deviceId = null, ClientType? clientType = null)
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)
public Task SendPayloadToOrganizationAsync(string orgId, PushType type, object payload, string? identifier,
string? deviceId = null, ClientType? clientType = null)
{
throw new NotImplementedException();
}