add support for API permissions at the UI layer - including ability to delegate user, role, profile management

This commit is contained in:
Shaun Walker
2023-01-09 11:38:25 -05:00
parent 1616f94b86
commit e136972cd7
50 changed files with 628 additions and 799 deletions

View File

@ -1,172 +0,0 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Authorization;
using System.Collections.Generic;
using Oqtane.Shared;
using Oqtane.Models;
using Oqtane.Infrastructure;
using Oqtane.Enums;
using System.Net;
using Oqtane.Repository;
using Oqtane.Extensions;
using System.Reflection;
using System;
using System.Linq;
namespace Oqtane.Controllers
{
[Route(ControllerRoutes.ApiRoute)]
public class ApiController : Controller
{
private readonly IPermissionRepository _permissions;
private readonly IRoleRepository _roles;
private readonly ILogManager _logger;
private readonly Alias _alias;
public ApiController(IPermissionRepository permissions, IRoleRepository roles, ILogManager logger, ITenantManager tenantManager)
{
_permissions = permissions;
_roles = roles;
_logger = logger;
_alias = tenantManager.GetAlias();
}
// GET: api/<controller>?siteid=x
[HttpGet]
[Authorize(Roles = RoleNames.Admin)]
public List<Api> Get(string siteid)
{
int SiteId;
if (int.TryParse(siteid, out SiteId) && SiteId == _alias.SiteId)
{
var apis = new List<Api>();
var assemblies = AppDomain.CurrentDomain.GetOqtaneAssemblies();
foreach (var assembly in assemblies)
{
// iterate controllers
foreach (var type in assembly.GetTypes().Where(type => typeof(Controller).IsAssignableFrom(type)))
{
// iterate controller methods with authorize attribute
var actions = type.GetMethods(BindingFlags.Public | BindingFlags.Instance)
.Where(m => m.GetCustomAttributes<AuthorizeAttribute>().Any());
foreach(var action in actions)
{
// get policy
var policy = action.GetCustomAttribute<AuthorizeAttribute>().Policy;
if (!string.IsNullOrEmpty(policy) && policy.Contains(":") && !policy.Contains(Constants.RequireEntityId))
{
// parse policy
var segments = policy.Split(':');
if (!apis.Any(item => item.EntityName == segments[0]))
{
apis.Add(new Api { SiteId = SiteId, EntityName = segments[0], Permissions = segments[1] });
}
else
{
// concatenate permissions
var permissions = apis.SingleOrDefault(item => item.EntityName == segments[0]).Permissions;
if (!permissions.Split(',').Contains(segments[1]))
{
apis.SingleOrDefault(item => item.EntityName == segments[0]).Permissions += "," + segments[1];
}
}
}
}
}
}
return apis;
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Api Get Attempt {SiteId}", siteid);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
return null;
}
}
// GET: api/<controller>/1/user
[HttpGet("{siteid}/{entityname}")]
[Authorize(Roles = RoleNames.Admin)]
public Api Get(int siteid, string entityname)
{
if (siteid == _alias.SiteId)
{
var permissions = _permissions.GetPermissions(siteid, entityname);
if (permissions == null || permissions.ToList().Count == 0)
{
permissions = GetPermissions(siteid, entityname);
}
return new Api { SiteId = siteid, EntityName = entityname, Permissions = permissions.EncodePermissions() };
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Api Get Attempt {SiteId} {EntityName}", siteid, entityname);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
return null;
}
}
// POST: api/<controller>
[HttpPost]
[Authorize(Roles = RoleNames.Admin)]
public void Post([FromBody] Api api)
{
if (ModelState.IsValid && api.SiteId == _alias.SiteId)
{
_permissions.UpdatePermissions(api.SiteId, api.EntityName, -1, api.Permissions);
_logger.Log(LogLevel.Information, this, LogFunction.Update, "Api Updated {Api}", api);
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Api Post Attempt {Api}", api);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
}
}
private List<Permission> GetPermissions(int siteid, string entityname)
{
var permissions = new List<Permission>();
var assemblies = AppDomain.CurrentDomain.GetOqtaneAssemblies();
foreach (var assembly in assemblies)
{
// iterate controllers
foreach (var type in assembly.GetTypes().Where(type => typeof(Controller).IsAssignableFrom(type)))
{
// iterate controller methods with authorize attribute
var actions = type.GetMethods(BindingFlags.Public | BindingFlags.Instance)
.Where(m => m.GetCustomAttributes<AuthorizeAttribute>().Any());
foreach (var action in actions)
{
// get policy
var policy = action.GetCustomAttribute<AuthorizeAttribute>().Policy;
if (!string.IsNullOrEmpty(policy) && policy.Contains(":") && !policy.Contains(Constants.RequireEntityId))
{
// parse policy
var segments = policy.Split(':');
// entity match
if (segments[0] == entityname && segments.Length > 2)
{
var roles = _roles.GetRoles(siteid);
foreach (var rolename in (segments[2]).Split(','))
{
var role = roles.FirstOrDefault(item => item.Name == rolename);
if (role != null)
{
if (!permissions.Any(item => item.EntityName == entityname && item.PermissionName == segments[1] && item.RoleId == role.RoleId))
{
permissions.Add(new Permission { SiteId = siteid, EntityName = entityname, EntityId = -1, PermissionName = segments[1], RoleId = role.RoleId, Role = role, UserId = null, IsAuthorized = true });
}
}
}
}
}
}
}
}
return permissions;
}
}
}

