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

Shard notification hub (#4450)

* Allow for binning of comb IDs by date and value

* Introduce notification hub pool

* Replace device type sharding with comb + range sharding

* Fix proxy interface

* Use enumerable services for multiServiceNotificationHub

* Fix push interface usage

* Fix push notification service dependencies

* Fix push notification keys

* Fixup documentation

* Remove deprecated settings

* Fix tests

* PascalCase method names

* Remove unused request model properties

* Remove unused setting

* Improve DateFromComb precision

* Prefer readonly service enumerable

* Pascal case template holes

* Name TryParse methods TryParse

* Apply suggestions from code review

Co-authored-by: Justin Baur <19896123+justindbaur@users.noreply.github.com>

* AllClients is a set of clients and must be deduplicated

* Fix registration start time

* Add logging to initialization of a notification hub

* more logging

* Add lower level logging for hub settings

* Log when connection is resolved

* Improve log message

* Log pushes to notification hub

* temporarily elevate log messages for visibility

* Log in multi-service when relaying to another push service

* Revert to more reasonable logging free of user information

* Fixup merge

Deleting user was extracted to a command in #4803, this updates that work to use just the device ids as I did elsewhere in abd67e8ec

* Do not use bouncy castle exception types

* Add required services for logging

---------

Co-authored-by: Justin Baur <19896123+justindbaur@users.noreply.github.com>
Co-authored-by: bnagawiecki <107435978+bnagawiecki@users.noreply.github.com>
This commit is contained in:
Matt Gibson
2024-10-22 09:20:57 -07:00
committed by GitHub
parent 00bfcb5fa5
commit f44a59f7a9
29 changed files with 888 additions and 245 deletions

View File

@ -1,6 +1,5 @@
using AutoFixture;
using Microsoft.AspNetCore.Mvc;
using Org.BouncyCastle.Security;
namespace Bit.Test.Common.AutoFixture;
@ -15,7 +14,7 @@ public class ControllerCustomization : ICustomization
{
if (!controllerType.IsAssignableTo(typeof(Controller)))
{
throw new InvalidParameterException($"{nameof(controllerType)} must derive from {typeof(Controller).Name}");
throw new Exception($"{nameof(controllerType)} must derive from {typeof(Controller).Name}");
}
_controllerType = controllerType;

View File

@ -0,0 +1,205 @@
using Bit.Core.Settings;
using Bit.Core.Utilities;
using Xunit;
namespace Bit.Core.Test.NotificationHub;
public class NotificationHubConnectionTests
{
[Fact]
public void IsValid_ConnectionStringIsNull_ReturnsFalse()
{
// Arrange
var hub = new GlobalSettings.NotificationHubSettings()
{
ConnectionString = null,
HubName = "hub",
RegistrationStartDate = DateTime.UtcNow,
RegistrationEndDate = DateTime.UtcNow.AddDays(1)
};
// Act
var connection = NotificationHubConnection.From(hub);
// Assert
Assert.False(connection.IsValid);
}
[Fact]
public void IsValid_HubNameIsNull_ReturnsFalse()
{
// Arrange
var hub = new GlobalSettings.NotificationHubSettings()
{
ConnectionString = "Endpoint=sb://example.servicebus.windows.net/;",
HubName = null,
RegistrationStartDate = DateTime.UtcNow,
RegistrationEndDate = DateTime.UtcNow.AddDays(1)
};
// Act
var connection = NotificationHubConnection.From(hub);
// Assert
Assert.False(connection.IsValid);
}
[Fact]
public void IsValid_ConnectionStringAndHubNameAreNotNull_ReturnsTrue()
{
// Arrange
var hub = new GlobalSettings.NotificationHubSettings()
{
ConnectionString = "connection",
HubName = "hub",
RegistrationStartDate = DateTime.UtcNow,
RegistrationEndDate = DateTime.UtcNow.AddDays(1)
};
// Act
var connection = NotificationHubConnection.From(hub);
// Assert
Assert.True(connection.IsValid);
}
[Fact]
public void RegistrationEnabled_QueryTimeIsBeforeStartDate_ReturnsFalse()
{
// Arrange
var hub = new GlobalSettings.NotificationHubSettings()
{
ConnectionString = "connection",
HubName = "hub",
RegistrationStartDate = DateTime.UtcNow.AddDays(1),
RegistrationEndDate = DateTime.UtcNow.AddDays(2)
};
var connection = NotificationHubConnection.From(hub);
// Act
var result = connection.RegistrationEnabled(DateTime.UtcNow);
// Assert
Assert.False(result);
}
[Fact]
public void RegistrationEnabled_QueryTimeIsAfterEndDate_ReturnsFalse()
{
// Arrange
var hub = new GlobalSettings.NotificationHubSettings()
{
ConnectionString = "connection",
HubName = "hub",
RegistrationStartDate = DateTime.UtcNow,
RegistrationEndDate = DateTime.UtcNow.AddDays(1)
};
var connection = NotificationHubConnection.From(hub);
// Act
var result = connection.RegistrationEnabled(DateTime.UtcNow.AddDays(2));
// Assert
Assert.False(result);
}
[Fact]
public void RegistrationEnabled_NullStartDate_ReturnsFalse()
{
// Arrange
var hub = new GlobalSettings.NotificationHubSettings()
{
ConnectionString = "connection",
HubName = "hub",
RegistrationStartDate = null,
RegistrationEndDate = DateTime.UtcNow.AddDays(1)
};
var connection = NotificationHubConnection.From(hub);
// Act
var result = connection.RegistrationEnabled(DateTime.UtcNow);
// Assert
Assert.False(result);
}
[Fact]
public void RegistrationEnabled_QueryTimeIsBetweenStartDateAndEndDate_ReturnsTrue()
{
// Arrange
var hub = new GlobalSettings.NotificationHubSettings()
{
ConnectionString = "connection",
HubName = "hub",
RegistrationStartDate = DateTime.UtcNow,
RegistrationEndDate = DateTime.UtcNow.AddDays(1)
};
var connection = NotificationHubConnection.From(hub);
// Act
var result = connection.RegistrationEnabled(DateTime.UtcNow.AddHours(1));
// Assert
Assert.True(result);
}
[Fact]
public void RegistrationEnabled_CombTimeIsBeforeStartDate_ReturnsFalse()
{
// Arrange
var hub = new GlobalSettings.NotificationHubSettings()
{
ConnectionString = "connection",
HubName = "hub",
RegistrationStartDate = DateTime.UtcNow.AddDays(1),
RegistrationEndDate = DateTime.UtcNow.AddDays(2)
};
var connection = NotificationHubConnection.From(hub);
// Act
var result = connection.RegistrationEnabled(CoreHelpers.GenerateComb(Guid.NewGuid(), DateTime.UtcNow));
// Assert
Assert.False(result);
}
[Fact]
public void RegistrationEnabled_CombTimeIsAfterEndDate_ReturnsFalse()
{
// Arrange
var hub = new GlobalSettings.NotificationHubSettings()
{
ConnectionString = "connection",
HubName = "hub",
RegistrationStartDate = DateTime.UtcNow,
RegistrationEndDate = DateTime.UtcNow.AddDays(1)
};
var connection = NotificationHubConnection.From(hub);
// Act
var result = connection.RegistrationEnabled(CoreHelpers.GenerateComb(Guid.NewGuid(), DateTime.UtcNow.AddDays(2)));
// Assert
Assert.False(result);
}
[Fact]
public void RegistrationEnabled_CombTimeIsBetweenStartDateAndEndDate_ReturnsTrue()
{
// Arrange
var hub = new GlobalSettings.NotificationHubSettings()
{
ConnectionString = "connection",
HubName = "hub",
RegistrationStartDate = DateTime.UtcNow,
RegistrationEndDate = DateTime.UtcNow.AddDays(1)
};
var connection = NotificationHubConnection.From(hub);
// Act
var result = connection.RegistrationEnabled(CoreHelpers.GenerateComb(Guid.NewGuid(), DateTime.UtcNow.AddHours(1)));
// Assert
Assert.True(result);
}
}

View File

@ -0,0 +1,156 @@
using Bit.Core.NotificationHub;
using Bit.Core.Settings;
using Bit.Core.Utilities;
using Microsoft.Extensions.Logging;
using NSubstitute;
using Xunit;
using static Bit.Core.Settings.GlobalSettings;
namespace Bit.Core.Test.NotificationHub;
public class NotificationHubPoolTests
{
[Fact]
public void NotificationHubPool_WarnsOnMissingConnectionString()
{
// Arrange
var globalSettings = new GlobalSettings()
{
NotificationHubPool = new NotificationHubPoolSettings()
{
NotificationHubs = new() {
new() {
ConnectionString = null,
HubName = "hub",
RegistrationStartDate = DateTime.UtcNow,
RegistrationEndDate = DateTime.UtcNow.AddDays(1)
}
}
}
};
var logger = Substitute.For<ILogger<NotificationHubPool>>();
// Act
var sut = new NotificationHubPool(logger, globalSettings);
// Assert
logger.Received().Log(LogLevel.Warning, Arg.Any<EventId>(),
Arg.Is<object>(o => o.ToString() == "Invalid notification hub settings: hub"),
null,
Arg.Any<Func<object, Exception, string>>());
}
[Fact]
public void NotificationHubPool_WarnsOnMissingHubName()
{
// Arrange
var globalSettings = new GlobalSettings()
{
NotificationHubPool = new NotificationHubPoolSettings()
{
NotificationHubs = new() {
new() {
ConnectionString = "connection",
HubName = null,
RegistrationStartDate = DateTime.UtcNow,
RegistrationEndDate = DateTime.UtcNow.AddDays(1)
}
}
}
};
var logger = Substitute.For<ILogger<NotificationHubPool>>();
// Act
var sut = new NotificationHubPool(logger, globalSettings);
// Assert
logger.Received().Log(LogLevel.Warning, Arg.Any<EventId>(),
Arg.Is<object>(o => o.ToString() == "Invalid notification hub settings: hub name missing"),
null,
Arg.Any<Func<object, Exception, string>>());
}
[Fact]
public void NotificationHubPool_ClientFor_ThrowsOnNoValidHubs()
{
// Arrange
var globalSettings = new GlobalSettings()
{
NotificationHubPool = new NotificationHubPoolSettings()
{
NotificationHubs = new() {
new() {
ConnectionString = "connection",
HubName = "hub",
RegistrationStartDate = null,
RegistrationEndDate = null,
}
}
}
};
var logger = Substitute.For<ILogger<NotificationHubPool>>();
var sut = new NotificationHubPool(logger, globalSettings);
// Act
Action act = () => sut.ClientFor(Guid.NewGuid());
// Assert
Assert.Throws<InvalidOperationException>(act);
}
[Fact]
public void NotificationHubPool_ClientFor_ReturnsClient()
{
// Arrange
var globalSettings = new GlobalSettings()
{
NotificationHubPool = new NotificationHubPoolSettings()
{
NotificationHubs = new() {
new() {
ConnectionString = "Endpoint=sb://example.servicebus.windows.net/;SharedAccessKey=example///example=",
HubName = "hub",
RegistrationStartDate = DateTime.UtcNow.AddMinutes(-1),
RegistrationEndDate = DateTime.UtcNow.AddDays(1),
}
}
}
};
var logger = Substitute.For<ILogger<NotificationHubPool>>();
var sut = new NotificationHubPool(logger, globalSettings);
// Act
var client = sut.ClientFor(CoreHelpers.GenerateComb(Guid.NewGuid(), DateTime.UtcNow));
// Assert
Assert.NotNull(client);
}
[Fact]
public void NotificationHubPool_AllClients_ReturnsProxy()
{
// Arrange
var globalSettings = new GlobalSettings()
{
NotificationHubPool = new NotificationHubPoolSettings()
{
NotificationHubs = new() {
new() {
ConnectionString = "connection",
HubName = "hub",
RegistrationStartDate = DateTime.UtcNow,
RegistrationEndDate = DateTime.UtcNow.AddDays(1),
}
}
}
};
var logger = Substitute.For<ILogger<NotificationHubPool>>();
var sut = new NotificationHubPool(logger, globalSettings);
// Act
var proxy = sut.AllClients;
// Assert
Assert.NotNull(proxy);
}
}

View File

@ -0,0 +1,40 @@
using AutoFixture;
using Bit.Core.NotificationHub;
using Bit.Test.Common.AutoFixture;
using Microsoft.Azure.NotificationHubs;
using NSubstitute;
using Xunit;
namespace Bit.Core.Test.NotificationHub;
public class NotificationHubProxyTests
{
private readonly IEnumerable<INotificationHubClient> _clients;
public NotificationHubProxyTests()
{
_clients = new Fixture().WithAutoNSubstitutions().CreateMany<INotificationHubClient>();
}
public static IEnumerable<object[]> ClientMethods =
[
[
(NotificationHubClientProxy c) => c.SendTemplateNotificationAsync(new Dictionary<string, string>() { { "key", "value" } }, "tag"),
(INotificationHubClient c) => c.SendTemplateNotificationAsync(Arg.Is<Dictionary<string, string>>((a) => a.Keys.Count == 1 && a.ContainsKey("key") && a["key"] == "value"), "tag"),
],
];
[Theory]
[MemberData(nameof(ClientMethods))]
public async void CallsAllClients(Func<NotificationHubClientProxy, Task> proxyMethod, Func<INotificationHubClient, Task> clientMethod)
{
var clients = _clients.ToArray();
var proxy = new NotificationHubClientProxy(clients);
await proxyMethod(proxy);
foreach (var client in clients)
{
await clientMethod(client.Received());
}
}
}

View File

@ -1,32 +1,32 @@
using Bit.Core.Repositories;
using Bit.Core.NotificationHub;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Settings;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using NSubstitute;
using Xunit;
namespace Bit.Core.Test.Services;
namespace Bit.Core.Test.NotificationHub;
public class NotificationHubPushNotificationServiceTests
{
private readonly NotificationHubPushNotificationService _sut;
private readonly IInstallationDeviceRepository _installationDeviceRepository;
private readonly GlobalSettings _globalSettings;
private readonly INotificationHubPool _notificationHubPool;
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly ILogger<NotificationsApiPushNotificationService> _logger;
public NotificationHubPushNotificationServiceTests()
{
_installationDeviceRepository = Substitute.For<IInstallationDeviceRepository>();
_globalSettings = new GlobalSettings();
_httpContextAccessor = Substitute.For<IHttpContextAccessor>();
_notificationHubPool = Substitute.For<INotificationHubPool>();
_logger = Substitute.For<ILogger<NotificationsApiPushNotificationService>>();
_sut = new NotificationHubPushNotificationService(
_installationDeviceRepository,
_globalSettings,
_notificationHubPool,
_httpContextAccessor,
_logger
);

View File

@ -1,11 +1,11 @@
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.NotificationHub;
using Bit.Core.Repositories;
using Bit.Core.Settings;
using Microsoft.Extensions.Logging;
using NSubstitute;
using Xunit;
namespace Bit.Core.Test.Services;
namespace Bit.Core.Test.NotificationHub;
public class NotificationHubPushRegistrationServiceTests
{
@ -15,6 +15,7 @@ public class NotificationHubPushRegistrationServiceTests
private readonly IServiceProvider _serviceProvider;
private readonly ILogger<NotificationHubPushRegistrationService> _logger;
private readonly GlobalSettings _globalSettings;
private readonly INotificationHubPool _notificationHubPool;
public NotificationHubPushRegistrationServiceTests()
{
@ -22,10 +23,12 @@ public class NotificationHubPushRegistrationServiceTests
_serviceProvider = Substitute.For<IServiceProvider>();
_logger = Substitute.For<ILogger<NotificationHubPushRegistrationService>>();
_globalSettings = new GlobalSettings();
_notificationHubPool = Substitute.For<INotificationHubPool>();
_sut = new NotificationHubPushRegistrationService(
_installationDeviceRepository,
_globalSettings,
_notificationHubPool,
_serviceProvider,
_logger
);

View File

@ -1,10 +1,10 @@
using Bit.Core.Repositories;
using AutoFixture;
using Bit.Core.Services;
using Bit.Core.Settings;
using Microsoft.AspNetCore.Http;
using Bit.Test.Common.AutoFixture;
using Microsoft.Extensions.Logging;
using NSubstitute;
using Xunit;
using GlobalSettingsCustomization = Bit.Test.Common.AutoFixture.GlobalSettings;
namespace Bit.Core.Test.Services;
@ -12,35 +12,26 @@ public class MultiServicePushNotificationServiceTests
{
private readonly MultiServicePushNotificationService _sut;
private readonly IHttpClientFactory _httpFactory;
private readonly IDeviceRepository _deviceRepository;
private readonly IInstallationDeviceRepository _installationDeviceRepository;
private readonly GlobalSettings _globalSettings;
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly ILogger<MultiServicePushNotificationService> _logger;
private readonly ILogger<RelayPushNotificationService> _relayLogger;
private readonly ILogger<NotificationsApiPushNotificationService> _hubLogger;
private readonly IEnumerable<IPushNotificationService> _services;
private readonly Settings.GlobalSettings _globalSettings;
public MultiServicePushNotificationServiceTests()
{
_httpFactory = Substitute.For<IHttpClientFactory>();
_deviceRepository = Substitute.For<IDeviceRepository>();
_installationDeviceRepository = Substitute.For<IInstallationDeviceRepository>();
_globalSettings = new GlobalSettings();
_httpContextAccessor = Substitute.For<IHttpContextAccessor>();
_logger = Substitute.For<ILogger<MultiServicePushNotificationService>>();
_relayLogger = Substitute.For<ILogger<RelayPushNotificationService>>();
_hubLogger = Substitute.For<ILogger<NotificationsApiPushNotificationService>>();
var fixture = new Fixture().WithAutoNSubstitutions().Customize(new GlobalSettingsCustomization());
_services = fixture.CreateMany<IPushNotificationService>();
_globalSettings = fixture.Create<Settings.GlobalSettings>();
_sut = new MultiServicePushNotificationService(
_httpFactory,
_deviceRepository,
_installationDeviceRepository,
_globalSettings,
_httpContextAccessor,
_services,
_logger,
_relayLogger,
_hubLogger
_globalSettings
);
}

View File

@ -34,33 +34,30 @@ public class CoreHelpersTests
// the comb are working properly
}
public static IEnumerable<object[]> GenerateCombCases = new[]
{
new object[]
{
public static IEnumerable<object[]> GuidSeedCases = [
[
Guid.Parse("a58db474-43d8-42f1-b4ee-0c17647cd0c0"), // Input Guid
new DateTime(2022, 3, 12, 12, 12, 0, DateTimeKind.Utc), // Input Time
Guid.Parse("a58db474-43d8-42f1-b4ee-ae5600c90cc1"), // Expected Comb
},
new object[]
{
],
[
Guid.Parse("f776e6ee-511f-4352-bb28-88513002bdeb"),
new DateTime(2021, 5, 10, 10, 52, 0, DateTimeKind.Utc),
Guid.Parse("f776e6ee-511f-4352-bb28-ad2400b313c1"),
},
new object[]
{
],
[
Guid.Parse("51a25fc7-3cad-497d-8e2f-8d77011648a1"),
new DateTime(1999, 2, 26, 16, 53, 13, DateTimeKind.Utc),
Guid.Parse("51a25fc7-3cad-497d-8e2f-8d77011649cd"),
},
new object[]
{
],
[
Guid.Parse("bfb8f353-3b32-4a9e-bef6-24fe0b54bfb0"),
new DateTime(2024, 10, 20, 1, 32, 16, DateTimeKind.Utc),
Guid.Parse("bfb8f353-3b32-4a9e-bef6-b20f00195780"),
}
};
]
];
public static IEnumerable<object[]> GenerateCombCases = GuidSeedCases.Zip([
Guid.Parse("a58db474-43d8-42f1-b4ee-ae5600c90cc1"), // Expected Comb for each Guid Seed case
Guid.Parse("f776e6ee-511f-4352-bb28-ad2400b313c1"),
Guid.Parse("51a25fc7-3cad-497d-8e2f-8d77011649cd"),
Guid.Parse("bfb8f353-3b32-4a9e-bef6-b20f00195780"),
]).Select((zip) => new object[] { zip.Item1[0], zip.Item1[1], zip.Item2 });
[Theory]
[MemberData(nameof(GenerateCombCases))]
@ -71,6 +68,31 @@ public class CoreHelpersTests
Assert.Equal(expectedComb, comb);
}
[Theory]
[MemberData(nameof(GuidSeedCases))]
public void DateFromComb_WithComb_Success(Guid inputGuid, DateTime inputTime)
{
var comb = CoreHelpers.GenerateComb(inputGuid, inputTime);
var inverseComb = CoreHelpers.DateFromComb(comb);
Assert.Equal(inputTime, inverseComb, TimeSpan.FromMilliseconds(4));
}
[Theory]
[InlineData("00000000-0000-0000-0000-000000000000", 1, 0)]
[InlineData("00000000-0000-0000-0000-000000000001", 1, 0)]
[InlineData("00000000-0000-0000-0000-000000000000", 500, 430)]
[InlineData("00000000-0000-0000-0000-000000000001", 500, 430)]
[InlineData("10000000-0000-0000-0000-000000000001", 500, 454)]
[InlineData("00000000-0000-0100-0000-000000000001", 500, 19)]
public void BinForComb_Success(string guidString, int nbins, int expectedBin)
{
var guid = Guid.Parse(guidString);
var bin = CoreHelpers.BinForComb(guid, nbins);
Assert.Equal(expectedBin, bin);
}
/*
[Fact]
public void ToGuidIdArrayTVP_Success()