From 8199b7710c837f4b9c9539e85a8bbd63aefd20fb Mon Sep 17 00:00:00 2001 From: Stephan Date: Tue, 20 Mar 2018 15:48:18 +0100 Subject: [PATCH] Port v7@2aa0dfb2c5 - WIP --- .../Auditing/AuditEventHandler.cs | 351 ++++++++++++++++++ .../Auditing/IdentityAuditEventArgs.cs | 54 ++- .../Cache/HttpRequestCacheProvider.cs | 2 +- .../Collections/CompositeTypeTypeKey.cs | 61 +++ src/Umbraco.Core/Collections/TypeList.cs | 33 ++ src/Umbraco.Web/UI/Pages/ClientTools.cs | 18 +- 6 files changed, 505 insertions(+), 14 deletions(-) create mode 100644 src/Umbraco.Core/Auditing/AuditEventHandler.cs create mode 100644 src/Umbraco.Core/Collections/CompositeTypeTypeKey.cs create mode 100644 src/Umbraco.Core/Collections/TypeList.cs diff --git a/src/Umbraco.Core/Auditing/AuditEventHandler.cs b/src/Umbraco.Core/Auditing/AuditEventHandler.cs new file mode 100644 index 0000000000..9b38d5b59e --- /dev/null +++ b/src/Umbraco.Core/Auditing/AuditEventHandler.cs @@ -0,0 +1,351 @@ +using System; +using System.Linq; +using System.Text; +using System.Threading; +using System.Web; +using Umbraco.Core.Events; +using Umbraco.Core.Models; +using Umbraco.Core.Models.Membership; +using Umbraco.Core.Security; +using Umbraco.Core.Services; + +namespace Umbraco.Core.Auditing +{ + public sealed class AuditEventHandler : ApplicationEventHandler + { + private IAuditService _auditServiceInstance; + private IUserService _userServiceInstance; + private IEntityService _entityServiceInstance; + + private IUser CurrentPerformingUser + { + get + { + var identity = Thread.CurrentPrincipal?.GetUmbracoIdentity(); + return identity == null + ? new User { Id = 0, Name = "SYSTEM", Email = "" } + : _userServiceInstance.GetUserById(Convert.ToInt32(identity.Id)); + } + } + + private IUser GetPerformingUser(int userId) + { + var found = userId >= 0 ? _userServiceInstance.GetUserById(userId) : null; + return found ?? new User {Id = 0, Name = "SYSTEM", Email = ""}; + } + + private string PerformingIp + { + get + { + var httpContext = HttpContext.Current == null ? (HttpContextBase) null : new HttpContextWrapper(HttpContext.Current); + var ip = httpContext.GetCurrentRequestIpAddress(); + if (ip.ToLowerInvariant().StartsWith("unknown")) ip = ""; + return ip; + } + } + + protected override void ApplicationStarted(UmbracoApplicationBase umbracoApplication, ApplicationContext applicationContext) + { + _auditServiceInstance = applicationContext.Services.AuditService; + _userServiceInstance = applicationContext.Services.UserService; + _entityServiceInstance = applicationContext.Services.EntityService; + + //BackOfficeUserManager.AccountLocked += ; + //BackOfficeUserManager.AccountUnlocked += ; + BackOfficeUserManager.ForgotPasswordRequested += OnForgotPasswordRequest; + BackOfficeUserManager.ForgotPasswordChangedSuccess += OnForgotPasswordChange; + BackOfficeUserManager.LoginFailed += OnLoginFailed; + //BackOfficeUserManager.LoginRequiresVerification += ; + BackOfficeUserManager.LoginSuccess += OnLoginSuccess; + BackOfficeUserManager.LogoutSuccess += OnLogoutSuccess; + BackOfficeUserManager.PasswordChanged += OnPasswordChanged; + BackOfficeUserManager.PasswordReset += OnPasswordReset; + //BackOfficeUserManager.ResetAccessFailedCount += ; + + UserService.SavedUserGroup2 += OnSavedUserGroupWithUsers; + + UserService.SavedUser += OnSavedUser; + UserService.DeletedUser += OnDeletedUser; + UserService.UserGroupPermissionsAssigned += UserGroupPermissionAssigned; + + MemberService.Saved += OnSavedMember; + MemberService.Deleted += OnDeletedMember; + MemberService.AssignedRoles += OnAssignedRoles; + MemberService.RemovedRoles += OnRemovedRoles; + MemberService.Exported += OnMemberExported; + } + + private string FormatEmail(IMember member) + { + return member == null ? string.Empty : member.Email.IsNullOrWhiteSpace() ? "" : $"<{member.Email}>"; + } + + private string FormatEmail(IUser user) + { + return user == null ? string.Empty : user.Email.IsNullOrWhiteSpace() ? "" : $"<{user.Email}>"; + } + + private void OnRemovedRoles(IMemberService sender, RolesEventArgs args) + { + var performingUser = CurrentPerformingUser; + var roles = string.Join(", ", args.Roles); + var members = sender.GetAllMembers(args.MemberIds).ToDictionary(x => x.Id, x => x); + foreach (var id in args.MemberIds) + { + members.TryGetValue(id, out var member); + _auditServiceInstance.Write(performingUser.Id, $"User \"{performingUser.Name}\" {FormatEmail(performingUser)}", PerformingIp, + DateTime.UtcNow, + -1, $"Member {id} \"{member?.Name ?? "(unknown)"}\" {FormatEmail(member)}", + "umbraco/member/roles/removed", $"roles modified, removed {roles}"); + } + } + + private void OnAssignedRoles(IMemberService sender, RolesEventArgs args) + { + var performingUser = CurrentPerformingUser; + var roles = string.Join(", ", args.Roles); + var members = sender.GetAllMembers(args.MemberIds).ToDictionary(x => x.Id, x => x); + foreach (var id in args.MemberIds) + { + members.TryGetValue(id, out var member); + _auditServiceInstance.Write(performingUser.Id, $"User \"{performingUser.Name}\" {FormatEmail(performingUser)}", PerformingIp, + DateTime.UtcNow, + -1, $"Member {id} \"{member?.Name ?? "(unknown)"}\" {FormatEmail(member)}", + "umbraco/member/roles/assigned", $"roles modified, assigned {roles}"); + } + } + + private void OnMemberExported(IMemberService sender, ExportedMemberEventArgs exportedMemberEventArgs) + { + var performingUser = CurrentPerformingUser; + var member = exportedMemberEventArgs.Member; + + _auditServiceInstance.Write(performingUser.Id, $"User \"{performingUser.Name}\" {FormatEmail(performingUser)}", PerformingIp, + DateTime.UtcNow, + -1, $"Member {member.Id} \"{member.Name}\" {FormatEmail(member)}", + "umbraco/member/exported", "exported member data"); + } + + private void OnSavedUserGroupWithUsers(IUserService sender, SaveEventArgs saveEventArgs) + { + var performingUser = CurrentPerformingUser; + foreach (var groupWithUser in saveEventArgs.SavedEntities) + { + var group = groupWithUser.UserGroup; + + var dp = string.Join(", ", ((UserGroup)group).GetPreviouslyDirtyProperties()); + var sections = ((UserGroup)group).WasPropertyDirty("AllowedSections") + ? string.Join(", ", group.AllowedSections) + : null; + var perms = ((UserGroup)group).WasPropertyDirty("Permissions") + ? string.Join(", ", group.Permissions) + : null; + + var sb = new StringBuilder(); + sb.Append($"updating {(string.IsNullOrWhiteSpace(dp) ? "(nothing)" : dp)};"); + if (sections != null) + sb.Append($", assigned sections: {sections}"); + if (perms != null) + { + if (sections != null) + sb.Append(", "); + sb.Append($"default perms: {perms}"); + } + + _auditServiceInstance.Write(performingUser.Id, $"User \"{performingUser.Name}\" {FormatEmail(performingUser)}", PerformingIp, + DateTime.UtcNow, + -1, $"User Group {group.Id} \"{group.Name}\" ({group.Alias})", + "umbraco/user-group/save", $"{sb}"); + + // now audit the users that have changed + + foreach (var user in groupWithUser.RemovedUsers) + { + _auditServiceInstance.Write(performingUser.Id, $"User \"{performingUser.Name}\" {FormatEmail(performingUser)}", PerformingIp, + DateTime.UtcNow, + user.Id, $"User \"{user.Name}\" {FormatEmail(user)}", + "umbraco/user-group/save", $"Removed user \"{user.Name}\" {FormatEmail(user)} from group {group.Id} \"{group.Name}\" ({group.Alias})"); + } + + foreach (var user in groupWithUser.AddedUsers) + { + _auditServiceInstance.Write(performingUser.Id, $"User \"{performingUser.Name}\" {FormatEmail(performingUser)}", PerformingIp, + DateTime.UtcNow, + user.Id, $"User \"{user.Name}\" {FormatEmail(user)}", + "umbraco/user-group/save", $"Added user \"{user.Name}\" {FormatEmail(user)} to group {group.Id} \"{group.Name}\" ({group.Alias})"); + } + } + } + + private void UserGroupPermissionAssigned(IUserService sender, SaveEventArgs saveEventArgs) + { + var performingUser = CurrentPerformingUser; + var perms = saveEventArgs.SavedEntities; + foreach (var perm in perms) + { + var group = sender.GetUserGroupById(perm.UserGroupId); + var assigned = string.Join(", ", perm.AssignedPermissions); + var entity = _entityServiceInstance.Get(perm.EntityId); + + _auditServiceInstance.Write(performingUser.Id, $"User \"{performingUser.Name}\" {FormatEmail(performingUser)}", PerformingIp, + DateTime.UtcNow, + -1, $"User Group {group.Id} \"{group.Name}\" ({group.Alias})", + "umbraco/user-group/permissions-change", $"assigning {(string.IsNullOrWhiteSpace(assigned) ? "(nothing)" : assigned)} on id:{perm.EntityId} \"{entity.Name}\""); + } + } + + private void OnSavedMember(IMemberService sender, SaveEventArgs saveEventArgs) + { + var performingUser = CurrentPerformingUser; + var members = saveEventArgs.SavedEntities; + foreach (var member in members) + { + var dp = string.Join(", ", ((Member) member).GetPreviouslyDirtyProperties()); + + _auditServiceInstance.Write(performingUser.Id, $"User \"{performingUser.Name}\" {FormatEmail(performingUser)}", PerformingIp, + DateTime.UtcNow, + -1, $"Member {member.Id} \"{member.Name}\" {FormatEmail(member)}", + "umbraco/member/save", $"updating {(string.IsNullOrWhiteSpace(dp) ? "(nothing)" : dp)}"); + } + } + + private void OnDeletedMember(IMemberService sender, DeleteEventArgs deleteEventArgs) + { + var performingUser = CurrentPerformingUser; + var members = deleteEventArgs.DeletedEntities; + foreach (var member in members) + { + _auditServiceInstance.Write(performingUser.Id, $"User \"{performingUser.Name}\" {FormatEmail(performingUser)}", PerformingIp, + DateTime.UtcNow, + -1, $"Member {member.Id} \"{member.Name}\" {FormatEmail(member)}", + "umbraco/member/delete", $"delete member id:{member.Id} \"{member.Name}\" {FormatEmail(member)}"); + } + } + + private void OnSavedUser(IUserService sender, SaveEventArgs saveEventArgs) + { + var performingUser = CurrentPerformingUser; + var affectedUsers = saveEventArgs.SavedEntities; + foreach (var affectedUser in affectedUsers) + { + var groups = affectedUser.WasPropertyDirty("Groups") + ? string.Join(", ", affectedUser.Groups.Select(x => x.Alias)) + : null; + + var dp = string.Join(", ", ((User)affectedUser).GetPreviouslyDirtyProperties()); + + _auditServiceInstance.Write(performingUser.Id, $"User \"{performingUser.Name}\" {FormatEmail(performingUser)}", PerformingIp, + DateTime.UtcNow, + affectedUser.Id, $"User \"{affectedUser.Name}\" {FormatEmail(affectedUser)}", + "umbraco/user/save", $"updating {(string.IsNullOrWhiteSpace(dp) ? "(nothing)" : dp)}{(groups == null ? "" : "; groups assigned: " + groups)}"); + } + } + + private void OnDeletedUser(IUserService sender, DeleteEventArgs deleteEventArgs) + { + var performingUser = CurrentPerformingUser; + var affectedUsers = deleteEventArgs.DeletedEntities; + foreach (var affectedUser in affectedUsers) + _auditServiceInstance.Write(performingUser.Id, $"User \"{performingUser.Name}\" {FormatEmail(performingUser)}", PerformingIp, + DateTime.UtcNow, + affectedUser.Id, $"User \"{affectedUser.Name}\" {FormatEmail(affectedUser)}", + "umbraco/user/delete", "delete user"); + } + + private void OnLoginSuccess(object sender, EventArgs args) + { + if (args is IdentityAuditEventArgs identityArgs) + { + var performingUser = GetPerformingUser(identityArgs.PerformingUser); + WriteAudit(performingUser, identityArgs.AffectedUser, identityArgs.IpAddress, "umbraco/user/sign-in/login", "login success"); + } + } + + private void OnLogoutSuccess(object sender, EventArgs args) + { + if (args is IdentityAuditEventArgs identityArgs) + { + var performingUser = GetPerformingUser(identityArgs.PerformingUser); + WriteAudit(performingUser, identityArgs.AffectedUser, identityArgs.IpAddress, "umbraco/user/sign-in/logout", "logout success"); + } + } + + private void OnPasswordReset(object sender, EventArgs args) + { + if (args is IdentityAuditEventArgs identityArgs && identityArgs.PerformingUser >= 0) + { + WriteAudit(identityArgs.PerformingUser, identityArgs.AffectedUser, identityArgs.IpAddress, "umbraco/user/password/reset", "password reset"); + } + } + + private void OnPasswordChanged(object sender, EventArgs args) + { + if (args is IdentityAuditEventArgs identityArgs && identityArgs.PerformingUser >= 0) + { + WriteAudit(identityArgs.PerformingUser, identityArgs.AffectedUser, identityArgs.IpAddress, "umbraco/user/password/change", "password change"); + } + } + + private void OnLoginFailed(object sender, EventArgs args) + { + if (args is IdentityAuditEventArgs identityArgs && identityArgs.PerformingUser >= 0) + { + WriteAudit(identityArgs.PerformingUser, 0, identityArgs.IpAddress, "umbraco/user/sign-in/failed", "login failed", affectedDetails: ""); + } + } + + private void OnForgotPasswordChange(object sender, EventArgs args) + { + if (args is IdentityAuditEventArgs identityArgs && identityArgs.PerformingUser >= 0) + { + WriteAudit(identityArgs.PerformingUser, identityArgs.AffectedUser, identityArgs.IpAddress, "umbraco/user/password/forgot/change", "password forgot/change"); + } + } + + private void OnForgotPasswordRequest(object sender, EventArgs args) + { + if (args is IdentityAuditEventArgs identityArgs && identityArgs.PerformingUser >= 0) + { + WriteAudit(identityArgs.PerformingUser, identityArgs.AffectedUser, identityArgs.IpAddress, "umbraco/user/password/forgot/request", "password forgot/request"); + } + } + + private void WriteAudit(int performingId, int affectedId, string ipAddress, string eventType, string eventDetails, string affectedDetails = null) + { + var performingUser = _userServiceInstance.GetUserById(performingId); + + var performingDetails = performingUser == null + ? $"User UNKNOWN:{performingId}" + : $"User \"{performingUser.Name}\" {FormatEmail(performingUser)}"; + + WriteAudit(performingId, performingDetails, affectedId, ipAddress, eventType, eventDetails, affectedDetails); + } + + private void WriteAudit(IUser performingUser, int affectedId, string ipAddress, string eventType, string eventDetails) + { + var performingDetails = performingUser == null + ? $"User UNKNOWN" + : $"User \"{performingUser.Name}\" {FormatEmail(performingUser)}"; + + WriteAudit(performingUser?.Id ?? 0, performingDetails, affectedId, ipAddress, eventType, eventDetails); + } + + private void WriteAudit(int performingId, string performingDetails, int affectedId, string ipAddress, string eventType, string eventDetails, string affectedDetails = null) + { + if (affectedDetails == null) + { + var affectedUser = _userServiceInstance.GetUserById(affectedId); + affectedDetails = affectedUser == null + ? $"User UNKNOWN:{affectedId}" + : $"User \"{affectedUser.Name}\" {FormatEmail(affectedUser)}"; + } + + _auditServiceInstance.Write(performingId, performingDetails, + ipAddress, + DateTime.UtcNow, + affectedId, affectedDetails, + eventType, eventDetails); + } + } +} diff --git a/src/Umbraco.Core/Auditing/IdentityAuditEventArgs.cs b/src/Umbraco.Core/Auditing/IdentityAuditEventArgs.cs index 14445d461f..c58bb409b0 100644 --- a/src/Umbraco.Core/Auditing/IdentityAuditEventArgs.cs +++ b/src/Umbraco.Core/Auditing/IdentityAuditEventArgs.cs @@ -1,4 +1,5 @@ using System; +using System.ComponentModel; using System.Threading; using System.Web; using Umbraco.Core.Security; @@ -45,12 +46,8 @@ namespace Umbraco.Core.Auditing /// public string Username { get; private set; } - /// - /// Sets the properties on the event being raised, all parameters are optional except for the action being performed - /// - /// An action based on the AuditEvent enum - /// The client's IP address. This is usually automatically set but could be overridden if necessary - /// The Id of the user performing the action (if different from the user affected by the action) + [Obsolete("Use the method that has the affectedUser parameter instead")] + [EditorBrowsable(EditorBrowsableState.Never)] public IdentityAuditEventArgs(AuditEvent action, string ipAddress, int performingUser = -1) { DateTimeUtc = DateTime.UtcNow; @@ -63,6 +60,35 @@ namespace Umbraco.Core.Auditing : performingUser; } + /// + /// Default constructor + /// + /// + /// + /// + /// + /// + public IdentityAuditEventArgs(AuditEvent action, string ipAddress, string comment = null, int performingUser = -1, int affectedUser = -1) + { + DateTimeUtc = DateTime.UtcNow; + Action = action; + + IpAddress = ipAddress; + Comment = comment; + AffectedUser = affectedUser; + + PerformingUser = performingUser == -1 + ? GetCurrentRequestBackofficeUserId() + : performingUser; + } + + /// + /// Creates an instance without a performing or affected user (the id will be set to -1) + /// + /// + /// + /// + /// public IdentityAuditEventArgs(AuditEvent action, string ipAddress, string username, string comment) { DateTimeUtc = DateTime.UtcNow; @@ -71,6 +97,22 @@ namespace Umbraco.Core.Auditing IpAddress = ipAddress; Username = username; Comment = comment; + + PerformingUser = -1; + } + + public IdentityAuditEventArgs(AuditEvent action, string ipAddress, string username, string comment, int performingUser) + { + DateTimeUtc = DateTime.UtcNow; + Action = action; + + IpAddress = ipAddress; + Username = username; + Comment = comment; + + PerformingUser = performingUser == -1 + ? GetCurrentRequestBackofficeUserId() + : performingUser; } /// diff --git a/src/Umbraco.Core/Cache/HttpRequestCacheProvider.cs b/src/Umbraco.Core/Cache/HttpRequestCacheProvider.cs index 4a43dd154f..6f97651042 100644 --- a/src/Umbraco.Core/Cache/HttpRequestCacheProvider.cs +++ b/src/Umbraco.Core/Cache/HttpRequestCacheProvider.cs @@ -145,7 +145,7 @@ namespace Umbraco.Core.Cache #region Insert #endregion - private class NoopLocker : DisposableObject + private class NoopLocker : DisposableObjectSlim { protected override void DisposeResources() { } diff --git a/src/Umbraco.Core/Collections/CompositeTypeTypeKey.cs b/src/Umbraco.Core/Collections/CompositeTypeTypeKey.cs new file mode 100644 index 0000000000..1a4e7ae1a9 --- /dev/null +++ b/src/Umbraco.Core/Collections/CompositeTypeTypeKey.cs @@ -0,0 +1,61 @@ +using System; + +namespace Umbraco.Core.Collections +{ + /// + /// Represents a composite key of (Type, Type) for fast dictionaries. + /// + internal struct CompositeTypeTypeKey : IEquatable + { + /// + /// Initializes a new instance of the struct. + /// + public CompositeTypeTypeKey(Type type1, Type type2) : this() + { + Type1 = type1; + Type2 = type2; + } + + /// + /// Gets the first type. + /// + public Type Type1 { get; private set; } + + /// + /// Gets the second type. + /// + public Type Type2 { get; private set; } + + /// + public bool Equals(CompositeTypeTypeKey other) + { + return Type1 == other.Type1 && Type2 == other.Type2; + } + + /// + public override bool Equals(object obj) + { + var other = obj is CompositeTypeTypeKey ? (CompositeTypeTypeKey)obj : default(CompositeTypeTypeKey); + return Type1 == other.Type1 && Type2 == other.Type2; + } + + public static bool operator ==(CompositeTypeTypeKey key1, CompositeTypeTypeKey key2) + { + return key1.Type1 == key2.Type1 && key1.Type2 == key2.Type2; + } + + public static bool operator !=(CompositeTypeTypeKey key1, CompositeTypeTypeKey key2) + { + return key1.Type1 != key2.Type1 || key1.Type2 != key2.Type2; + } + + /// + public override int GetHashCode() + { + unchecked + { + return (Type1.GetHashCode() * 397) ^ Type2.GetHashCode(); + } + } + } +} diff --git a/src/Umbraco.Core/Collections/TypeList.cs b/src/Umbraco.Core/Collections/TypeList.cs new file mode 100644 index 0000000000..37ca427ba1 --- /dev/null +++ b/src/Umbraco.Core/Collections/TypeList.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; + +namespace Umbraco.Core.Collections +{ + /// + /// Represents a list of types. + /// + /// Types in the list are, or derive from, or implement, the base type. + /// The base type. + internal class TypeList + { + private readonly List _list = new List(); + + /// + /// Adds a type to the list. + /// + /// The type to add. + public void Add() + where T : TBase + { + _list.Add(typeof(T)); + } + + /// + /// Determines whether a type is in the list. + /// + public bool Contains(Type type) + { + return _list.Contains(type); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/UI/Pages/ClientTools.cs b/src/Umbraco.Web/UI/Pages/ClientTools.cs index 0cd59591be..7b59725101 100644 --- a/src/Umbraco.Web/UI/Pages/ClientTools.cs +++ b/src/Umbraco.Web/UI/Pages/ClientTools.cs @@ -46,7 +46,8 @@ namespace Umbraco.Web.UI.Pages { return string.Format(ClientMgrScript + ".reloadContentFrameUrlIfPathLoaded('{0}');", url); } - public static string ReloadLocation { get { return string.Format(ClientMgrScript + ".reloadLocation();"); } } + public static string ReloadLocation { get { return ClientMgrScript + ".reloadLocation();"; } } + public static string ReloadLocationIfMatched { get { return ClientMgrScript + ".reloadLocation('{0}');"; } } public static string ChildNodeCreated = GetMainTree + ".childNodeCreated();"; public static string SyncTree { get { return GetMainTree + ".syncTree('{0}', {1});"; } } public static string ClearTreeCache { get { return GetMainTree + ".clearTreeCache();"; } } @@ -168,16 +169,19 @@ namespace Umbraco.Web.UI.Pages return this; } - /// - /// Reloads location, refreshing what is in the content frame - /// - public ClientTools ReloadLocation() + public ClientTools ReloadLocationIfMatched(string routePath) { - RegisterClientScript(Scripts.ReloadLocation); - + RegisterClientScript(string.Format(Scripts.ReloadLocationIfMatched, routePath)); return this; } + public ClientTools ReloadLocation() + { + RegisterClientScript(Scripts.ReloadLocation); + return this; + } + + private string EnsureUmbracoUrl(string url) { if (url.StartsWith("/") && url.StartsWith(IOHelper.ResolveUrl(SystemDirectories.Umbraco)) == false)