View File

@ -47,7 +47,6 @@ namespace Oqtane.Controllers
int SiteId;
if (int.TryParse(siteid, out SiteId) && SiteId == _alias.SiteId)
{
List<ModuleDefinition> moduledefinitions = _moduleDefinitions.GetModuleDefinitions(SiteId).ToList();
List<Setting> settings = _settings.GetSettings(EntityNames.Module).ToList();
foreach (PageModule pagemodule in _pageModules.GetPageModules(SiteId))
@ -75,7 +74,6 @@ namespace Oqtane.Controllers
module.Order = pagemodule.Order;
module.ContainerType = pagemodule.ContainerType;
module.ModuleDefinition = moduledefinitions.Find(item => item.ModuleDefinitionName == module.ModuleDefinitionName);
module.Settings = settings.Where(item => item.EntityId == pagemodule.ModuleId)
.Where(item => !item.IsPrivate || _userPermissions.IsAuthorized(User, PermissionNames.Edit, pagemodule.Module.Permissions))
.ToDictionary(setting => setting.SettingName, setting => setting.SettingValue);

View File

@ -281,7 +281,7 @@ namespace Oqtane.Controllers
// synchronize module permissions
if (added.Count > 0 || removed.Count > 0)
{
foreach (PageModule pageModule in _pageModules.GetPageModules(page.PageId, "").ToList())
foreach (PageModule pageModule in _pageModules.GetPageModules(page.SiteId).Where(item => item.PageId == page.PageId).ToList())
{
var modulePermissions = _permissionRepository.GetPermissions(pageModule.Module.SiteId, EntityNames.Module, pageModule.Module.ModuleId).ToList();
// permissions added

View File

@ -120,7 +120,8 @@ namespace Oqtane.Controllers
if (page != null && page.SiteId == _alias.SiteId && _userPermissions.IsAuthorized(User, page.SiteId, EntityNames.Page, pageid, PermissionNames.Edit))
{
int order = 1;
List<PageModule> pagemodules = _pageModules.GetPageModules(pageid, pane).OrderBy(item => item.Order).ToList();
List<PageModule> pagemodules = _pageModules.GetPageModules(page.SiteId)
.Where(item => item.PageId == pageid && item.Pane == pane).OrderBy(item => item.Order).ToList();
foreach (PageModule pagemodule in pagemodules)
{
if (pagemodule.Order != order)

View File

@ -28,7 +28,7 @@ namespace Oqtane.Controllers
// GET: api/<controller>?siteid=x
[HttpGet]
[Authorize(Policy = $"{EntityNames.Profile}:{PermissionNames.Read}:{RoleNames.Registered}")]
[Authorize(Roles = RoleNames.Registered)]
public IEnumerable<Profile> Get(string siteid)
{
int SiteId;
@ -46,7 +46,7 @@ namespace Oqtane.Controllers
// GET api/<controller>/5
[HttpGet("{id}")]
[Authorize(Policy = $"{EntityNames.Profile}:{PermissionNames.Read}:{RoleNames.Registered}")]
[Authorize(Roles = RoleNames.Registered)]
public Profile Get(int id)
{
var profile = _profiles.GetProfile(id);

View File

@ -28,7 +28,7 @@ namespace Oqtane.Controllers
// GET: api/<controller>?siteid=x&global=true/false
[HttpGet]
[Authorize(Policy = $"{EntityNames.Role}:{PermissionNames.Read}:{RoleNames.Registered}")]
[Authorize(Roles = RoleNames.Registered)]
public IEnumerable<Role> Get(string siteid, string global)
{
int SiteId;
@ -50,7 +50,7 @@ namespace Oqtane.Controllers
// GET api/<controller>/5
[HttpGet("{id}")]
[Authorize(Policy = $"{EntityNames.Role}:{PermissionNames.Read}:{RoleNames.Registered}")]
[Authorize(Roles = RoleNames.Registered)]
public Role Get(int id)
{
var role = _roles.GetRole(id);

View File

@ -212,7 +212,7 @@ namespace Oqtane.Controllers
authorized = true;
if (permissionName == PermissionNames.Edit)
{
authorized = User.IsInRole(RoleNames.Admin) || (_userPermissions.GetUser(User).UserId == entityId);
authorized = _userPermissions.IsAuthorized(User, _alias.SiteId, entityName, -1, PermissionNames.Write, RoleNames.Admin) || (_userPermissions.GetUser(User).UserId == entityId);
}
break;
case EntityNames.Visitor:
@ -226,14 +226,11 @@ namespace Oqtane.Controllers
}
break;
default: // custom entity
authorized = true;
if (permissionName == PermissionNames.Edit)
{
authorized = User.IsInRole(RoleNames.Admin) || _userPermissions.IsAuthorized(User, _alias.SiteId, entityName, entityId, permissionName);
}
else
{
authorized = true;
}
break;
}
return authorized;

View File

@ -29,23 +29,25 @@ namespace Oqtane.Controllers
private readonly ITenantManager _tenantManager;
private readonly INotificationRepository _notifications;
private readonly IFolderRepository _folders;
private readonly ISyncManager _syncManager;
private readonly ISiteRepository _sites;
private readonly IUserPermissions _userPermissions;
private readonly IJwtManager _jwtManager;
private readonly ISyncManager _syncManager;
private readonly ILogManager _logger;
public UserController(IUserRepository users, IUserRoleRepository userRoles, UserManager<IdentityUser> identityUserManager, SignInManager<IdentityUser> identitySignInManager, ITenantManager tenantManager, INotificationRepository notifications, IFolderRepository folders, ISyncManager syncManager, ISiteRepository sites, IJwtManager jwtManager, ILogManager logger)
public UserController(IUserRepository users, IUserRoleRepository userRoles, UserManager<IdentityUser> identityUserManager, SignInManager<IdentityUser> identitySignInManager, ITenantManager tenantManager, INotificationRepository notifications, IFolderRepository folders, ISiteRepository sites, IUserPermissions userPermissions, IJwtManager jwtManager, ISyncManager syncManager, ILogManager logger)
{
_users = users;
_userRoles = userRoles;
_identityUserManager = identityUserManager;
_identitySignInManager = identitySignInManager;
_tenantManager = tenantManager;
_folders = folders;
_notifications = notifications;
_syncManager = syncManager;
_folders = folders;
_sites = sites;
_userPermissions = userPermissions;
_jwtManager = jwtManager;
_syncManager = syncManager;
_logger = logger;
}
@ -105,7 +107,7 @@ namespace Oqtane.Controllers
user.TwoFactorCode = "";
user.TwoFactorExpiry = null;
if (!User.IsInRole(RoleNames.Admin) && User.Identity.Name?.ToLower() != user.Username.ToLower())
if (!_userPermissions.IsAuthorized(User, user.SiteId, EntityNames.User, -1, PermissionNames.Write, RoleNames.Admin) && User.Identity.Name?.ToLower() != user.Username.ToLower())
{
user.Email = "";
user.PhotoFileId = null;
@ -148,8 +150,8 @@ namespace Oqtane.Controllers
User newUser = null;
bool verified;
bool allowregistration;
if (User.IsInRole(RoleNames.Admin))
bool allowregistration;
if (_userPermissions.IsAuthorized(User, user.SiteId, EntityNames.User, -1, PermissionNames.Write, RoleNames.Admin))
{
verified = true;
allowregistration = true;
@ -241,7 +243,8 @@ namespace Oqtane.Controllers
[Authorize]
public async Task<User> Put(int id, [FromBody] User user)
{
if (ModelState.IsValid && user.SiteId == _tenantManager.GetAlias().SiteId && _users.GetUser(user.UserId, false) != null && (User.IsInRole(RoleNames.Admin) || User.Identity.Name == user.Username))
if (ModelState.IsValid && user.SiteId == _tenantManager.GetAlias().SiteId && _users.GetUser(user.UserId, false) != null
&& (_userPermissions.IsAuthorized(User, user.SiteId, EntityNames.User, -1, PermissionNames.Write, RoleNames.Admin) || User.Identity.Name == user.Username))
{
IdentityUser identityuser = await _identityUserManager.FindByNameAsync(user.Username);
if (identityuser != null)
@ -287,7 +290,7 @@ namespace Oqtane.Controllers
// DELETE api/<controller>/5?siteid=x
[HttpDelete("{id}")]
[Authorize(Roles = RoleNames.Admin)]
[Authorize(Policy = $"{EntityNames.User}:{PermissionNames.Write}:{RoleNames.Admin}")]
public async Task Delete(int id, string siteid)
{
int SiteId;

View File

@ -10,6 +10,7 @@ using System.Linq;
using System.Net;
using Oqtane.Security;
using System;
using Oqtane.Modules.Admin.Roles;
namespace Oqtane.Controllers
{
@ -93,7 +94,7 @@ namespace Oqtane.Controllers
userrole.User.TwoFactorCode = "";
userrole.User.TwoFactorExpiry = null;
if (!User.IsInRole(RoleNames.Admin) && userid != userrole.User.UserId)
if (!_userPermissions.IsAuthorized(User, userrole.User.SiteId, EntityNames.User, -1, PermissionNames.Write, RoleNames.Admin) && userid != userrole.User.UserId)
{
userrole.User.Email = "";
userrole.User.PhotoFileId = null;
@ -115,7 +116,7 @@ namespace Oqtane.Controllers
// POST api/<controller>
[HttpPost]
[Authorize(Roles = RoleNames.Admin)]
[Authorize(Policy = $"{EntityNames.UserRole}:{PermissionNames.Write}:{RoleNames.Admin}")]
public UserRole Post([FromBody] UserRole userRole)
{
var role = _roles.GetRole(userRole.RoleId);
@ -138,7 +139,7 @@ namespace Oqtane.Controllers
// PUT api/<controller>/5
[HttpPut("{id}")]
[Authorize(Roles = RoleNames.Admin)]
[Authorize(Policy = $"{EntityNames.UserRole}:{PermissionNames.Write}:{RoleNames.Admin}")]
public UserRole Put(int id, [FromBody] UserRole userRole)
{
var role = _roles.GetRole(userRole.RoleId);
@ -160,7 +161,7 @@ namespace Oqtane.Controllers
// DELETE api/<controller>/5
[HttpDelete("{id}")]
[Authorize(Roles = RoleNames.Admin)]
[Authorize(Policy = $"{EntityNames.UserRole}:{PermissionNames.Write}:{RoleNames.Admin}")]
public void Delete(int id)
{
UserRole userrole = _userRoles.GetUserRole(id);