fix #4284 - handle user role effective and expiry date

This commit is contained in:
sbwalker 2024-07-22 21:09:35 -04:00
parent 8b2e55a969
commit 8ca2f0a49f
11 changed files with 153 additions and 74 deletions

View File

@ -11,7 +11,7 @@
<div class="search-result-container">
<div class="row">
<div class="col">
<form method="post" @formname="SearchInputForm" @onsubmit="Search" data-enhance>
<form method="post" @formname="SearchResultsForm" @onsubmit="Search" data-enhance>
<div class="input-group mb-3">
<span class="input-group-text">@Localizer["SearchLabel"]</span>
<input type="hidden" name="@Constants.RequestVerificationToken" value="@SiteState.AntiForgeryToken" />
@ -79,7 +79,7 @@
private SearchResults _searchResults;
private bool _loading;
[SupplyParameterFromForm(FormName = "SearchInputForm")]
[SupplyParameterFromForm(FormName = "SearchResultsForm")]
public string KeyWords { get => ""; set => _keywords = value; }
protected override async Task OnInitializedAsync()

View File

@ -31,7 +31,7 @@ namespace Oqtane.Services
public async Task<User> GetUserAsync(string username, int siteId)
{
return await GetUserAsync(username, "", siteId);
return await GetJsonAsync<User>($"{Apiurl}/username/{username}?siteid={siteId}");
}
public async Task<User> GetUserAsync(string username, string email, int siteId)

View File

@ -4,16 +4,15 @@
@inherits ThemeControlBase
@inject IStringLocalizer<Search> Localizer
@inject NavigationManager NavigationManager
@inject IHttpContextAccessor HttpContext
@if (_searchResultsPage != null)
{
<span class="app-search @CssClass">
<form method="post" class="app-form-inline" @formname="@($"{Position}SearchForm")" @onsubmit="@PerformSearch" data-enhance>
<form method="post" class="app-form-inline" @formname="@($"SearchForm")" @onsubmit="@PerformSearch" data-enhance>
<input type="hidden" name="@Constants.RequestVerificationToken" value="@SiteState.AntiForgeryToken" />
<input type="text" name="keywords" maxlength="50"
class="form-control d-inline-block pe-5 shadow-none"
@bind-value="_keywords"
@bind="_keywords"
placeholder="@Localizer["SearchPlaceHolder"]"
aria-label="Search" />
<button type="submit" class="btn btn-search">
@ -32,12 +31,15 @@
[Parameter]
public string CssClass { get; set; }
[Parameter]
public string Position { get; set; } = "Main";
[Parameter]
public string SearchResultPagePath { get; set; } = "search";
[CascadingParameter]
HttpContext HttpContext { get; set; }
[SupplyParameterFromForm(FormName = "SearchForm")]
public string KeyWords { get => ""; set => _keywords = value; }
protected override void OnInitialized()
{
if(!string.IsNullOrEmpty(SearchResultPagePath))
@ -46,16 +48,11 @@
}
}
protected override void OnParametersSet()
{
}
private void PerformSearch()
{
var keywords = HttpContext.HttpContext.Request.Form["keywords"];
if (_searchResultsPage != null)
{
var url = NavigateUrl(_searchResultsPage.Path, $"q={keywords}");
var url = NavigateUrl(_searchResultsPage.Path, $"q={_keywords}");
NavigationManager.NavigateTo(url);
}
}

View File

