mirror of
https://github.com/bitwarden/server
synced 2025-12-06 00:03:34 +00:00
[PM-24011] Create new policy sync push notification (#6594)
* create new policy sync push notification * CR feedback * add tests, fix typo
This commit is contained in:
@@ -4,6 +4,8 @@ using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models;
|
||||
using Bit.Core.Platform.Push;
|
||||
using Bit.Core.Services;
|
||||
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.Implementations;
|
||||
@@ -16,19 +18,22 @@ public class SavePolicyCommand : ISavePolicyCommand
|
||||
private readonly IReadOnlyDictionary<PolicyType, IPolicyValidator> _policyValidators;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
private readonly IPostSavePolicySideEffect _postSavePolicySideEffect;
|
||||
private readonly IPushNotificationService _pushNotificationService;
|
||||
|
||||
public SavePolicyCommand(IApplicationCacheService applicationCacheService,
|
||||
IEventService eventService,
|
||||
IPolicyRepository policyRepository,
|
||||
IEnumerable<IPolicyValidator> policyValidators,
|
||||
TimeProvider timeProvider,
|
||||
IPostSavePolicySideEffect postSavePolicySideEffect)
|
||||
IPostSavePolicySideEffect postSavePolicySideEffect,
|
||||
IPushNotificationService pushNotificationService)
|
||||
{
|
||||
_applicationCacheService = applicationCacheService;
|
||||
_eventService = eventService;
|
||||
_policyRepository = policyRepository;
|
||||
_timeProvider = timeProvider;
|
||||
_postSavePolicySideEffect = postSavePolicySideEffect;
|
||||
_pushNotificationService = pushNotificationService;
|
||||
|
||||
var policyValidatorsDict = new Dictionary<PolicyType, IPolicyValidator>();
|
||||
foreach (var policyValidator in policyValidators)
|
||||
@@ -75,6 +80,8 @@ public class SavePolicyCommand : ISavePolicyCommand
|
||||
await _policyRepository.UpsertAsync(policy);
|
||||
await _eventService.LogPolicyEventAsync(policy, EventType.Policy_Updated);
|
||||
|
||||
await PushPolicyUpdateToClients(policy.OrganizationId, policy);
|
||||
|
||||
return policy;
|
||||
}
|
||||
|
||||
@@ -152,4 +159,17 @@ public class SavePolicyCommand : ISavePolicyCommand
|
||||
var currentPolicy = savedPoliciesDict.GetValueOrDefault(policyUpdate.Type);
|
||||
return (savedPoliciesDict, currentPolicy);
|
||||
}
|
||||
|
||||
Task PushPolicyUpdateToClients(Guid organizationId, Policy policy) => this._pushNotificationService.PushAsync(new PushNotification<SyncPolicyPushNotification>
|
||||
{
|
||||
Type = PushType.PolicyChanged,
|
||||
Target = NotificationTarget.Organization,
|
||||
TargetId = organizationId,
|
||||
ExcludeCurrentContext = false,
|
||||
Payload = new SyncPolicyPushNotification
|
||||
{
|
||||
Policy = policy,
|
||||
OrganizationId = organizationId
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@ using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyUpdateEvents.Int
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models;
|
||||
using Bit.Core.Platform.Push;
|
||||
using Bit.Core.Services;
|
||||
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.Implementations;
|
||||
@@ -15,7 +17,8 @@ public class VNextSavePolicyCommand(
|
||||
IPolicyRepository policyRepository,
|
||||
IEnumerable<IPolicyUpdateEvent> policyUpdateEventHandlers,
|
||||
TimeProvider timeProvider,
|
||||
IPolicyEventHandlerFactory policyEventHandlerFactory)
|
||||
IPolicyEventHandlerFactory policyEventHandlerFactory,
|
||||
IPushNotificationService pushNotificationService)
|
||||
: IVNextSavePolicyCommand
|
||||
{
|
||||
|
||||
@@ -74,7 +77,7 @@ public class VNextSavePolicyCommand(
|
||||
policy.RevisionDate = timeProvider.GetUtcNow().UtcDateTime;
|
||||
|
||||
await policyRepository.UpsertAsync(policy);
|
||||
|
||||
await PushPolicyUpdateToClients(policyUpdateRequest.OrganizationId, policy);
|
||||
return policy;
|
||||
}
|
||||
|
||||
@@ -192,4 +195,17 @@ public class VNextSavePolicyCommand(
|
||||
var savedPoliciesDict = savedPolicies.ToDictionary(p => p.Type);
|
||||
return savedPoliciesDict;
|
||||
}
|
||||
|
||||
Task PushPolicyUpdateToClients(Guid organizationId, Policy policy) => pushNotificationService.PushAsync(new PushNotification<SyncPolicyPushNotification>
|
||||
{
|
||||
Type = PushType.PolicyChanged,
|
||||
Target = NotificationTarget.Organization,
|
||||
TargetId = organizationId,
|
||||
ExcludeCurrentContext = false,
|
||||
Payload = new SyncPolicyPushNotification
|
||||
{
|
||||
Policy = policy,
|
||||
OrganizationId = organizationId
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.NotificationCenter.Enums;
|
||||
|
||||
namespace Bit.Core.Models;
|
||||
@@ -103,3 +104,9 @@ public class LogOutPushNotification
|
||||
public Guid UserId { get; set; }
|
||||
public PushNotificationLogOutReason? Reason { get; set; }
|
||||
}
|
||||
|
||||
public class SyncPolicyPushNotification
|
||||
{
|
||||
public Guid OrganizationId { get; set; }
|
||||
public required Policy Policy { get; set; }
|
||||
}
|
||||
|
||||
@@ -95,5 +95,8 @@ public enum PushType : byte
|
||||
OrganizationBankAccountVerified = 23,
|
||||
|
||||
[NotificationInfo("@bitwarden/team-billing-dev", typeof(Models.ProviderBankAccountVerifiedPushNotification))]
|
||||
ProviderBankAccountVerified = 24
|
||||
ProviderBankAccountVerified = 24,
|
||||
|
||||
[NotificationInfo("@bitwarden/team-admin-console-dev", typeof(Models.SyncPolicyPushNotification))]
|
||||
PolicyChanged = 25,
|
||||
}
|
||||
|
||||
@@ -231,9 +231,26 @@ public class HubHelpers
|
||||
await _hubContext.Clients.User(pendingTasksData.Payload.UserId.ToString())
|
||||
.SendAsync(_receiveMessageMethod, pendingTasksData, cancellationToken);
|
||||
break;
|
||||
case PushType.PolicyChanged:
|
||||
await policyChangedNotificationHandler(notificationJson, cancellationToken);
|
||||
break;
|
||||
default:
|
||||
_logger.LogWarning("Notification type '{NotificationType}' has not been registered in HubHelpers and will not be pushed as as result", notification.Type);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task policyChangedNotificationHandler(string notificationJson, CancellationToken cancellationToken)
|
||||
{
|
||||
var policyData = JsonSerializer.Deserialize<PushNotificationData<SyncPolicyPushNotification>>(notificationJson, _deserializerOptions);
|
||||
if (policyData is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await _hubContext.Clients
|
||||
.Group(NotificationsHub.GetOrganizationGroup(policyData.Payload.OrganizationId))
|
||||
.SendAsync(_receiveMessageMethod, policyData, cancellationToken);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,8 +6,11 @@ using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Implementations;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models;
|
||||
using Bit.Core.Models.Data.Organizations;
|
||||
using Bit.Core.Platform.Push;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Test.AdminConsole.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
@@ -95,7 +98,8 @@ public class SavePolicyCommandTests
|
||||
Substitute.For<IPolicyRepository>(),
|
||||
[new FakeSingleOrgPolicyValidator(), new FakeSingleOrgPolicyValidator()],
|
||||
Substitute.For<TimeProvider>(),
|
||||
Substitute.For<IPostSavePolicySideEffect>()));
|
||||
Substitute.For<IPostSavePolicySideEffect>(),
|
||||
Substitute.For<IPushNotificationService>()));
|
||||
Assert.Contains("Duplicate PolicyValidator for SingleOrg policy", exception.Message);
|
||||
}
|
||||
|
||||
@@ -360,6 +364,103 @@ public class SavePolicyCommandTests
|
||||
.ExecuteSideEffectsAsync(default!, default!, default!);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task VNextSaveAsync_SendsPushNotification(
|
||||
[PolicyUpdate(PolicyType.SingleOrg)] PolicyUpdate policyUpdate,
|
||||
[Policy(PolicyType.SingleOrg, false)] Policy currentPolicy)
|
||||
{
|
||||
// Arrange
|
||||
var fakePolicyValidator = new FakeSingleOrgPolicyValidator();
|
||||
fakePolicyValidator.ValidateAsyncMock(policyUpdate, null).Returns("");
|
||||
var sutProvider = SutProviderFactory([fakePolicyValidator]);
|
||||
var savePolicyModel = new SavePolicyModel(policyUpdate);
|
||||
|
||||
currentPolicy.OrganizationId = policyUpdate.OrganizationId;
|
||||
sutProvider.GetDependency<IPolicyRepository>()
|
||||
.GetByOrganizationIdTypeAsync(policyUpdate.OrganizationId, policyUpdate.Type)
|
||||
.Returns(currentPolicy);
|
||||
|
||||
ArrangeOrganization(sutProvider, policyUpdate);
|
||||
sutProvider.GetDependency<IPolicyRepository>()
|
||||
.GetManyByOrganizationIdAsync(policyUpdate.OrganizationId)
|
||||
.Returns([currentPolicy]);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.VNextSaveAsync(savePolicyModel);
|
||||
|
||||
// Assert
|
||||
await sutProvider.GetDependency<IPushNotificationService>().Received(1)
|
||||
.PushAsync(Arg.Is<PushNotification<SyncPolicyPushNotification>>(p =>
|
||||
p.Type == PushType.PolicyChanged &&
|
||||
p.Target == NotificationTarget.Organization &&
|
||||
p.TargetId == policyUpdate.OrganizationId &&
|
||||
p.ExcludeCurrentContext == false &&
|
||||
p.Payload.OrganizationId == policyUpdate.OrganizationId &&
|
||||
p.Payload.Policy.Id == result.Id &&
|
||||
p.Payload.Policy.Type == policyUpdate.Type &&
|
||||
p.Payload.Policy.Enabled == policyUpdate.Enabled &&
|
||||
p.Payload.Policy.Data == policyUpdate.Data));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task SaveAsync_SendsPushNotification([PolicyUpdate(PolicyType.SingleOrg)] PolicyUpdate policyUpdate)
|
||||
{
|
||||
var fakePolicyValidator = new FakeSingleOrgPolicyValidator();
|
||||
fakePolicyValidator.ValidateAsyncMock(policyUpdate, null).Returns("");
|
||||
var sutProvider = SutProviderFactory([fakePolicyValidator]);
|
||||
|
||||
ArrangeOrganization(sutProvider, policyUpdate);
|
||||
sutProvider.GetDependency<IPolicyRepository>().GetManyByOrganizationIdAsync(policyUpdate.OrganizationId).Returns([]);
|
||||
|
||||
var result = await sutProvider.Sut.SaveAsync(policyUpdate);
|
||||
|
||||
await sutProvider.GetDependency<IPushNotificationService>().Received(1)
|
||||
.PushAsync(Arg.Is<PushNotification<SyncPolicyPushNotification>>(p =>
|
||||
p.Type == PushType.PolicyChanged &&
|
||||
p.Target == NotificationTarget.Organization &&
|
||||
p.TargetId == policyUpdate.OrganizationId &&
|
||||
p.ExcludeCurrentContext == false &&
|
||||
p.Payload.OrganizationId == policyUpdate.OrganizationId &&
|
||||
p.Payload.Policy.Id == result.Id &&
|
||||
p.Payload.Policy.Type == policyUpdate.Type &&
|
||||
p.Payload.Policy.Enabled == policyUpdate.Enabled &&
|
||||
p.Payload.Policy.Data == policyUpdate.Data));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task SaveAsync_ExistingPolicy_SendsPushNotificationWithUpdatedPolicy(
|
||||
[PolicyUpdate(PolicyType.SingleOrg)] PolicyUpdate policyUpdate,
|
||||
[Policy(PolicyType.SingleOrg, false)] Policy currentPolicy)
|
||||
{
|
||||
var fakePolicyValidator = new FakeSingleOrgPolicyValidator();
|
||||
fakePolicyValidator.ValidateAsyncMock(policyUpdate, null).Returns("");
|
||||
var sutProvider = SutProviderFactory([fakePolicyValidator]);
|
||||
|
||||
currentPolicy.OrganizationId = policyUpdate.OrganizationId;
|
||||
sutProvider.GetDependency<IPolicyRepository>()
|
||||
.GetByOrganizationIdTypeAsync(policyUpdate.OrganizationId, policyUpdate.Type)
|
||||
.Returns(currentPolicy);
|
||||
|
||||
ArrangeOrganization(sutProvider, policyUpdate);
|
||||
sutProvider.GetDependency<IPolicyRepository>()
|
||||
.GetManyByOrganizationIdAsync(policyUpdate.OrganizationId)
|
||||
.Returns([currentPolicy]);
|
||||
|
||||
var result = await sutProvider.Sut.SaveAsync(policyUpdate);
|
||||
|
||||
await sutProvider.GetDependency<IPushNotificationService>().Received(1)
|
||||
.PushAsync(Arg.Is<PushNotification<SyncPolicyPushNotification>>(p =>
|
||||
p.Type == PushType.PolicyChanged &&
|
||||
p.Target == NotificationTarget.Organization &&
|
||||
p.TargetId == policyUpdate.OrganizationId &&
|
||||
p.ExcludeCurrentContext == false &&
|
||||
p.Payload.OrganizationId == policyUpdate.OrganizationId &&
|
||||
p.Payload.Policy.Id == result.Id &&
|
||||
p.Payload.Policy.Type == policyUpdate.Type &&
|
||||
p.Payload.Policy.Enabled == policyUpdate.Enabled &&
|
||||
p.Payload.Policy.Data == policyUpdate.Data));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a new SutProvider with the PolicyValidators registered in the Sut.
|
||||
/// </summary>
|
||||
|
||||
@@ -225,6 +225,30 @@ public class HubHelpersTest
|
||||
.Group(Arg.Any<string>());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task SendNotificationToHubAsync_PolicyChanged_SentToOrganizationGroup(
|
||||
SutProvider<HubHelpers> sutProvider,
|
||||
SyncPolicyPushNotification notification,
|
||||
string contextId,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var json = ToNotificationJson(notification, PushType.PolicyChanged, contextId);
|
||||
await sutProvider.Sut.SendNotificationToHubAsync(json, cancellationToken);
|
||||
|
||||
sutProvider.GetDependency<IHubContext<NotificationsHub>>().Clients.Received(0).User(Arg.Any<string>());
|
||||
await sutProvider.GetDependency<IHubContext<NotificationsHub>>().Clients.Received(1)
|
||||
.Group($"Organization_{notification.OrganizationId}")
|
||||
.Received(1)
|
||||
.SendCoreAsync("ReceiveMessage", Arg.Is<object?[]>(objects =>
|
||||
objects.Length == 1 && AssertSyncPolicyPushNotification(notification, objects[0],
|
||||
PushType.PolicyChanged, contextId)),
|
||||
cancellationToken);
|
||||
sutProvider.GetDependency<IHubContext<AnonymousNotificationsHub>>().Clients.Received(0).User(Arg.Any<string>());
|
||||
sutProvider.GetDependency<IHubContext<AnonymousNotificationsHub>>().Clients.Received(0)
|
||||
.Group(Arg.Any<string>());
|
||||
}
|
||||
|
||||
private static string ToNotificationJson(object payload, PushType type, string contextId)
|
||||
{
|
||||
var notification = new PushNotificationData<object>(type, payload, contextId);
|
||||
@@ -247,4 +271,20 @@ public class HubHelpersTest
|
||||
expected.ClientType == pushNotificationData.Payload.ClientType &&
|
||||
expected.RevisionDate == pushNotificationData.Payload.RevisionDate;
|
||||
}
|
||||
|
||||
private static bool AssertSyncPolicyPushNotification(SyncPolicyPushNotification expected, object? actual,
|
||||
PushType type, string contextId)
|
||||
{
|
||||
if (actual is not PushNotificationData<SyncPolicyPushNotification> pushNotificationData)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return pushNotificationData.Type == type &&
|
||||
pushNotificationData.ContextId == contextId &&
|
||||
expected.OrganizationId == pushNotificationData.Payload.OrganizationId &&
|
||||
expected.Policy.Id == pushNotificationData.Payload.Policy.Id &&
|
||||
expected.Policy.Type == pushNotificationData.Payload.Policy.Type &&
|
||||
expected.Policy.Enabled == pushNotificationData.Payload.Policy.Enabled;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user