Reputation: 131
I'm trying to restrict users in Umbraco so that users with the "collarDatabase" role can only add members to the "Collar_Database" member group. If a user attempts to add a member to any other group, the operation should fail, and no changes should be saved. I’m using MemberSavingNotification to cancel the save if unauthorized groups are detected. The problem is that the handler triggers multiple times for the same request and the member changes are still saved even after calling notification.CancelOperation(). The UI shows first that is saved, followed by three error messages that the user is unauthorized. How can I ensure the handler runs only once per request and that the save is completely blocked if unauthorized groups are detected?
Here is my handler.cs:
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Concurrent;
using System.Linq;
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Notifications;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services;
public class RestrictMemberGroupEditHandler : INotificationHandler<MemberSavingNotification>
{
private readonly ILogger<RestrictMemberGroupEditHandler> _logger;
private readonly IMemberService _memberService;
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
private static readonly ConcurrentDictionary<string, bool> ProcessedRequestIds = new ConcurrentDictionary<string, bool>();
public RestrictMemberGroupEditHandler(
ILogger<RestrictMemberGroupEditHandler> logger,
IMemberService memberService,
IBackOfficeSecurityAccessor backOfficeSecurityAccessor)
{
_logger = logger;
_memberService = memberService;
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
}
public void Handle(MemberSavingNotification notification)
{
// Get a unique identifier for the current request
string requestId = GetRequestId();
if (ProcessedRequestIds.ContainsKey(requestId))
{
_logger.LogInformation("Skipping execution for RequestId {RequestId} as it has already been processed.", requestId);
return;
}
ProcessedRequestIds.TryAdd(requestId, true);
var currentUser = _backOfficeSecurityAccessor.BackOfficeSecurity?.CurrentUser;
if (currentUser == null)
{
_logger.LogWarning("No current backend user found.");
return;
}
_logger.LogInformation("RestrictMemberGroupEditHandler triggered for user: {UserName}", currentUser.Name);
if (!currentUser.Groups.Any(g => g.Alias == "collarDatabase"))
{
_logger.LogInformation("User {UserName} is not in the 'Collar_Database' role. No restriction applied.", currentUser.Name);
return;
}
foreach (var member in notification.SavedEntities)
{
var assignedGroups = _memberService.GetAllRoles(member.Username).ToList();
_logger.LogInformation("Groups being assigned to member {MemberName}: {Groups}",
member.Name,
string.Join(", ", assignedGroups));
// Check for unauthorized groups
var unauthorizedGroups = assignedGroups.Where(group => group != "Collar_Database").ToList();
if (unauthorizedGroups.Any())
{
_logger.LogWarning("User {UserName} attempted to add member {MemberName} to unauthorized groups: {Groups}",
currentUser.Name,
member.Name,
string.Join(", ", unauthorizedGroups));
notification.CancelOperation(new EventMessage(
"Access Denied",
$"You can only assign members to the 'Collar_Database' group. Unauthorized groups: {string.Join(", ", unauthorizedGroups)}",
EventMessageType.Error));
return;
}
}
ProcessedRequestIds.TryRemove(requestId, out _);
}
private string GetRequestId()
{
return System.Guid.NewGuid().ToString();
}
}
Here is my composer:
using Umbraco.Cms.Core.Composing;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Notifications;
public class RestrictMemberGroupEditComposer : IComposer
{
public void Compose(IUmbracoBuilder builder)
{
builder.AddNotificationHandler<MemberSavingNotification, RestrictMemberGroupEditHandler>();
}
}
Here’s the log from when John Smith tries to add John Doe to an unauthorized group. .
{"@t":"2024-12-09T01:04:51.369Z","@mt":"RestrictMemberGroupEditHandler triggered for user: {UserName}","UserName":"John Smith"}
{"@t":"2024-12-09T01:04:51.372Z","@mt":"Groups being assigned to member {MemberName}: {Groups}","MemberName":"John Doe","Groups":"example-group, Collar_Database"}
{"@t":"2024-12-09T01:04:51.372Z","@mt":"User {UserName} attempted to add member {MemberName} to unauthorized groups: {Groups}","UserName":"John Smith","MemberName":"John Doe","Groups":"example-group"}
{"@t":"2024-12-09T01:04:51.405Z","@mt":"RestrictMemberGroupEditHandler triggered for user: {UserName}","UserName":"John Smith"}
{"@t":"2024-12-09T01:04:51.406Z","@mt":"Groups being assigned to member {MemberName}: {Groups}","MemberName":"John Doe","Groups":"example-group, Collar_Database"}
{"@t":"2024-12-09T01:04:51.407Z","@mt":"User {UserName} attempted to add member {MemberName} to unauthorized groups: {Groups}","UserName":"John Smith","MemberName":"John Doe","Groups":"example-group"}
Upvotes: 0
Views: 38
Reputation: 210
Just by the look of it, I can see in documentation that it seems that MemberSavedNotification is called when the member already has been saved, so your code will work to late. It says:
The MemberSavedNotification, which is triggered whenever a Member is saved.
This notification is triggered when any Member is saved.
But there are another one in the same member notification, which you perhaps can take a look at, its called:
MemberSavingNotification
Membersaving should go off, before its actually saved.
MemberSavingNotification(IEnumerable<IMember>, EventMessages)
Initializes a new instance of the MemberSavingNotification.
Declaration
public MemberSavingNotification(IEnumerable<IMember> target, EventMessages messages)
Parameters
Type Name Description
IEnumerable<IMember> target
Gets the collection of IMember objects being saved.
EventMessages messages
Initializes a new instance of the EventMessages.
If nothing of it works, you could just let it save, and then just delete it afterwards. Not a pretty solution, though.
Upvotes: 0