Skip to content

Commit

Permalink
V15: Notification Hub (umbraco#17776)
Browse files Browse the repository at this point in the history
* Initial stab at how this could look

* Authorization PoC wip

* Add connection manager

* Add DI to its own class

* Use enum instead of string

* Use groups

* Refactor group management into its own service

* Update a users groups when it's saved

* Add saved events

* Wire up deleted notifications

* Ensure update date and create date is the same

* Cleanup

* Minor cleanup

* Remove unusued usings

* Move route to constant

* Add docstrings to server event router

* Fix and suppress warnings

* Refactor to authorizer pattern

* Update EventType

* Remove unused enums

* Add trashed events

* Notify current user that they've been updated

* Add broadcast

We don't need it, but seems like a thing that a server event router should be able to do.

* Add ServerEventRouterTests

* Add ServerEventUserManagerTests

* Use TimeProvider

* Remove principal null check

* Don't assign event type

* Minor cleanup

* Rename AuthorizedEventSources

* Change permission for relations

* Exctract event authorization into its own service

* Add some tests

* Update name

* Add forgotten file

* Rmember to add to DI
  • Loading branch information
nikolajlauridsen authored Jan 10, 2025
1 parent 7932eb9 commit aaad9c0
Show file tree
Hide file tree
Showing 51 changed files with 1,709 additions and 4 deletions.
4 changes: 2 additions & 2 deletions Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@
TODO: Fix and remove overrides:
[NU5104] Warning As Error: A stable release of a package should not have a prerelease dependency. Either modify the version spec of dependency
-->
<NoWarn>$(NoWarn),NU5104</NoWarn>
<WarningsNotAsErrors>$(WarningsNotAsErrors),NU5104</WarningsNotAsErrors>
<NoWarn>$(NoWarn),NU5104,SA1309</NoWarn>
<WarningsNotAsErrors>$(WarningsNotAsErrors),NU5104,SA1600</WarningsNotAsErrors>
</PropertyGroup>

<!-- SourceLink -->
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
using System.Text.Json.Serialization;
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Api.Management.ServerEvents;
using Umbraco.Cms.Api.Management.ServerEvents.Authorizers;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Notifications;
using Umbraco.Cms.Core.ServerEvents;

namespace Umbraco.Cms.Api.Management.DependencyInjection;

internal static class ServerEventExtensions
{
internal static IUmbracoBuilder AddServerEvents(this IUmbracoBuilder builder)
{
builder.Services.AddSingleton<IUserConnectionManager, UserConnectionManager>();
builder.Services.AddSingleton<IServerEventRouter, ServerEventRouter>();
builder.Services.AddSingleton<IServerEventUserManager, ServerEventUserManager>();
builder.Services.AddSingleton<IServerEventAuthorizationService, ServerEventAuthorizationService>();
builder.AddNotificationAsyncHandler<UserSavedNotification, UserConnectionRefresher>();

builder
.AddEvents()
.AddAuthorizers();

return builder;
}

private static IUmbracoBuilder AddEvents(this IUmbracoBuilder builder)
{
builder.AddNotificationAsyncHandler<ContentSavedNotification, ServerEventSender>();
builder.AddNotificationAsyncHandler<ContentTypeSavedNotification, ServerEventSender>();
builder.AddNotificationAsyncHandler<MediaSavedNotification, ServerEventSender>();
builder.AddNotificationAsyncHandler<MediaTypeSavedNotification, ServerEventSender>();
builder.AddNotificationAsyncHandler<MemberSavedNotification, ServerEventSender>();
builder.AddNotificationAsyncHandler<MemberTypeSavedNotification, ServerEventSender>();
builder.AddNotificationAsyncHandler<MemberGroupSavedNotification, ServerEventSender>();
builder.AddNotificationAsyncHandler<DataTypeSavedNotification, ServerEventSender>();
builder.AddNotificationAsyncHandler<LanguageSavedNotification, ServerEventSender>();
builder.AddNotificationAsyncHandler<ScriptSavedNotification, ServerEventSender>();
builder.AddNotificationAsyncHandler<StylesheetSavedNotification, ServerEventSender>();
builder.AddNotificationAsyncHandler<TemplateSavedNotification, ServerEventSender>();
builder.AddNotificationAsyncHandler<DictionaryItemSavedNotification, ServerEventSender>();
builder.AddNotificationAsyncHandler<DomainSavedNotification, ServerEventSender>();
builder.AddNotificationAsyncHandler<PartialViewSavedNotification, ServerEventSender>();
builder.AddNotificationAsyncHandler<PublicAccessEntrySavedNotification, ServerEventSender>();
builder.AddNotificationAsyncHandler<RelationSavedNotification, ServerEventSender>();
builder.AddNotificationAsyncHandler<RelationTypeSavedNotification, ServerEventSender>();
builder.AddNotificationAsyncHandler<UserGroupSavedNotification, ServerEventSender>();
builder.AddNotificationAsyncHandler<UserSavedNotification, ServerEventSender>();
builder.AddNotificationAsyncHandler<WebhookSavedNotification, ServerEventSender>();

builder.AddNotificationAsyncHandler<ContentDeletedNotification, ServerEventSender>();
builder.AddNotificationAsyncHandler<ContentTypeDeletedNotification, ServerEventSender>();
builder.AddNotificationAsyncHandler<MediaDeletedNotification, ServerEventSender>();
builder.AddNotificationAsyncHandler<MediaTypeDeletedNotification, ServerEventSender>();
builder.AddNotificationAsyncHandler<MemberDeletedNotification, ServerEventSender>();
builder.AddNotificationAsyncHandler<MemberTypeDeletedNotification, ServerEventSender>();
builder.AddNotificationAsyncHandler<MemberGroupDeletedNotification, ServerEventSender>();
builder.AddNotificationAsyncHandler<DataTypeDeletedNotification, ServerEventSender>();
builder.AddNotificationAsyncHandler<LanguageDeletedNotification, ServerEventSender>();
builder.AddNotificationAsyncHandler<ScriptDeletedNotification, ServerEventSender>();
builder.AddNotificationAsyncHandler<StylesheetDeletedNotification, ServerEventSender>();
builder.AddNotificationAsyncHandler<TemplateDeletedNotification, ServerEventSender>();
builder.AddNotificationAsyncHandler<DictionaryItemDeletedNotification, ServerEventSender>();
builder.AddNotificationAsyncHandler<DomainDeletedNotification, ServerEventSender>();
builder.AddNotificationAsyncHandler<PartialViewDeletedNotification, ServerEventSender>();
builder.AddNotificationAsyncHandler<PublicAccessEntryDeletedNotification, ServerEventSender>();
builder.AddNotificationAsyncHandler<RelationDeletedNotification, ServerEventSender>();
builder.AddNotificationAsyncHandler<RelationTypeDeletedNotification, ServerEventSender>();
builder.AddNotificationAsyncHandler<UserGroupDeletedNotification, ServerEventSender>();
builder.AddNotificationAsyncHandler<UserDeletedNotification, ServerEventSender>();
builder.AddNotificationAsyncHandler<WebhookDeletedNotification, ServerEventSender>();

builder.AddNotificationAsyncHandler<ContentMovedToRecycleBinNotification, ServerEventSender>();
builder.AddNotificationAsyncHandler<MediaMovedToRecycleBinNotification, ServerEventSender>();

return builder;
}

private static IUmbracoBuilder AddAuthorizers(this IUmbracoBuilder builder)
{
builder.EventSourceAuthorizers()
.Append<DocumentEventAuthorizer>()
.Append<DocumentTypeEventAuthorizer>()
.Append<MediaEventAuthorizer>()
.Append<MediaTypeEventAuthorizer>()
.Append<MemberEventAuthorizer>()
.Append<MemberGroupEventAuthorizer>()
.Append<MemberTypeEventAuthorizer>()
.Append<DataTypeEventAuthorizer>()
.Append<LanguageEventAuthorizer>()
.Append<ScriptEventAuthorizer>()
.Append<StylesheetEventAuthorizer>()
.Append<TemplateEventAuthorizer>()
.Append<DictionaryItemEventAuthorizer>()
.Append<DomainEventAuthorizer>()
.Append<PartialViewEventAuthorizer>()
.Append<PublicAccessEntryEventAuthorizer>()
.Append<RelationEventAuthorizer>()
.Append<RelationTypeEventAuthorizer>()
.Append<UserGroupEventAuthorizer>()
.Append<UserEventAuthorizer>()
.Append<WebhookEventAuthorizer>();
return builder;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ public static IUmbracoBuilder AddUmbracoManagementApi(this IUmbracoBuilder build
.AddCorsPolicy()
.AddWebhooks()
.AddPreview()
.AddServerEvents()
.AddPasswordConfiguration()
.AddSupplemenataryLocalizedTextFileSources()
.AddUserData()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@

using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Api.Management.ViewModels.DataType;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.PropertyEditors;
using Umbraco.Cms.Core.Serialization;
Expand All @@ -16,17 +18,35 @@ public class DataTypePresentationFactory : IDataTypePresentationFactory
private readonly PropertyEditorCollection _propertyEditorCollection;
private readonly IDataValueEditorFactory _dataValueEditorFactory;
private readonly IConfigurationEditorJsonSerializer _configurationEditorJsonSerializer;
private readonly TimeProvider _timeProvider;

public DataTypePresentationFactory(
IDataTypeContainerService dataTypeContainerService,
PropertyEditorCollection propertyEditorCollection,
IDataValueEditorFactory dataValueEditorFactory,
IConfigurationEditorJsonSerializer configurationEditorJsonSerializer)
IConfigurationEditorJsonSerializer configurationEditorJsonSerializer,
TimeProvider timeProvider)
{
_dataTypeContainerService = dataTypeContainerService;
_propertyEditorCollection = propertyEditorCollection;
_dataValueEditorFactory = dataValueEditorFactory;
_configurationEditorJsonSerializer = configurationEditorJsonSerializer;
_timeProvider = timeProvider;
}

[Obsolete("Use constructor that takes a TimeProvider")]
public DataTypePresentationFactory(
IDataTypeContainerService dataTypeContainerService,
PropertyEditorCollection propertyEditorCollection,
IDataValueEditorFactory dataValueEditorFactory,
IConfigurationEditorJsonSerializer configurationEditorJsonSerializer)
: this(
dataTypeContainerService,
propertyEditorCollection,
dataValueEditorFactory,
configurationEditorJsonSerializer,
StaticServiceProvider.Instance.GetRequiredService<TimeProvider>())
{
}

/// <inheritdoc />
Expand All @@ -44,14 +64,16 @@ public async Task<Attempt<IDataType, DataTypeOperationStatus>> CreateAsync(Creat
return Attempt.FailWithStatus<IDataType, DataTypeOperationStatus>(parentAttempt.Status, new DataType(new VoidEditor(_dataValueEditorFactory), _configurationEditorJsonSerializer));
}

DateTime createDate = _timeProvider.GetLocalNow().DateTime;
var dataType = new DataType(editor, _configurationEditorJsonSerializer)
{
Name = requestModel.Name,
EditorUiAlias = requestModel.EditorUiAlias,
DatabaseType = GetEditorValueStorageType(editor),
ConfigurationData = MapConfigurationData(requestModel, editor),
ParentId = parentAttempt.Result,
CreateDate = DateTime.Now,
CreateDate = createDate,
UpdateDate = createDate,
};

if (requestModel.Id.HasValue)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Api.Management.Controllers.Security;
using Umbraco.Cms.Api.Management.ServerEvents;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Hosting;
Expand Down Expand Up @@ -54,6 +55,7 @@ public void CreateRoutes(IEndpointRouteBuilder endpoints)
case RuntimeLevel.Run:
MapMinimalBackOffice(endpoints);
endpoints.MapHub<BackofficeHub>(_umbracoPathSegment + Constants.Web.BackofficeSignalRHub);
endpoints.MapHub<ServerEventHub>(_umbracoPathSegment + Constants.Web.ServerEventSignalRHub);
break;
case RuntimeLevel.BootFailed:
case RuntimeLevel.Unknown:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using Microsoft.AspNetCore.Authorization;
using Umbraco.Cms.Core;
using Umbraco.Cms.Web.Common.Authorization;

namespace Umbraco.Cms.Api.Management.ServerEvents.Authorizers;

public class DataTypeEventAuthorizer : EventSourcePolicyAuthorizer
{
public DataTypeEventAuthorizer(IAuthorizationService authorizationService) : base(authorizationService)
{
}

public override IEnumerable<string> AuthorizableEventSources => [Constants.ServerEvents.EventSource.DataType];

protected override string Policy => AuthorizationPolicies.TreeAccessDataTypes;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using Microsoft.AspNetCore.Authorization;
using Umbraco.Cms.Core;
using Umbraco.Cms.Web.Common.Authorization;

namespace Umbraco.Cms.Api.Management.ServerEvents.Authorizers;

public class DictionaryItemEventAuthorizer : EventSourcePolicyAuthorizer
{
public DictionaryItemEventAuthorizer(IAuthorizationService authorizationService) : base(authorizationService)
{
}

public override IEnumerable<string> AuthorizableEventSources => [Constants.ServerEvents.EventSource.DictionaryItem];

protected override string Policy => AuthorizationPolicies.TreeAccessDictionary;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using Microsoft.AspNetCore.Authorization;
using Umbraco.Cms.Core;
using Umbraco.Cms.Web.Common.Authorization;

namespace Umbraco.Cms.Api.Management.ServerEvents.Authorizers;

public class DocumentEventAuthorizer : EventSourcePolicyAuthorizer
{
public DocumentEventAuthorizer(IAuthorizationService authorizationService) : base(authorizationService)
{
}


public override IEnumerable<string> AuthorizableEventSources => [Constants.ServerEvents.EventSource.Document];

protected override string Policy => AuthorizationPolicies.TreeAccessDocuments;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using Microsoft.AspNetCore.Authorization;
using Umbraco.Cms.Core;
using Umbraco.Cms.Web.Common.Authorization;

namespace Umbraco.Cms.Api.Management.ServerEvents.Authorizers;

public class DocumentTypeEventAuthorizer : EventSourcePolicyAuthorizer
{
public DocumentTypeEventAuthorizer(IAuthorizationService authorizationService) : base(authorizationService)
{
}

public override IEnumerable<string> AuthorizableEventSources => [Constants.ServerEvents.EventSource.DocumentType];

protected override string Policy => AuthorizationPolicies.TreeAccessDocumentTypes;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using Microsoft.AspNetCore.Authorization;
using Umbraco.Cms.Core;
using Umbraco.Cms.Web.Common.Authorization;

namespace Umbraco.Cms.Api.Management.ServerEvents.Authorizers;

public class DomainEventAuthorizer : EventSourcePolicyAuthorizer
{
public DomainEventAuthorizer(IAuthorizationService authorizationService) : base(authorizationService)
{
}

public override IEnumerable<string> AuthorizableEventSources => [Constants.ServerEvents.EventSource.Domain];

protected override string Policy => AuthorizationPolicies.TreeAccessDocuments;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using Microsoft.AspNetCore.Authorization;
using Umbraco.Cms.Core;
using Umbraco.Cms.Web.Common.Authorization;

namespace Umbraco.Cms.Api.Management.ServerEvents.Authorizers;

public class LanguageEventAuthorizer : EventSourcePolicyAuthorizer
{
public LanguageEventAuthorizer(IAuthorizationService authorizationService) : base(authorizationService)
{
}

public override IEnumerable<string> AuthorizableEventSources => [Constants.ServerEvents.EventSource.Language];

protected override string Policy => AuthorizationPolicies.TreeAccessLanguages;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using Microsoft.AspNetCore.Authorization;
using Umbraco.Cms.Core;
using Umbraco.Cms.Web.Common.Authorization;

namespace Umbraco.Cms.Api.Management.ServerEvents.Authorizers;

public class MediaEventAuthorizer : EventSourcePolicyAuthorizer
{
public MediaEventAuthorizer(IAuthorizationService authorizationService) : base(authorizationService)
{
}

public override IEnumerable<string> AuthorizableEventSources => [Constants.ServerEvents.EventSource.Media];

protected override string Policy => AuthorizationPolicies.TreeAccessMediaOrMediaTypes;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using Microsoft.AspNetCore.Authorization;
using Umbraco.Cms.Core;
using Umbraco.Cms.Web.Common.Authorization;

namespace Umbraco.Cms.Api.Management.ServerEvents.Authorizers;

public class MediaTypeEventAuthorizer : EventSourcePolicyAuthorizer
{
public MediaTypeEventAuthorizer(IAuthorizationService authorizationService) : base(authorizationService)
{
}

public override IEnumerable<string> AuthorizableEventSources => [Constants.ServerEvents.EventSource.MediaType];

protected override string Policy => AuthorizationPolicies.TreeAccessMediaTypes;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using Microsoft.AspNetCore.Authorization;
using Umbraco.Cms.Core;
using Umbraco.Cms.Web.Common.Authorization;

namespace Umbraco.Cms.Api.Management.ServerEvents.Authorizers;

public class MemberEventAuthorizer : EventSourcePolicyAuthorizer
{
public MemberEventAuthorizer(IAuthorizationService authorizationService) : base(authorizationService)
{
}

public override IEnumerable<string> AuthorizableEventSources => [Constants.ServerEvents.EventSource.Member];

protected override string Policy => AuthorizationPolicies.TreeAccessMembersOrMemberTypes;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using Microsoft.AspNetCore.Authorization;
using Umbraco.Cms.Core;
using Umbraco.Cms.Web.Common.Authorization;

namespace Umbraco.Cms.Api.Management.ServerEvents.Authorizers;

public class MemberGroupEventAuthorizer : EventSourcePolicyAuthorizer
{
public MemberGroupEventAuthorizer(IAuthorizationService authorizationService) : base(authorizationService)
{
}

public override IEnumerable<string> AuthorizableEventSources => [Constants.ServerEvents.EventSource.MemberGroup];

protected override string Policy => AuthorizationPolicies.TreeAccessMemberGroups;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using Microsoft.AspNetCore.Authorization;
using Umbraco.Cms.Core;
using Umbraco.Cms.Web.Common.Authorization;

namespace Umbraco.Cms.Api.Management.ServerEvents.Authorizers;

public class MemberTypeEventAuthorizer : EventSourcePolicyAuthorizer
{
public MemberTypeEventAuthorizer(IAuthorizationService authorizationService) : base(authorizationService)
{
}

public override IEnumerable<string> AuthorizableEventSources => [Constants.ServerEvents.EventSource.MemberType];

protected override string Policy => AuthorizationPolicies.TreeAccessMemberTypes;
}
Loading

0 comments on commit aaad9c0

Please sign in to comment.