@ -39,7 +39,7 @@
}
_comment += " -->";
if (PageState.RenderMode != RenderModes.Static || ModuleState.RenderMode != RenderModes.Static)
if (PageState.RenderMode == RenderModes.Static && ModuleState.RenderMode == RenderModes.Interactive)
{
// trim PageState to mitigate page bloat caused by Blazor serializing/encrypting state when crossing render mode boundaries
// please note that this performance optimization results in the PageState.Pages property not being available for use in Interactive components

View File

@ -2,6 +2,7 @@
@using System.Net
@using Microsoft.AspNetCore.Http
@using System.Globalization
@using System.Security.Claims
@namespace Oqtane.UI
@inject AuthenticationStateProvider AuthenticationStateProvider
@inject SiteState SiteState
@ -159,7 +160,8 @@
if (authState.User.Identity.IsAuthenticated && authState.User.Claims.Any(item => item.Type == "sitekey" && item.Value == SiteState.Alias.SiteKey))
{
// get user
user = await UserService.GetUserAsync(authState.User.Identity.Name, SiteState.Alias.SiteId);
var userid = int.Parse(authState.User.Claims.First(item => item.Type == ClaimTypes.NameIdentifier).Value);
user = await UserService.GetUserAsync(userid, SiteState.Alias.SiteId);
if (user != null)
{
user.IsAuthenticated = authState.User.Identity.IsAuthenticated;

View File

@ -72,15 +72,13 @@ namespace Oqtane.Controllers
}
}
// GET api/<controller>/name/{name}/{email}?siteid=x
[HttpGet("name/{name}/{email}")]
public User Get(string name, string email, string siteid)
// GET api/<controller>/username/{username}?siteid=x
[HttpGet("username/{username}")]
public User Get(string username, string siteid)
{
if (int.TryParse(siteid, out int SiteId) && SiteId == _tenantManager.GetAlias().SiteId)
{
name = (name == "-") ? "" : name;
email = (email == "-") ? "" : email;
User user = _userManager.GetUser(name, email, SiteId);
User user = _userManager.GetUser(username, SiteId);
if (user == null)
{
HttpContext.Response.StatusCode = (int)HttpStatusCode.NotFound;
@ -95,7 +93,36 @@ namespace Oqtane.Controllers
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized User Get Attempt {Username} {Email} {SiteId}", name, email, siteid);
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized User Get Attempt {Username} {SiteId}", username, siteid);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
return null;
}
}
// GET api/<controller>/name/{username}/{email}?siteid=x
[HttpGet("search/{username}/{email}")]
public User Get(string username, string email, string siteid)
{
if (int.TryParse(siteid, out int SiteId) && SiteId == _tenantManager.GetAlias().SiteId)
{
username = (username == "-") ? "" : username;
email = (email == "-") ? "" : email;
User user = _userManager.GetUser(username, email, SiteId);
if (user == null)
{
HttpContext.Response.StatusCode = (int)HttpStatusCode.NotFound;
}
else
{
List<Setting> settings = _settings.GetSettings(EntityNames.User, user.UserId).ToList();
user.Settings = settings.Where(item => !item.IsPrivate || _userPermissions.GetUser(User).UserId == user.UserId)
.ToDictionary(setting => setting.SettingName, setting => setting.SettingValue);
}
return Filter(user);
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized User Get Attempt {Username} {Email} {SiteId}", username, email, siteid);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
return null;
}
@ -340,14 +367,11 @@ namespace Oqtane.Controllers
if (user.IsAuthenticated)
{
user.Username = User.Identity.Name;
if (User.HasClaim(item => item.Type == ClaimTypes.NameIdentifier))
{
user.UserId = int.Parse(User.Claims.First(item => item.Type == ClaimTypes.NameIdentifier).Value);
}
user.UserId = User.UserId();
string roles = "";
foreach (var claim in User.Claims.Where(item => item.Type == ClaimTypes.Role))
foreach (var roleName in User.Roles())
{
roles += claim.Value + ";";
roles += roleName + ";";
}
if (roles != "") roles = ";" + roles;
user.Roles = roles;

View File

@ -33,14 +33,10 @@ namespace Oqtane.Extensions
}
}
public static string Roles(this ClaimsPrincipal claimsPrincipal)
public static string[] Roles(this ClaimsPrincipal claimsPrincipal)
{
var roles = "";
foreach (var claim in claimsPrincipal.Claims.Where(item => item.Type == ClaimTypes.Role))
{
roles += ((roles == "") ? "" : ";") + claim.Value;
}
return roles;
return claimsPrincipal.Claims.Where(item => item.Type == ClaimTypes.Role)
.Select(item => item.Value).ToArray();
}
public static string SiteKey(this ClaimsPrincipal claimsPrincipal)

View File

@ -6,6 +6,7 @@ using System.Linq;
using System.Net;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Localization;
using Oqtane.Enums;
using Oqtane.Infrastructure;
@ -25,15 +26,15 @@ namespace Oqtane.Managers
private readonly ITenantManager _tenantManager;
private readonly INotificationRepository _notifications;
private readonly IFolderRepository _folders;
private readonly IFileRepository _files;
private readonly IProfileRepository _profiles;
private readonly ISettingRepository _settings;
private readonly ISiteRepository _sites;
private readonly ISyncManager _syncManager;
private readonly ILogManager _logger;
private readonly IMemoryCache _cache;
private readonly IStringLocalizer<UserManager> _localizer;
private readonly ISiteRepository _siteRepo;
public UserManager(IUserRepository users, IRoleRepository roles, IUserRoleRepository userRoles, UserManager<IdentityUser> identityUserManager, SignInManager<IdentityUser> identitySignInManager, ITenantManager tenantManager, INotificationRepository notifications, IFolderRepository folders, IFileRepository files, IProfileRepository profiles, ISettingRepository settings, ISyncManager syncManager, ILogManager logger, IStringLocalizer<UserManager> localizer, ISiteRepository siteRepo)
public UserManager(IUserRepository users, IRoleRepository roles, IUserRoleRepository userRoles, UserManager<IdentityUser> identityUserManager, SignInManager<IdentityUser> identitySignInManager, ITenantManager tenantManager, INotificationRepository notifications, IFolderRepository folders, IProfileRepository profiles, ISettingRepository settings, ISiteRepository sites, ISyncManager syncManager, ILogManager logger, IMemoryCache cache, IStringLocalizer<UserManager> localizer)
{
_users = users;
_roles = roles;
@ -43,18 +44,34 @@ namespace Oqtane.Managers
_tenantManager = tenantManager;
_notifications = notifications;
_folders = folders;
_files = files;
_profiles = profiles;
_settings = settings;
_sites = sites;
_syncManager = syncManager;
_logger = logger;
_cache = cache;
_localizer = localizer;
_siteRepo = siteRepo;
}
public User GetUser(int userid, int siteid)
{
User user = _users.GetUser(userid);
var alias = _tenantManager.GetAlias();
return _cache.GetOrCreate($"user:{userid}:{alias.SiteKey}", entry =>
{
entry.SlidingExpiration = TimeSpan.FromMinutes(30);
User user = _users.GetUser(userid);
if (user != null)
{
user.SiteId = siteid;
user.Roles = GetUserRoles(user.UserId, user.SiteId);
}
return user;
});
}
public User GetUser(string username, int siteid)
{
User user = _users.GetUser(username);
if (user != null)
{
user.SiteId = siteid;
@ -63,11 +80,6 @@ namespace Oqtane.Managers
return user;
}
public User GetUser(string username, int siteid)
{
return GetUser(username, "", siteid);
}
public User GetUser(string username, string email, int siteid)
{
User user = _users.GetUser(username, email);
@ -85,14 +97,17 @@ namespace Oqtane.Managers
List<UserRole> userroles = _userRoles.GetUserRoles(userId, siteId).ToList();
foreach (UserRole userrole in userroles)
{
roles += userrole.Role.Name + ";";
if (userrole.Role.Name == RoleNames.Host && !userroles.Any(item => item.Role.Name == RoleNames.Admin))
if (Utilities.IsEffectiveOrExpired(userrole.EffectiveDate, userrole.ExpiryDate))
{
roles += RoleNames.Admin + ";";
}
if (userrole.Role.Name == RoleNames.Host && !userroles.Any(item => item.Role.Name == RoleNames.Registered))
{
roles += RoleNames.Registered + ";";
roles += userrole.Role.Name + ";";
if (userrole.Role.Name == RoleNames.Host && !userroles.Any(item => item.Role.Name == RoleNames.Admin))
{
roles += RoleNames.Admin + ";";
}
if (userrole.Role.Name == RoleNames.Host && !userroles.Any(item => item.Role.Name == RoleNames.Registered))
{
roles += RoleNames.Registered + ";";
}
}
}
if (roles != "") roles = ";" + roles;
@ -153,7 +168,7 @@ namespace Oqtane.Managers
if (User != null)
{
string siteName = _siteRepo.GetSite(user.SiteId).Name;
string siteName = _sites.GetSite(user.SiteId).Name;
if (!user.EmailConfirmed)
{
string token = await _identityUserManager.GenerateEmailConfirmationTokenAsync(identityuser);
@ -201,6 +216,8 @@ namespace Oqtane.Managers
IdentityUser identityuser = await _identityUserManager.FindByNameAsync(user.Username);
if (identityuser != null)
{
var alias = _tenantManager.GetAlias();
if (!string.IsNullOrEmpty(user.Password))
{
var validator = new PasswordValidator<IdentityUser>();
@ -224,7 +241,6 @@ namespace Oqtane.Managers
// if email address changed and it is not confirmed, verification is required for new email address
if (!user.EmailConfirmed)
{
var alias = _tenantManager.GetAlias();
string token = await _identityUserManager.GenerateEmailConfirmationTokenAsync(identityuser);
string url = alias.Protocol + alias.Name + "/login?name=" + user.Username + "&token=" + WebUtility.UrlEncode(token);
string body = "Dear " + user.DisplayName + ",\n\nIn Order To Verify The Email Address Associated To Your User Account Please Click The Link Displayed Below:\n\n" + url + "\n\nThank You!";
@ -242,6 +258,7 @@ namespace Oqtane.Managers
user = _users.UpdateUser(user);
_syncManager.AddSyncEvent(_tenantManager.GetAlias(), EntityNames.User, user.UserId, SyncEventActions.Update);
_syncManager.AddSyncEvent(_tenantManager.GetAlias(), EntityNames.User, user.UserId, SyncEventActions.Reload);
_cache.Remove($"user:{user.UserId}:{alias.SiteKey}");
user.Password = ""; // remove sensitive information
_logger.Log(LogLevel.Information, this, LogFunction.Update, "User Updated {User}", user);
}
@ -324,7 +341,7 @@ namespace Oqtane.Managers
_users.UpdateUser(user);
var alias = _tenantManager.GetAlias();
string url = alias.Protocol + alias.Name;
string siteName = _siteRepo.GetSite(alias.SiteId).Name;
string siteName = _sites.GetSite(alias.SiteId).Name;
string subject = _localizer["TwoFactorEmailSubject"];
subject = subject.Replace("[SiteName]", siteName);
string body = _localizer["TwoFactorEmailBody"].Value;
@ -376,7 +393,7 @@ namespace Oqtane.Managers
user = _users.GetUser(user.Username);
string token = await _identityUserManager.GeneratePasswordResetTokenAsync(identityuser);
string url = alias.Protocol + alias.Name + "/reset?name=" + user.Username + "&token=" + WebUtility.UrlEncode(token);
string siteName = _siteRepo.GetSite(alias.SiteId).Name;
string siteName = _sites.GetSite(alias.SiteId).Name;
string subject = _localizer["UserLockoutEmailSubject"];
subject = subject.Replace("[SiteName]", siteName);
string body = _localizer["UserLockoutEmailBody"].Value;
@ -429,7 +446,7 @@ namespace Oqtane.Managers
user = _users.GetUser(user.Username);
string token = await _identityUserManager.GeneratePasswordResetTokenAsync(identityuser);
string url = alias.Protocol + alias.Name + "/reset?name=" + user.Username + "&token=" + WebUtility.UrlEncode(token);
string siteName = _siteRepo.GetSite(alias.SiteId).Name;
string siteName = _sites.GetSite(alias.SiteId).Name;
string subject = _localizer["ForgotPasswordEmailSubject"];
subject = subject.Replace("[SiteName]", siteName);
string body = _localizer["ForgotPasswordEmailBody"].Value;

View File

@ -1,6 +1,9 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory;
using Oqtane.Infrastructure;
using Oqtane.Models;
using Oqtane.Shared;
@ -10,11 +13,15 @@ namespace Oqtane.Repository
{
private readonly IDbContextFactory<TenantDBContext> _dbContextFactory;
private readonly IRoleRepository _roles;
private readonly ITenantManager _tenantManager;
private readonly IMemoryCache _cache;
public UserRoleRepository(IDbContextFactory<TenantDBContext> dbContextFactory, IRoleRepository roles)
public UserRoleRepository(IDbContextFactory<TenantDBContext> dbContextFactory, IRoleRepository roles, ITenantManager tenantManager, IMemoryCache cache)
{
_dbContextFactory = dbContextFactory;
_roles = roles;
_tenantManager = tenantManager;
_cache = cache;
}
public IEnumerable<UserRole> GetUserRoles(int siteId)
@ -28,11 +35,16 @@ namespace Oqtane.Repository
public IEnumerable<UserRole> GetUserRoles(int userId, int siteId)
{
using var db = _dbContextFactory.CreateDbContext();
return db.UserRole
.Include(item => item.Role) // eager load roles
.Include(item => item.User) // eager load users
.Where(item => (item.Role.SiteId == siteId || item.Role.SiteId == null || siteId == -1) && item.UserId == userId).ToList();
var alias = _tenantManager.GetAlias();
return _cache.GetOrCreate($"userroles:{userId}:{alias.SiteKey}", entry =>
{
entry.SlidingExpiration = TimeSpan.FromMinutes(30);
using var db = _dbContextFactory.CreateDbContext();
return db.UserRole
.Include(item => item.Role) // eager load roles
.Include(item => item.User) // eager load users
.Where(item => (item.Role.SiteId == siteId || item.Role.SiteId == null || siteId == -1) && item.UserId == userId).ToList();
});
}
public IEnumerable<UserRole> GetUserRoles(string roleName, int siteId)
@ -57,6 +69,10 @@ namespace Oqtane.Repository
DeleteUserRoles(userRole.UserId);
}
var alias = _tenantManager.GetAlias();
_cache.Remove($"user:{userRole.UserId}:{alias.SiteKey}");
_cache.Remove($"userroles:{userRole.UserId}:{alias.SiteKey}");
return userRole;
}
@ -65,6 +81,11 @@ namespace Oqtane.Repository
using var db = _dbContextFactory.CreateDbContext();
db.Entry(userRole).State = EntityState.Modified;
db.SaveChanges();
var alias = _tenantManager.GetAlias();
_cache.Remove($"user:{userRole.UserId}:{alias.SiteKey}");
_cache.Remove($"userroles:{userRole.UserId}:{alias.SiteKey}");
return userRole;
}
@ -122,6 +143,10 @@ namespace Oqtane.Repository
var userRole = db.UserRole.Find(userRoleId);
db.UserRole.Remove(userRole);
db.SaveChanges();
var alias = _tenantManager.GetAlias();
_cache.Remove($"user:{userRole.UserId}:{alias.SiteKey}");
_cache.Remove($"userroles:{userRole.UserId}:{alias.SiteKey}");
}
public void DeleteUserRoles(int userId)
@ -132,6 +157,10 @@ namespace Oqtane.Repository
db.UserRole.Remove(userRole);
}
db.SaveChanges();
var alias = _tenantManager.GetAlias();
_cache.Remove($"user:{userId}:{alias.SiteKey}");
_cache.Remove($"userroles:{userId}:{alias.SiteKey}");
}
}
}

View File

@ -7,6 +7,7 @@ using Oqtane.Extensions;
using System;
using System.Collections.Generic;
using System.Text.Json;
using Oqtane.Shared;
namespace Oqtane.Security
{
@ -26,11 +27,13 @@ namespace Oqtane.Security
public class UserPermissions : IUserPermissions
{
private readonly IPermissionRepository _permissions;
private readonly IUserRoleRepository _userRoles;
private readonly IHttpContextAccessor _accessor;
public UserPermissions(IPermissionRepository permissions, IHttpContextAccessor accessor)
public UserPermissions(IPermissionRepository permissions, IUserRoleRepository userRoles, IHttpContextAccessor accessor)
{
_permissions = permissions;
_userRoles = userRoles;
_accessor = accessor;
}
@ -71,13 +74,24 @@ namespace Oqtane.Security
if (user.IsAuthenticated)
{
user.Username = principal.Identity.Name;
if (principal.Claims.Any(item => item.Type == ClaimTypes.NameIdentifier))
user.UserId = principal.UserId();
// include roles
var userRoles = _userRoles.GetUserRoles(user.UserId, principal.SiteId()).ToList();
foreach (var roleName in principal.Roles())
{
user.UserId = int.Parse(principal.Claims.First(item => item.Type == ClaimTypes.NameIdentifier).Value);
}
foreach (var claim in principal.Claims.Where(item => item.Type == ClaimTypes.Role))
{
user.Roles += claim.Value + ";";
var role = userRoles.FirstOrDefault(item => item.Role.Name == roleName);
if (role != null)
{
if (Utilities.IsEffectiveOrExpired(role.EffectiveDate,role.ExpiryDate))
{
user.Roles += roleName + ";";
}
}
else
{
user.Roles += roleName + ";";
}
}
if (user.Roles != "") user.Roles = ";" + user.Roles;
}

View File

@ -40,7 +40,7 @@ namespace Oqtane.Models
public DateTime? LastLoginOn { get; set; }
/// <summary>
/// Tracking information of IP used when the user last worked on this site.
/// IP address when the user last logged in to this site.
/// </summary>
public string LastIPAddress { get; set; }