From 3f48c1f8fe616ed154d4ab632be109d143456fc7 Mon Sep 17 00:00:00 2001 From: Shaun Walker Date: Sun, 23 May 2021 10:29:05 -0400 Subject: [PATCH] fix #1367 - provides support for multiple entities in auth policy and makes parameter names more intuitive - backward compatible with entityid --- .../HtmlText/Services/HtmlTextService.cs | 9 ++- Oqtane.Client/Services/ServiceBase.cs | 27 +++++++- .../Themes/Controls/Theme/ControlPanel.razor | 23 +------ .../Controllers/ModuleControllerBase.cs | 18 ++++- Oqtane.Server/Controllers/PageController.cs | 65 ++++++++++++++++++- .../Controllers/HtmlTextController.cs | 8 +-- Oqtane.Server/Security/PermissionHandler.cs | 18 +++-- 7 files changed, 126 insertions(+), 42 deletions(-) diff --git a/Oqtane.Client/Modules/HtmlText/Services/HtmlTextService.cs b/Oqtane.Client/Modules/HtmlText/Services/HtmlTextService.cs index 90dfcdde..a90451cd 100644 --- a/Oqtane.Client/Modules/HtmlText/Services/HtmlTextService.cs +++ b/Oqtane.Client/Modules/HtmlText/Services/HtmlTextService.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Linq; using System.Net.Http; using System.Threading.Tasks; -using Oqtane.Modules.HtmlText.Models; using Oqtane.Services; using Oqtane.Shared; @@ -21,23 +20,23 @@ namespace Oqtane.Modules.HtmlText.Services public async Task GetHtmlTextAsync(int moduleId) { - var htmltext = await GetJsonAsync>(CreateAuthorizationPolicyUrl($"{ApiUrl}/{moduleId}", moduleId)); + var htmltext = await GetJsonAsync>(CreateAuthorizationPolicyUrl($"{ApiUrl}/{moduleId}", new Dictionary() { { EntityNames.Module, moduleId } })); return htmltext.FirstOrDefault(); } public async Task AddHtmlTextAsync(Models.HtmlText htmlText) { - await PostJsonAsync(CreateAuthorizationPolicyUrl($"{ApiUrl}", htmlText.ModuleId), htmlText); + await PostJsonAsync(CreateAuthorizationPolicyUrl($"{ApiUrl}", new Dictionary() { { EntityNames.Module, htmlText.ModuleId } }), htmlText); } public async Task UpdateHtmlTextAsync(Models.HtmlText htmlText) { - await PutJsonAsync(CreateAuthorizationPolicyUrl($"{ApiUrl}/{htmlText.HtmlTextId}", htmlText.ModuleId), htmlText); + await PutJsonAsync(CreateAuthorizationPolicyUrl($"{ApiUrl}/{htmlText.HtmlTextId}", new Dictionary() { { EntityNames.Module, htmlText.ModuleId } }), htmlText); } public async Task DeleteHtmlTextAsync(int moduleId) { - await DeleteAsync(CreateAuthorizationPolicyUrl($"{ApiUrl}/{moduleId}", moduleId)); + await DeleteAsync(CreateAuthorizationPolicyUrl($"{ApiUrl}/{moduleId}", new Dictionary() { { EntityNames.Module, moduleId } })); } } } diff --git a/Oqtane.Client/Services/ServiceBase.cs b/Oqtane.Client/Services/ServiceBase.cs index f4ca76f5..f08b54ec 100644 --- a/Oqtane.Client/Services/ServiceBase.cs +++ b/Oqtane.Client/Services/ServiceBase.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Net; using System.Net.Http; using System.Net.Http.Json; @@ -52,10 +53,15 @@ namespace Oqtane.Services return apiurl; } - // add entityid parameter to url for custom authorization policy - public string CreateAuthorizationPolicyUrl(string url, int entityId) + // add authentityid parameters to url for custom authorization policy - args in form of entityname = entityid + public string CreateAuthorizationPolicyUrl(string url, Dictionary args) { - string qs = "entityid=" + entityId.ToString(); + string qs = ""; + foreach (KeyValuePair kvp in args) + { + qs += (qs != "") ? "&" : ""; + qs += "auth" + kvp.Key.ToLower() + "id=" + kvp.Value.ToString(); + } if (url.Contains("?")) { @@ -202,5 +208,20 @@ namespace Oqtane.Services { return CreateApiUrl(serviceName, alias, ControllerRoutes.Default); } + + [Obsolete("This method is obsolete. Use CreateAuthorizationPolicyUrl(string url, Dictionary args) instead - in conjunction with _authEntityId in Server Controller.", false)] + public string CreateAuthorizationPolicyUrl(string url, int entityId) + { + string qs = "entityid=" + entityId.ToString(); + + if (url.Contains("?")) + { + return url + "&" + qs; + } + else + { + return url + "?" + qs; + } + } } } diff --git a/Oqtane.Client/Themes/Controls/Theme/ControlPanel.razor b/Oqtane.Client/Themes/Controls/Theme/ControlPanel.razor index a4bf5d16..14e985e1 100644 --- a/Oqtane.Client/Themes/Controls/Theme/ControlPanel.razor +++ b/Oqtane.Client/Themes/Controls/Theme/ControlPanel.razor @@ -513,28 +513,7 @@ { List permissions; - if (action == "publish") - { - // publish all modules - foreach (var module in PageState.Modules.Where(item => item.PageId == PageState.Page.PageId)) - { - permissions = UserSecurity.GetPermissionStrings(module.Permissions); - foreach (var permissionstring in permissions) - { - if (permissionstring.PermissionName == PermissionNames.View) - { - List ids = permissionstring.Permissions.Split(';').ToList(); - if (!ids.Contains(RoleNames.Everyone)) ids.Add(RoleNames.Everyone); - if (!ids.Contains(RoleNames.Registered)) ids.Add(RoleNames.Registered); - permissionstring.Permissions = string.Join(";", ids.ToArray()); - } - } - module.Permissions = UserSecurity.SetPermissionStrings(permissions); - await ModuleService.UpdateModuleAsync(module); - } - } - - // publish page + // publish/unpublish page var page = PageState.Page; permissions = UserSecurity.GetPermissionStrings(page.Permissions); foreach (var permissionstring in permissions) diff --git a/Oqtane.Server/Controllers/ModuleControllerBase.cs b/Oqtane.Server/Controllers/ModuleControllerBase.cs index b934e684..baa9a0a1 100644 --- a/Oqtane.Server/Controllers/ModuleControllerBase.cs +++ b/Oqtane.Server/Controllers/ModuleControllerBase.cs @@ -1,21 +1,35 @@ -using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Http; using Oqtane.Infrastructure; +using System.Collections.Generic; +using System; namespace Oqtane.Controllers { public class ModuleControllerBase : Controller { protected readonly ILogManager _logger; - protected int _entityId = -1; // passed as a querystring parameter for policy authorization and used for validation + // querystring parameters for policy authorization and validation + protected Dictionary _authEntityId = new Dictionary(StringComparer.OrdinalIgnoreCase); + protected int _entityId = -1; // deprecated public ModuleControllerBase(ILogManager logger, IHttpContextAccessor accessor) { _logger = logger; + int value; + foreach (var param in accessor.HttpContext.Request.Query) + { + if (param.Key.StartsWith("auth") && param.Key.EndsWith("id") && int.TryParse(param.Value, out value)) + { + _authEntityId.Add(param.Key.Substring(4, param.Key.Length - 6), int.Parse(param.Value)); + } + } + // entityid is deprecated if (accessor.HttpContext.Request.Query.ContainsKey("entityid")) { _entityId = int.Parse(accessor.HttpContext.Request.Query["entityid"]); } } + } } diff --git a/Oqtane.Server/Controllers/PageController.cs b/Oqtane.Server/Controllers/PageController.cs index 3c9edaef..2ddccb06 100644 --- a/Oqtane.Server/Controllers/PageController.cs +++ b/Oqtane.Server/Controllers/PageController.cs @@ -19,17 +19,19 @@ namespace Oqtane.Controllers private readonly IPageRepository _pages; private readonly IModuleRepository _modules; private readonly IPageModuleRepository _pageModules; + private readonly IPermissionRepository _permissionRepository; private readonly ISettingRepository _settings; private readonly IUserPermissions _userPermissions; private readonly ISyncManager _syncManager; private readonly ILogManager _logger; private readonly Alias _alias; - public PageController(IPageRepository pages, IModuleRepository modules, IPageModuleRepository pageModules, ISettingRepository settings, IUserPermissions userPermissions, ITenantManager tenantManager, ISyncManager syncManager, ILogManager logger) + public PageController(IPageRepository pages, IModuleRepository modules, IPageModuleRepository pageModules, IPermissionRepository permissionRepository, ISettingRepository settings, IUserPermissions userPermissions, ITenantManager tenantManager, ISyncManager syncManager, ILogManager logger) { _pages = pages; _modules = modules; _pageModules = pageModules; + _permissionRepository = permissionRepository; _settings = settings; _userPermissions = userPermissions; _syncManager = syncManager; @@ -227,7 +229,54 @@ namespace Oqtane.Controllers { if (ModelState.IsValid && _userPermissions.IsAuthorized(User, EntityNames.Page, page.PageId, PermissionNames.Edit)) { + // preserve page permissions + var oldPermissions = _permissionRepository.GetPermissions(EntityNames.Page, page.PageId).ToList(); + page = _pages.UpdatePage(page); + + // get differences between old and new page permissions + var newPermissions = _permissionRepository.DecodePermissions(page.Permissions, page.SiteId, EntityNames.Page, page.PageId).ToList(); + var added = GetPermissionsDifferences(newPermissions, oldPermissions); + var removed = GetPermissionsDifferences(oldPermissions, newPermissions); + + // synchronize module permissions + if (added.Count > 0 || removed.Count > 0) + { + foreach (PageModule pageModule in _pageModules.GetPageModules(page.PageId, "").ToList()) + { + var modulePermissions = _permissionRepository.GetPermissions(EntityNames.Module, pageModule.Module.ModuleId).ToList(); + //var modulePermissions = _permissionRepository.DecodePermissions(pageModule.Module.Permissions, page.SiteId, EntityNames.Module, pageModule.ModuleId).ToList(); + // permissions added + foreach(Permission permission in added) + { + if (!modulePermissions.Any(item => item.PermissionName == permission.PermissionName + && item.RoleId == permission.RoleId && item.UserId == permission.UserId && item.IsAuthorized == permission.IsAuthorized)) + { + _permissionRepository.AddPermission(new Permission + { + SiteId = page.SiteId, + EntityName = EntityNames.Module, + EntityId = pageModule.ModuleId, + PermissionName = permission.PermissionName, + RoleId = permission.RoleId, + UserId = permission.UserId, + IsAuthorized = permission.IsAuthorized + }); + } + } + // permissions removed + foreach (Permission permission in removed) + { + var modulePermission = modulePermissions.FirstOrDefault(item => item.PermissionName == permission.PermissionName + && item.RoleId == permission.RoleId && item.UserId == permission.UserId && item.IsAuthorized == permission.IsAuthorized); + if (modulePermission != null) + { + _permissionRepository.DeletePermission(modulePermission.PermissionId); + } + } + } + } + _syncManager.AddSyncEvent(_alias.TenantId, EntityNames.Site, page.SiteId); _logger.Log(LogLevel.Information, this, LogFunction.Update, "Page Updated {Page}", page); } @@ -240,6 +289,19 @@ namespace Oqtane.Controllers return page; } + private List GetPermissionsDifferences(List permissions1, List permissions2) + { + var differences = new List(); + foreach (Permission p in permissions1) + { + if (!permissions2.Any(item => item.PermissionName == p.PermissionName && item.RoleId == p.RoleId && item.UserId == p.UserId && item.IsAuthorized == p.IsAuthorized)) + { + differences.Add(p); + } + } + return differences; + } + // PUT api//?siteid=x&pageid=y&parentid=z [HttpPut] [Authorize(Roles = RoleNames.Registered)] @@ -287,4 +349,5 @@ namespace Oqtane.Controllers } } } + } diff --git a/Oqtane.Server/Modules/HtmlText/Controllers/HtmlTextController.cs b/Oqtane.Server/Modules/HtmlText/Controllers/HtmlTextController.cs index e321b76b..3de69445 100644 --- a/Oqtane.Server/Modules/HtmlText/Controllers/HtmlTextController.cs +++ b/Oqtane.Server/Modules/HtmlText/Controllers/HtmlTextController.cs @@ -30,7 +30,7 @@ namespace Oqtane.Modules.HtmlText.Controllers try { Models.HtmlText htmlText = null; - if (_entityId == id) + if (_authEntityId[EntityNames.Module] == id) { htmlText = _htmlText.GetHtmlText(id); list.Add(htmlText); @@ -51,7 +51,7 @@ namespace Oqtane.Modules.HtmlText.Controllers { try { - if (ModelState.IsValid && htmlText.ModuleId == _entityId) + if (ModelState.IsValid && htmlText.ModuleId == _authEntityId[EntityNames.Module]) { htmlText = _htmlText.AddHtmlText(htmlText); _logger.Log(LogLevel.Information, this, LogFunction.Create, "Html/Text Added {HtmlText}", htmlText); @@ -72,7 +72,7 @@ namespace Oqtane.Modules.HtmlText.Controllers { try { - if (ModelState.IsValid && htmlText.ModuleId == _entityId) + if (ModelState.IsValid && htmlText.ModuleId == _authEntityId[EntityNames.Module]) { htmlText = _htmlText.UpdateHtmlText(htmlText); _logger.Log(LogLevel.Information, this, LogFunction.Update, "Html/Text Updated {HtmlText}", htmlText); @@ -93,7 +93,7 @@ namespace Oqtane.Modules.HtmlText.Controllers { try { - if (id == _entityId) + if (id == _authEntityId[EntityNames.Module]) { _htmlText.DeleteHtmlText(id); _logger.Log(LogLevel.Information, this, LogFunction.Delete, "Html/Text Deleted {HtmlTextId}", id); diff --git a/Oqtane.Server/Security/PermissionHandler.cs b/Oqtane.Server/Security/PermissionHandler.cs index 8735d0d0..4bd01a3c 100644 --- a/Oqtane.Server/Security/PermissionHandler.cs +++ b/Oqtane.Server/Security/PermissionHandler.cs @@ -1,4 +1,4 @@ -using System.Threading.Tasks; +using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Oqtane.Enums; @@ -22,11 +22,19 @@ namespace Oqtane.Security protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, PermissionRequirement requirement) { - // permission is scoped based on EntityId which must be passed as a querystring parameter + // permission is scoped based on auth{entityname}id (ie ?authmoduleid ) which must be passed as a querystring parameter var ctx = _httpContextAccessor.HttpContext; - if (ctx != null && ctx.Request.Query.ContainsKey("entityid")) + if (ctx != null) { - int entityId = int.Parse(ctx.Request.Query["entityid"]); + int entityId = -1; + if (ctx.Request.Query.ContainsKey("auth" + requirement.EntityName.ToLower() + "id")) + { + entityId = int.Parse(ctx.Request.Query["auth" + requirement.EntityName.ToLower() + "id"]); + } + if (entityId == -1 && ctx.Request.Query.ContainsKey("entityid")) + { + entityId = int.Parse(ctx.Request.Query["entityid"]); + } if (_userPermissions.IsAuthorized(context.User, requirement.EntityName, entityId, requirement.PermissionName)) { context.Succeed(requirement); @@ -39,4 +47,4 @@ namespace Oqtane.Security return Task.CompletedTask; } } -} \ No newline at end of file +}