From de6c57a7ee13d688b4824d448c6a2cd91aeb90c5 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Fri, 31 Jan 2025 11:14:13 -0500 Subject: [PATCH] add user impersonation --- Oqtane.Client/Modules/Admin/Users/Edit.razor | 26 ++++++ .../Resources/Modules/Admin/Users/Edit.resx | 6 ++ Oqtane.Server/Managers/UserManager.cs | 3 +- Oqtane.Server/Pages/Impersonate.cshtml | 3 + Oqtane.Server/Pages/Impersonate.cshtml.cs | 79 +++++++++++++++++++ Oqtane.Server/Pages/Login.cshtml.cs | 18 ++++- Oqtane.Shared/Security/UserSecurity.cs | 7 +- 7 files changed, 138 insertions(+), 4 deletions(-) create mode 100644 Oqtane.Server/Pages/Impersonate.cshtml create mode 100644 Oqtane.Server/Pages/Impersonate.cshtml.cs diff --git a/Oqtane.Client/Modules/Admin/Users/Edit.razor b/Oqtane.Client/Modules/Admin/Users/Edit.razor index e4aedaa0..13bff401 100644 --- a/Oqtane.Client/Modules/Admin/Users/Edit.razor +++ b/Oqtane.Client/Modules/Admin/Users/Edit.razor @@ -6,6 +6,7 @@ @inject IProfileService ProfileService @inject ISettingService SettingService @inject IFileService FileService +@inject IServiceProvider ServiceProvider @inject IStringLocalizer Localizer @inject IStringLocalizer SharedLocalizer @@ -130,6 +131,10 @@ @SharedLocalizer["Cancel"] + @if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin) && PageState.Runtime != Shared.Runtime.Hybrid && !ishost) + { + + } @if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host) && isdeleted == "True") { @@ -152,6 +157,7 @@ private string isdeleted; private string lastlogin; private string lastipaddress; + private bool ishost = false; private List profiles; private Dictionary userSettings; @@ -186,6 +192,7 @@ isdeleted = user.IsDeleted.ToString(); lastlogin = string.Format("{0:MMM dd yyyy HH:mm:ss}", user.LastLoginOn); lastipaddress = user.LastIPAddress; + ishost = UserSecurity.ContainsRole(user.Roles, RoleNames.Host); userSettings = user.Settings; createdby = user.CreatedBy; @@ -267,6 +274,25 @@ } } + private async Task ImpersonateUser() + { + try + { + await logger.LogInformation(LogFunction.Security, "User {Username} Impersonated By Administrator {Administrator}", username, PageState.User.Username); + + // post back to the server so that the cookies are set correctly + var interop = new Interop(JSRuntime); + var fields = new { __RequestVerificationToken = SiteState.AntiForgeryToken, username = username, returnurl = PageState.Alias.Path }; + string url = Utilities.TenantUrl(PageState.Alias, "/pages/impersonate/"); + await interop.SubmitForm(url, fields); + } + catch (Exception ex) + { + await logger.LogError(ex, "Error Impersonating User {Username} {Error}", username, ex.Message); + AddModuleMessage(Localizer["Error.User.Impersonate"], MessageType.Error); + } + } + private async Task DeleteUser() { try diff --git a/Oqtane.Client/Resources/Modules/Admin/Users/Edit.resx b/Oqtane.Client/Resources/Modules/Admin/Users/Edit.resx index 29aed594..3dbd1d1e 100644 --- a/Oqtane.Client/Resources/Modules/Admin/Users/Edit.resx +++ b/Oqtane.Client/Resources/Modules/Admin/Users/Edit.resx @@ -204,4 +204,10 @@ Are You Sure You Wish To Permanently Delete This User? + + Impersonate + + + Unable To Impersonate User + \ No newline at end of file diff --git a/Oqtane.Server/Managers/UserManager.cs b/Oqtane.Server/Managers/UserManager.cs index 694bfd03..84679d23 100644 --- a/Oqtane.Server/Managers/UserManager.cs +++ b/Oqtane.Server/Managers/UserManager.cs @@ -12,6 +12,7 @@ using Oqtane.Enums; using Oqtane.Infrastructure; using Oqtane.Models; using Oqtane.Repository; +using Oqtane.Security; using Oqtane.Shared; namespace Oqtane.Managers @@ -369,7 +370,7 @@ namespace Oqtane.Managers if (user != null) { // ensure user is registered for site - if (user.Roles.Contains(RoleNames.Registered)) + if (UserSecurity.ContainsRole(user.Roles, RoleNames.Registered)) { user.IsAuthenticated = true; user.LastLoginOn = DateTime.UtcNow; diff --git a/Oqtane.Server/Pages/Impersonate.cshtml b/Oqtane.Server/Pages/Impersonate.cshtml new file mode 100644 index 00000000..169e999e --- /dev/null +++ b/Oqtane.Server/Pages/Impersonate.cshtml @@ -0,0 +1,3 @@ +@page "/pages/impersonate" +@namespace Oqtane.Pages +@model Oqtane.Pages.ImpersonateModel diff --git a/Oqtane.Server/Pages/Impersonate.cshtml.cs b/Oqtane.Server/Pages/Impersonate.cshtml.cs new file mode 100644 index 00000000..951c1a80 --- /dev/null +++ b/Oqtane.Server/Pages/Impersonate.cshtml.cs @@ -0,0 +1,79 @@ +using System.Net; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Oqtane.Enums; +using Oqtane.Extensions; +using Oqtane.Infrastructure; +using Oqtane.Managers; +using Oqtane.Security; +using Oqtane.Shared; + +namespace Oqtane.Pages +{ + public class ImpersonateModel : PageModel + { + private readonly UserManager _identityUserManager; + private readonly SignInManager _identitySignInManager; + private readonly IUserManager _userManager; + private readonly ILogManager _logger; + + public ImpersonateModel(UserManager identityUserManager, SignInManager identitySignInManager, IUserManager userManager, ILogManager logger) + { + _identityUserManager = identityUserManager; + _identitySignInManager = identitySignInManager; + _userManager = userManager; + _logger = logger; + } + + public async Task OnPostAsync(string username, string returnurl) + { + if (User.IsInRole(RoleNames.Admin) && !string.IsNullOrEmpty(username)) + { + bool validuser = false; + IdentityUser identityuser = await _identityUserManager.FindByNameAsync(username); + if (identityuser != null) + { + var alias = HttpContext.GetAlias(); + var user = _userManager.GetUser(identityuser.UserName, alias.SiteId); + if (user != null && !user.IsDeleted && UserSecurity.ContainsRole(user.Roles, RoleNames.Registered) && !UserSecurity.ContainsRole(user.Roles, RoleNames.Host)) + { + validuser = true; + } + } + + if (validuser) + { + _logger.Log(LogLevel.Information, this, LogFunction.Security, "User {Username} Successfully Impersonated By Administrator {Administrator}", username, User.Identity.Name); + + // note that .NET Identity uses a hardcoded ApplicationScheme of "Identity.Application" in SignInAsync + await _identitySignInManager.SignInAsync(identityuser, false); + } + else + { + _logger.Log(LogLevel.Error, this, LogFunction.Security, "Impersonation By Administrator {Administrator} Failed For User {Username} ", User.Identity.Name, username); + } + } + else + { + _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Attempt To Impersonate User {Username} By User {User}", username, User.Identity.Name); + } + + if (returnurl == null) + { + returnurl = ""; + } + else + { + returnurl = WebUtility.UrlDecode(returnurl); + } + if (!returnurl.StartsWith("/")) + { + returnurl = "/" + returnurl; + } + + return LocalRedirect(Url.Content("~" + returnurl)); + } + } +} diff --git a/Oqtane.Server/Pages/Login.cshtml.cs b/Oqtane.Server/Pages/Login.cshtml.cs index 87fceedb..b1b01201 100644 --- a/Oqtane.Server/Pages/Login.cshtml.cs +++ b/Oqtane.Server/Pages/Login.cshtml.cs @@ -4,8 +4,11 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; +using Oqtane.Enums; using Oqtane.Extensions; +using Oqtane.Infrastructure; using Oqtane.Managers; +using Oqtane.Security; using Oqtane.Shared; namespace Oqtane.Pages @@ -16,12 +19,14 @@ namespace Oqtane.Pages private readonly UserManager _identityUserManager; private readonly SignInManager _identitySignInManager; private readonly IUserManager _userManager; + private readonly ILogManager _logger; - public LoginModel(UserManager identityUserManager, SignInManager identitySignInManager, IUserManager userManager) + public LoginModel(UserManager identityUserManager, SignInManager identitySignInManager, IUserManager userManager, ILogManager logger) { _identityUserManager = identityUserManager; _identitySignInManager = identitySignInManager; _userManager = userManager; + _logger = logger; } public async Task OnPostAsync(string username, string password, bool remember, string returnurl) @@ -37,7 +42,7 @@ namespace Oqtane.Pages { var alias = HttpContext.GetAlias(); var user = _userManager.GetUser(identityuser.UserName, alias.SiteId); - if (user != null && !user.IsDeleted) + if (user != null && !user.IsDeleted && UserSecurity.ContainsRole(user.Roles, RoleNames.Registered)) { validuser = true; } @@ -48,7 +53,16 @@ namespace Oqtane.Pages { // note that .NET Identity uses a hardcoded ApplicationScheme of "Identity.Application" in SignInAsync await _identitySignInManager.SignInAsync(identityuser, remember); + _logger.Log(LogLevel.Information, this, LogFunction.Security, "Login Successful For User {Username}", username); } + else + { + _logger.Log(LogLevel.Error, this, LogFunction.Security, "Login Failed For User {Username}", username); + } + } + else + { + _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Attempt To Login User {Username}", username); } if (returnurl == null) diff --git a/Oqtane.Shared/Security/UserSecurity.cs b/Oqtane.Shared/Security/UserSecurity.cs index 4d954037..7503238d 100644 --- a/Oqtane.Shared/Security/UserSecurity.cs +++ b/Oqtane.Shared/Security/UserSecurity.cs @@ -72,6 +72,11 @@ namespace Oqtane.Security return isAuthorized; } + public static bool ContainsRole(string roles, string roleName) + { + return roles.Split(';', StringSplitOptions.RemoveEmptyEntries).Contains(roleName); + } + public static bool ContainsRole(List permissions, string permissionName, string roleName) { return permissions.Any(item => item.PermissionName == permissionName && item.RoleName == roleName); @@ -101,7 +106,7 @@ namespace Oqtane.Security identity.AddClaim(new Claim(ClaimTypes.Name, user.Username)); identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, user.UserId.ToString())); identity.AddClaim(new Claim(Constants.SiteKeyClaimType, alias.SiteKey)); - if (user.Roles.Contains(RoleNames.Host)) + if (ContainsRole(user.Roles, RoleNames.Host)) { // host users are site admins by default identity.AddClaim(new Claim(ClaimTypes.Role, RoleNames.Host));