Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,13 @@ internal static class AuditLogBuilderExtensions
internal static IUmbracoBuilder AddAuditLogs(this IUmbracoBuilder builder)
{
builder.Services.AddTransient<IAuditLogPresentationFactory, AuditLogPresentationFactory>();
builder.AddNotificationHandler<UserLoginSuccessNotification, BackOfficeUserManagerAuditer>();
builder.AddNotificationHandler<UserLogoutSuccessNotification, BackOfficeUserManagerAuditer>();
builder.AddNotificationHandler<UserLoginFailedNotification, BackOfficeUserManagerAuditer>();
builder.AddNotificationHandler<UserForgotPasswordRequestedNotification, BackOfficeUserManagerAuditer>();
builder.AddNotificationHandler<UserForgotPasswordChangedNotification, BackOfficeUserManagerAuditer>();
builder.AddNotificationHandler<UserPasswordChangedNotification, BackOfficeUserManagerAuditer>();
builder.AddNotificationHandler<UserPasswordResetNotification, BackOfficeUserManagerAuditer>();
builder.AddNotificationAsyncHandler<UserLoginSuccessNotification, BackOfficeUserManagerAuditer>();
builder.AddNotificationAsyncHandler<UserLogoutSuccessNotification, BackOfficeUserManagerAuditer>();
builder.AddNotificationAsyncHandler<UserLoginFailedNotification, BackOfficeUserManagerAuditer>();
builder.AddNotificationAsyncHandler<UserForgotPasswordRequestedNotification, BackOfficeUserManagerAuditer>();
builder.AddNotificationAsyncHandler<UserForgotPasswordChangedNotification, BackOfficeUserManagerAuditer>();
builder.AddNotificationAsyncHandler<UserPasswordChangedNotification, BackOfficeUserManagerAuditer>();
builder.AddNotificationAsyncHandler<UserPasswordResetNotification, BackOfficeUserManagerAuditer>();

return builder;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
using System.Globalization;
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Models.Membership;
using Umbraco.Cms.Core.Notifications;
Expand All @@ -12,72 +15,86 @@ namespace Umbraco.Cms.Api.Management.Security;
/// Binds to notifications to write audit logs for the <see cref="BackOfficeUserManager" />
/// </summary>
internal sealed class BackOfficeUserManagerAuditer :
INotificationHandler<UserLoginSuccessNotification>,
INotificationHandler<UserLogoutSuccessNotification>,
INotificationHandler<UserLoginFailedNotification>,
INotificationHandler<UserForgotPasswordRequestedNotification>,
INotificationHandler<UserForgotPasswordChangedNotification>,
INotificationHandler<UserPasswordChangedNotification>,
INotificationHandler<UserPasswordResetNotification>
INotificationAsyncHandler<UserLoginSuccessNotification>,
INotificationAsyncHandler<UserLogoutSuccessNotification>,
INotificationAsyncHandler<UserLoginFailedNotification>,
INotificationAsyncHandler<UserForgotPasswordRequestedNotification>,
INotificationAsyncHandler<UserForgotPasswordChangedNotification>,
INotificationAsyncHandler<UserPasswordChangedNotification>,
INotificationAsyncHandler<UserPasswordResetNotification>
{
private readonly IAuditService _auditService;
private readonly IAuditEntryService _auditEntryService;
private readonly IUserService _userService;

public BackOfficeUserManagerAuditer(IAuditService auditService, IUserService userService)
/// <summary>
/// Initializes a new instance of the <see cref="BackOfficeUserManagerAuditer"/> class.
/// </summary>
/// <param name="auditEntryService">The audit entry service.</param>
/// <param name="userService">The user service.</param>
public BackOfficeUserManagerAuditer(
IAuditEntryService auditEntryService,
IUserService userService)
{
_auditService = auditService;
_auditEntryService = auditEntryService;
_userService = userService;
}

public void Handle(UserForgotPasswordChangedNotification notification) =>
/// <inheritdoc />
public Task HandleAsync(UserForgotPasswordChangedNotification notification, CancellationToken cancellationToken) =>
WriteAudit(
notification.PerformingUserId,
notification.AffectedUserId,
notification.IpAddress,
"umbraco/user/password/forgot/change",
"password forgot/change");

public void Handle(UserForgotPasswordRequestedNotification notification) =>
/// <inheritdoc />
public Task HandleAsync(UserForgotPasswordRequestedNotification notification, CancellationToken cancellationToken) =>
WriteAudit(
notification.PerformingUserId,
notification.AffectedUserId,
notification.IpAddress,
"umbraco/user/password/forgot/request",
"password forgot/request");

public void Handle(UserLoginFailedNotification notification) =>
/// <inheritdoc />
public Task HandleAsync(UserLoginFailedNotification notification, CancellationToken cancellationToken) =>
WriteAudit(
notification.PerformingUserId,
null,
notification.IpAddress,
"umbraco/user/sign-in/failed",
"login failed");

public void Handle(UserLoginSuccessNotification notification)
/// <inheritdoc />
public Task HandleAsync(UserLoginSuccessNotification notification, CancellationToken cancellationToken)
=> WriteAudit(
notification.PerformingUserId,
notification.AffectedUserId,
notification.IpAddress,
"umbraco/user/sign-in/login",
"login success");

public void Handle(UserLogoutSuccessNotification notification)
/// <inheritdoc />
public Task HandleAsync(UserLogoutSuccessNotification notification, CancellationToken cancellationToken)
=> WriteAudit(
notification.PerformingUserId,
notification.AffectedUserId,
notification.IpAddress,
"umbraco/user/sign-in/logout",
"logout success");

public void Handle(UserPasswordChangedNotification notification) =>
/// <inheritdoc />
public Task HandleAsync(UserPasswordChangedNotification notification, CancellationToken cancellationToken) =>
WriteAudit(
notification.PerformingUserId,
notification.AffectedUserId,
notification.IpAddress,
"umbraco/user/password/change",
"password change");

public void Handle(UserPasswordResetNotification notification) =>
/// <inheritdoc />
public Task HandleAsync(UserPasswordResetNotification notification, CancellationToken cancellationToken) =>
WriteAudit(
notification.PerformingUserId,
notification.AffectedUserId,
Expand All @@ -88,7 +105,7 @@ public void Handle(UserPasswordResetNotification notification) =>
private static string FormatEmail(IMembershipUser? user) =>
user is null ? string.Empty : user.Email.IsNullOrWhiteSpace() ? string.Empty : $"<{user.Email}>";

private void WriteAudit(
private async Task WriteAudit(
string performingId,
string? affectedId,
string ipAddress,
Expand All @@ -98,43 +115,47 @@ private void WriteAudit(
int? performingIdAsInt = ParseUserId(performingId);
int? affectedIdAsInt = ParseUserId(affectedId);

WriteAudit(performingIdAsInt, affectedIdAsInt, ipAddress, eventType, eventDetails);
await WriteAudit(performingIdAsInt, affectedIdAsInt, ipAddress, eventType, eventDetails);
}

private static int? ParseUserId(string? id)
=> int.TryParse(id, NumberStyles.Integer, CultureInfo.InvariantCulture, out var isAsInt) ? isAsInt : null;

private void WriteAudit(
private async Task WriteAudit(
int? performingId,
int? affectedId,
string ipAddress,
string eventType,
string eventDetails)
{
Guid? performingKey = null;
var performingDetails = "User UNKNOWN:0";
if (performingId.HasValue)
{
IUser? performingUser = _userService.GetUserById(performingId.Value);
performingKey = performingUser?.Key;
performingDetails = performingUser is null
? $"User UNKNOWN:{performingId.Value}"
: $"User \"{performingUser.Name}\" {FormatEmail(performingUser)}";
}

Guid? affectedKey = null;
var affectedDetails = "User UNKNOWN:0";
if (affectedId.HasValue)
{
IUser? affectedUser = _userService.GetUserById(affectedId.Value);
affectedKey = affectedUser?.Key;
affectedDetails = affectedUser is null
? $"User UNKNOWN:{affectedId.Value}"
: $"User \"{affectedUser.Name}\" {FormatEmail(affectedUser)}";
}

_auditService.Write(
performingId ?? 0,
await _auditEntryService.WriteAsync(
performingKey ?? Constants.Security.UnknownUserKey,
performingDetails,
ipAddress,
DateTime.UtcNow,
affectedId ?? 0,
affectedKey ?? Constants.Security.UnknownUserKey,
affectedDetails,
eventType,
eventDetails);
Expand Down
5 changes: 5 additions & 0 deletions src/Umbraco.Core/Constants-Security.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ public static class Security
/// </remarks>
public const int UnknownUserId = 0;

/// <summary>
/// The key for the 'unknown' user.
/// </summary>
public static readonly Guid UnknownUserKey = Guid.Empty;

/// <summary>
/// The name of the 'unknown' user.
/// </summary>
Expand Down
39 changes: 33 additions & 6 deletions src/Umbraco.Core/Events/RelateOnCopyNotificationHandler.cs
Original file line number Diff line number Diff line change
@@ -1,31 +1,53 @@
// Copyright (c) Umbraco.
// See LICENSE for more details.

using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Notifications;
using Umbraco.Cms.Core.Services;

namespace Umbraco.Cms.Core.Events;

public class RelateOnCopyNotificationHandler : INotificationHandler<ContentCopiedNotification>
public class RelateOnCopyNotificationHandler :
INotificationHandler<ContentCopiedNotification>,
INotificationAsyncHandler<ContentCopiedNotification>
{
private readonly IAuditService _auditService;
private readonly IUserIdKeyResolver _userIdKeyResolver;
private readonly IRelationService _relationService;

public RelateOnCopyNotificationHandler(IRelationService relationService, IAuditService auditService)
public RelateOnCopyNotificationHandler(
IRelationService relationService,
IAuditService auditService,
IUserIdKeyResolver userIdKeyResolver)
{
_relationService = relationService;
_auditService = auditService;
_userIdKeyResolver = userIdKeyResolver;
}

public void Handle(ContentCopiedNotification notification)
[Obsolete("Use the non-obsolete constructor instead. Scheduled for removal in V19.")]
public RelateOnCopyNotificationHandler(
IRelationService relationService,
IAuditService auditService)
: this(
relationService,
auditService,
StaticServiceProvider.Instance.GetRequiredService<IUserIdKeyResolver>())
{
}

/// <inheritdoc />
public async Task HandleAsync(ContentCopiedNotification notification, CancellationToken cancellationToken)
{
if (notification.RelateToOriginal == false)
{
return;
}

IRelationType? relationType = _relationService.GetRelationTypeByAlias(Constants.Conventions.RelationTypes.RelateDocumentOnCopyAlias);
IRelationType? relationType =
_relationService.GetRelationTypeByAlias(Constants.Conventions.RelationTypes.RelateDocumentOnCopyAlias);

if (relationType == null)
{
Expand All @@ -43,11 +65,16 @@ public void Handle(ContentCopiedNotification notification)
var relation = new Relation(notification.Original.Id, notification.Copy.Id, relationType);
_relationService.Save(relation);

_auditService.Add(
Guid writerKey = await _userIdKeyResolver.GetAsync(notification.Copy.WriterId);
await _auditService.AddAsync(
AuditType.Copy,
notification.Copy.WriterId,
writerKey,
notification.Copy.Id,
UmbracoObjectTypes.Document.GetName() ?? string.Empty,
$"Copied content with Id: '{notification.Copy.Id}' related to original content with Id: '{notification.Original.Id}'");
}

[Obsolete("Use the INotificationAsyncHandler.HandleAsync implementation instead. Scheduled for removal in V19.")]
public void Handle(ContentCopiedNotification notification) =>
HandleAsync(notification, CancellationToken.None).GetAwaiter().GetResult();
}
Loading