Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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