Skip to content

Commit 58d6404

Browse files
authored
V14: Reintroduce BackOfficeUserManagerAuditer (#17349)
* Reintroduce BackOfficeUserManagerAuditer * Cleanup legacy code * Notify user logout on logout
1 parent 53a5813 commit 58d6404

File tree

3 files changed

+155
-1
lines changed

3 files changed

+155
-1
lines changed

src/Umbraco.Cms.Api.Management/Controllers/Security/BackOfficeController.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,9 +185,12 @@ public async Task<IActionResult> Authorize(CancellationToken cancellationToken)
185185
[MapToApiVersion("1.0")]
186186
public async Task<IActionResult> Signout(CancellationToken cancellationToken)
187187
{
188-
var userName = await GetUserNameFromAuthCookie();
188+
AuthenticateResult cookieAuthResult = await HttpContext.AuthenticateAsync(Constants.Security.BackOfficeAuthenticationType);
189+
var userName = cookieAuthResult.Principal?.Identity?.Name;
190+
var userId = cookieAuthResult.Principal?.Identity?.GetUserId();
189191

190192
await _backOfficeSignInManager.SignOutAsync();
193+
_backOfficeUserManager.NotifyLogoutSuccess(cookieAuthResult.Principal ?? User, userId);
191194

192195
_logger.LogInformation(
193196
"User {UserName} from IP address {RemoteIpAddress} has logged out",

src/Umbraco.Cms.Api.Management/DependencyInjection/AuditLogBuilderExtensions.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
using Microsoft.Extensions.DependencyInjection;
22
using Umbraco.Cms.Api.Management.Factories;
3+
using Umbraco.Cms.Api.Management.Security;
34
using Umbraco.Cms.Core.DependencyInjection;
5+
using Umbraco.Cms.Core.Notifications;
46

57
namespace Umbraco.Cms.Api.Management.DependencyInjection;
68

@@ -9,6 +11,13 @@ internal static class AuditLogBuilderExtensions
911
internal static IUmbracoBuilder AddAuditLogs(this IUmbracoBuilder builder)
1012
{
1113
builder.Services.AddTransient<IAuditLogPresentationFactory, AuditLogPresentationFactory>();
14+
builder.AddNotificationHandler<UserLoginSuccessNotification, BackOfficeUserManagerAuditer>();
15+
builder.AddNotificationHandler<UserLogoutSuccessNotification, BackOfficeUserManagerAuditer>();
16+
builder.AddNotificationHandler<UserLoginFailedNotification, BackOfficeUserManagerAuditer>();
17+
builder.AddNotificationHandler<UserForgotPasswordRequestedNotification, BackOfficeUserManagerAuditer>();
18+
builder.AddNotificationHandler<UserForgotPasswordChangedNotification, BackOfficeUserManagerAuditer>();
19+
builder.AddNotificationHandler<UserPasswordChangedNotification, BackOfficeUserManagerAuditer>();
20+
builder.AddNotificationHandler<UserPasswordResetNotification, BackOfficeUserManagerAuditer>();
1221

1322
return builder;
1423
}
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
using System.Globalization;
2+
using Umbraco.Cms.Core.Events;
3+
using Umbraco.Cms.Core.Models.Membership;
4+
using Umbraco.Cms.Core.Notifications;
5+
using Umbraco.Cms.Core.Services;
6+
using Umbraco.Cms.Web.Common.Security;
7+
using Umbraco.Extensions;
8+
9+
namespace Umbraco.Cms.Api.Management.Security;
10+
11+
/// <summary>
12+
/// Binds to notifications to write audit logs for the <see cref="BackOfficeUserManager" />
13+
/// </summary>
14+
internal sealed class BackOfficeUserManagerAuditer :
15+
INotificationHandler<UserLoginSuccessNotification>,
16+
INotificationHandler<UserLogoutSuccessNotification>,
17+
INotificationHandler<UserLoginFailedNotification>,
18+
INotificationHandler<UserForgotPasswordRequestedNotification>,
19+
INotificationHandler<UserForgotPasswordChangedNotification>,
20+
INotificationHandler<UserPasswordChangedNotification>,
21+
INotificationHandler<UserPasswordResetNotification>
22+
{
23+
private readonly IAuditService _auditService;
24+
private readonly IUserService _userService;
25+
26+
public BackOfficeUserManagerAuditer(IAuditService auditService, IUserService userService)
27+
{
28+
_auditService = auditService;
29+
_userService = userService;
30+
}
31+
32+
public void Handle(UserForgotPasswordChangedNotification notification) =>
33+
WriteAudit(
34+
notification.PerformingUserId,
35+
notification.AffectedUserId,
36+
notification.IpAddress,
37+
"umbraco/user/password/forgot/change",
38+
"password forgot/change");
39+
40+
public void Handle(UserForgotPasswordRequestedNotification notification) =>
41+
WriteAudit(
42+
notification.PerformingUserId,
43+
notification.AffectedUserId,
44+
notification.IpAddress,
45+
"umbraco/user/password/forgot/request",
46+
"password forgot/request");
47+
48+
public void Handle(UserLoginFailedNotification notification) =>
49+
WriteAudit(
50+
notification.PerformingUserId,
51+
null,
52+
notification.IpAddress,
53+
"umbraco/user/sign-in/failed",
54+
"login failed");
55+
56+
public void Handle(UserLoginSuccessNotification notification)
57+
=> WriteAudit(
58+
notification.PerformingUserId,
59+
notification.AffectedUserId,
60+
notification.IpAddress,
61+
"umbraco/user/sign-in/login",
62+
"login success");
63+
64+
public void Handle(UserLogoutSuccessNotification notification)
65+
=> WriteAudit(
66+
notification.PerformingUserId,
67+
notification.AffectedUserId,
68+
notification.IpAddress,
69+
"umbraco/user/sign-in/logout",
70+
"logout success");
71+
72+
public void Handle(UserPasswordChangedNotification notification) =>
73+
WriteAudit(
74+
notification.PerformingUserId,
75+
notification.AffectedUserId,
76+
notification.IpAddress,
77+
"umbraco/user/password/change",
78+
"password change");
79+
80+
public void Handle(UserPasswordResetNotification notification) =>
81+
WriteAudit(
82+
notification.PerformingUserId,
83+
notification.AffectedUserId,
84+
notification.IpAddress,
85+
"umbraco/user/password/reset",
86+
"password reset");
87+
88+
private static string FormatEmail(IMembershipUser? user) =>
89+
user is null ? string.Empty : user.Email.IsNullOrWhiteSpace() ? string.Empty : $"<{user.Email}>";
90+
91+
private void WriteAudit(
92+
string performingId,
93+
string? affectedId,
94+
string ipAddress,
95+
string eventType,
96+
string eventDetails)
97+
{
98+
int? performingIdAsInt = ParseUserId(performingId);
99+
int? affectedIdAsInt = ParseUserId(affectedId);
100+
101+
WriteAudit(performingIdAsInt, affectedIdAsInt, ipAddress, eventType, eventDetails);
102+
}
103+
104+
private static int? ParseUserId(string? id)
105+
=> int.TryParse(id, NumberStyles.Integer, CultureInfo.InvariantCulture, out var isAsInt) ? isAsInt : null;
106+
107+
private void WriteAudit(
108+
int? performingId,
109+
int? affectedId,
110+
string ipAddress,
111+
string eventType,
112+
string eventDetails)
113+
{
114+
var performingDetails = "User UNKNOWN:0";
115+
if (performingId.HasValue)
116+
{
117+
IUser? performingUser = _userService.GetUserById(performingId.Value);
118+
performingDetails = performingUser is null
119+
? $"User UNKNOWN:{performingId.Value}"
120+
: $"User \"{performingUser.Name}\" {FormatEmail(performingUser)}";
121+
}
122+
123+
var affectedDetails = "User UNKNOWN:0";
124+
if (affectedId.HasValue)
125+
{
126+
IUser? affectedUser = _userService.GetUserById(affectedId.Value);
127+
affectedDetails = affectedUser is null
128+
? $"User UNKNOWN:{affectedId.Value}"
129+
: $"User \"{affectedUser.Name}\" {FormatEmail(affectedUser)}";
130+
}
131+
132+
_auditService.Write(
133+
performingId ?? 0,
134+
performingDetails,
135+
ipAddress,
136+
DateTime.UtcNow,
137+
affectedId ?? 0,
138+
affectedDetails,
139+
eventType,
140+
eventDetails);
141+
}
142+
}

0 commit comments

Comments
 (0)