Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Store user state in Audit Trail (Lombiq Technologies: OFFI-260) #17518

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using OrchardCore.Users.AuditTrail.Services;
using OrchardCore.Users.Events;
using OrchardCore.Users.Handlers;
using OrchardCore.Users.Models;

namespace OrchardCore.Users.AuditTrail.Handlers;

Expand Down Expand Up @@ -59,21 +60,29 @@ public Task IsLockedOutAsync(IUser user)
=> RecordAuditTrailEventAsync(UserAuditTrailEventConfiguration.LogInFailed, user);

public override Task DisabledAsync(UserContext context)
=> RecordAuditTrailEventAsync(UserAuditTrailEventConfiguration.Disabled, context.User, GetCurrentUserId(), GetCurrentUserName());
=> RecordAuditTrailUserEventAsync(UserAuditTrailEventConfiguration.Disabled, context, _httpContextAccessor);

public override Task EnabledAsync(UserContext context)
=> RecordAuditTrailEventAsync(UserAuditTrailEventConfiguration.Enabled, context.User, GetCurrentUserId(), GetCurrentUserName());
=> RecordAuditTrailUserEventAsync(UserAuditTrailEventConfiguration.Enabled, context, _httpContextAccessor);

public override Task CreatedAsync(UserCreateContext context)
=> RecordAuditTrailEventAsync(UserAuditTrailEventConfiguration.Created, context.User, GetCurrentUserId(), GetCurrentUserName());
=> RecordAuditTrailUserEventAsync(UserAuditTrailEventConfiguration.Created, context, _httpContextAccessor);

public override Task UpdatedAsync(UserUpdateContext context)
=> RecordAuditTrailEventAsync(UserAuditTrailEventConfiguration.Updated, context.User, GetCurrentUserId(), GetCurrentUserName());
=> RecordAuditTrailUserEventAsync(UserAuditTrailEventConfiguration.Updated, context, _httpContextAccessor);

public override Task DeletedAsync(UserDeleteContext context)
=> RecordAuditTrailEventAsync(UserAuditTrailEventConfiguration.Deleted, context.User, GetCurrentUserId(), GetCurrentUserName());
=> RecordAuditTrailUserEventAsync(UserAuditTrailEventConfiguration.Deleted, context, _httpContextAccessor);

private async Task RecordAuditTrailEventAsync(string name, IUser user, string userIdActual = "", string userNameActual = "")
public override Task ConfirmedAsync(UserConfirmContext context)
=> RecordAuditTrailUserEventAsync(UserAuditTrailEventConfiguration.Confirmed, context, _httpContextAccessor);

private async Task RecordAuditTrailEventAsync(
string name,
IUser user,
string userIdActual = "",
string userNameActual = "",
bool storeSnapshot = false)
{
var userName = user.UserName;
_userManager ??= _serviceProvider.GetRequiredService<UserManager<IUser>>();
Expand All @@ -94,6 +103,7 @@ private async Task RecordAuditTrailEventAsync(string name, IUser user, string us
{
UserName = userName,
UserId = userId,
User = storeSnapshot ? user as User : null,
};

var context = new AuditTrailContext<AuditTrailUserEvent>
Expand All @@ -120,9 +130,11 @@ public Task<IActionResult> ValidatingLoginAsync(IUser user)
=> Task.FromResult<IActionResult>(null);
#endregion

private string GetCurrentUserName()
=> _httpContextAccessor.HttpContext.User?.Identity?.Name;

private string GetCurrentUserId()
=> _httpContextAccessor.HttpContext.User?.FindFirstValue(ClaimTypes.NameIdentifier);
private Task RecordAuditTrailUserEventAsync(string name, UserContextBase context, IHttpContextAccessor accessor)
=> RecordAuditTrailEventAsync(
name,
context.User,
accessor.HttpContext?.User?.FindFirstValue(ClaimTypes.NameIdentifier),
accessor.HttpContext?.User?.Identity?.Name,
storeSnapshot: true);
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,29 @@
using OrchardCore.Users.Models;

namespace OrchardCore.Users.AuditTrail.Models;

// TODO a future version should also record the User state, enabling diff against users
/// <summary>
/// Event data for an Audit Trail event related to <see cref="Users.Models.User"/>s.
/// </summary>
public class AuditTrailUserEvent
{
/// <summary>
/// Gets or sets the event name.
/// </summary>
public string Name { get; set; } = "User";

/// <summary>
/// Gets or sets a snapshot of the <see cref="Users.Models.User"/> object, if the event modified it somehow.
/// </summary>
public User User { get; set; }

/// <summary>
/// Gets or sets the related user's <see cref="Users.Models.User.UserName"/>.
/// </summary>
public string UserName { get; set; }

/// <summary>
/// Gets or sets the related user's <see cref="Users.Models.User.UserId"/>.
/// </summary>
public string UserId { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ public sealed class UserAuditTrailEventConfiguration : IConfigureOptions<AuditTr
public const string Created = nameof(Created);
public const string Updated = nameof(Updated);
public const string Deleted = nameof(Deleted);
public const string Confirmed = nameof(Confirmed);

public void Configure(AuditTrailOptions options)
{
Expand All @@ -26,6 +27,7 @@ public void Configure(AuditTrailOptions options)
.WithEvent(Disabled, S => S["Disabled"], S => S["A user was disabled."], true)
.WithEvent(Created, S => S["Created"], S => S["A user was created."], true)
.WithEvent(Updated, S => S["Updated"], S => S["A user was updated."], true)
.WithEvent(Deleted, S => S["Deleted"], S => S["A user was deleted."], true);
.WithEvent(Deleted, S => S["Deleted"], S => S["A user was deleted."], true)
.WithEvent(Confirmed, S => S["Confirmed"], S => S["A user was confirmed."], true);
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
@model AuditTrailUserEventViewModel
@using System.Text.Json
@using System.Text.Json.Nodes

<h2 class="h3">@T["User Info"]</h2>

Expand All @@ -12,5 +14,12 @@
<th>@T["Username"]</th>
<td>@Model.UserEvent.UserName</td>
</tr>
@if (JObject.FromObject(Model.UserEvent.User) is { } userJson)
{
<tr>
<th>@T["User Snapshot"]</th>
<td><pre><code>@userJson.ToJsonString(JOptions.Indented)</code></pre></td>
</tr>
}
</tbody>
</table>