allow themes to define usage permissions similar to modules

This commit is contained in:
sbwalker
2025-09-25 13:55:02 -04:00
parent bebe70f46b
commit 8d23d9aba3
18 changed files with 296 additions and 130 deletions

View File

@ -252,7 +252,7 @@ namespace Oqtane.Controllers
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized ModuleDefinition Delete Attempt {ModuleDefinitionId}", id);
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized ModuleDefinition Delete Attempt {ModuleDefinitionId} {SiteId}", id, siteid);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
}
}

View File

@ -14,6 +14,9 @@ using System.Text.Json;
using System.Net;
using System;
using Microsoft.Extensions.DependencyInjection;
using System.Reflection.Metadata;
using Oqtane.Security;
using System.Security.Policy;
// ReSharper disable StringIndexOfIsCultureSpecific.1
@ -26,30 +29,50 @@ namespace Oqtane.Controllers
private readonly IInstallationManager _installationManager;
private readonly IWebHostEnvironment _environment;
private readonly ITenantManager _tenantManager;
private readonly IUserPermissions _userPermissions;
private readonly ISyncManager _syncManager;
private readonly ILogManager _logger;
private readonly Alias _alias;
private readonly IServiceProvider _serviceProvider;
public ThemeController(IThemeRepository themes, IInstallationManager installationManager, IWebHostEnvironment environment, ITenantManager tenantManager, ISyncManager syncManager, ILogManager logger, IServiceProvider serviceProvider)
public ThemeController(IThemeRepository themes, IInstallationManager installationManager, IWebHostEnvironment environment, ITenantManager tenantManager, IUserPermissions userPermissions, ISyncManager syncManager, ILogManager logger, IServiceProvider serviceProvider)
{
_themes = themes;
_installationManager = installationManager;
_environment = environment;
_tenantManager = tenantManager;
_userPermissions = userPermissions;
_syncManager = syncManager;
_logger = logger;
_alias = tenantManager.GetAlias();
_serviceProvider = serviceProvider;
}
// GET: api/<controller>
// GET: api/<controller>?siteid=x
[HttpGet]
[Authorize(Roles = RoleNames.Registered)]
public IEnumerable<Theme> Get()
public IEnumerable<Theme> Get(string siteid)
{
return _themes.GetThemes();
}
int SiteId;
if (int.TryParse(siteid, out SiteId) && SiteId == _alias.SiteId)
{
List<Theme> themes = new List<Theme>();
foreach (Theme theme in _themes.GetThemes(SiteId))
{
if (_userPermissions.IsAuthorized(User, PermissionNames.Utilize, theme.PermissionList))
{
themes.Add(theme);
}
}
return themes;
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Theme Get Attempt {SiteId}", siteid);
HttpContext.Response.StatusCode = (int) HttpStatusCode.Forbidden;
return null;
}
}
// GET api/<controller>/5?siteid=x
[HttpGet("{id}")]
@ -58,7 +81,24 @@ namespace Oqtane.Controllers
int SiteId;
if (int.TryParse(siteid, out SiteId) && SiteId == _alias.SiteId)
{
return _themes.GetTheme(id, SiteId);
Theme theme = _themes.GetTheme(id, SiteId);
if (theme != null && _userPermissions.IsAuthorized(User, PermissionNames.Utilize, theme.PermissionList))
{
return theme;
}
else
{
if (theme != null)
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Theme Get Attempt {ThemeId} {SiteId}", id, siteid);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
}
else
{
HttpContext.Response.StatusCode = (int)HttpStatusCode.NotFound;
}
return null;
}
}
else
{
@ -86,14 +126,13 @@ namespace Oqtane.Controllers
}
}
// DELETE api/<controller>/xxx
// DELETE api/<controller>/5?siteid=x
[HttpDelete("{themename}")]
[Authorize(Roles = RoleNames.Host)]
public void Delete(string themename)
public void Delete(int id, int siteid)
{
List<Theme> themes = _themes.GetThemes().ToList();
Theme theme = themes.Where(item => item.ThemeName == themename).FirstOrDefault();
if (theme != null && Utilities.GetAssemblyName(theme.ThemeName) != Constants.ClientId)
Theme theme = _themes.GetTheme(id, siteid);
if (theme != null && theme.SiteId == _alias.SiteId && Utilities.GetAssemblyName(theme.ThemeName) != Constants.ClientId)
{
// remove theme assets
if (_installationManager.UninstallPackage(theme.PackageName))
@ -126,7 +165,7 @@ namespace Oqtane.Controllers
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Theme Delete Attempt {Themename}", themename);
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Theme Delete Attempt {ThemeId} {SiteId}", id, siteid);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
}
}

View File

@ -386,6 +386,7 @@ namespace Oqtane.Repository
moduledefinition.Categories = "Common";
}
// default permissions
if (moduledefinition.Categories == "Admin")
{
var shortName = moduledefinition.ModuleDefinitionName.Replace("Oqtane.Modules.Admin.", "").Replace(", Oqtane.Client", "");
@ -455,18 +456,21 @@ namespace Oqtane.Repository
private List<Permission> ClonePermissions(int siteId, List<Permission> permissionList)
{
var permissions = new List<Permission>();
foreach (var p in permissionList)
if (permissionList != null)
{
var permission = new Permission();
permission.SiteId = siteId;
permission.EntityName = p.EntityName;
permission.EntityId = p.EntityId;
permission.PermissionName = p.PermissionName;
permission.RoleId = null;
permission.RoleName = p.RoleName;
permission.UserId = p.UserId;
permission.IsAuthorized = p.IsAuthorized;
permissions.Add(permission);
foreach (var p in permissionList)
{
var permission = new Permission();
permission.SiteId = siteId;
permission.EntityName = p.EntityName;
permission.EntityId = p.EntityId;
permission.PermissionName = p.PermissionName;
permission.RoleId = null;
permission.RoleName = p.RoleName;
permission.UserId = p.UserId;
permission.IsAuthorized = p.IsAuthorized;
permissions.Add(permission);
}
}
return permissions;
}

View File

@ -135,7 +135,7 @@ namespace Oqtane.Repository
if (site != null)
{
// initialize theme Assemblies
site.Themes = _themeRepository.GetThemes().ToList();
site.Themes = _themeRepository.GetThemes(site.SiteId).ToList();
// initialize module Assemblies
var moduleDefinitions = _moduleDefinitionRepository.GetModuleDefinitions(alias.SiteId);

View File

@ -15,7 +15,7 @@ namespace Oqtane.Repository
{
public interface IThemeRepository
{
IEnumerable<Theme> GetThemes();
IEnumerable<Theme> GetThemes(int siteId);
Theme GetTheme(int themeId, int siteId);
void UpdateTheme(Theme theme);
void DeleteTheme(int themeId);
@ -26,24 +26,25 @@ namespace Oqtane.Repository
{
private MasterDBContext _db;
private readonly IMemoryCache _cache;
private readonly IPermissionRepository _permissions;
private readonly ITenantManager _tenants;
private readonly ISettingRepository _settings;
private readonly IServerStateManager _serverState;
private readonly string settingprefix = "SiteEnabled:";
public ThemeRepository(MasterDBContext context, IMemoryCache cache, ITenantManager tenants, ISettingRepository settings, IServerStateManager serverState)
public ThemeRepository(MasterDBContext context, IMemoryCache cache, IPermissionRepository permissions, ITenantManager tenants, ISettingRepository settings, IServerStateManager serverState)
{
_db = context;
_cache = cache;
_permissions = permissions;
_tenants = tenants;
_settings = settings;
_serverState = serverState;
}
public IEnumerable<Theme> GetThemes()
public IEnumerable<Theme> GetThemes(int siteId)
{
// for consistency siteid should be passed in as parameter, but this would require breaking change
return LoadThemes(_tenants.GetAlias().SiteId);
return LoadThemes(siteId);
}
public Theme GetTheme(int themeId, int siteId)
@ -56,6 +57,7 @@ namespace Oqtane.Repository
{
_db.Entry(theme).State = EntityState.Modified;
_db.SaveChanges();
_permissions.UpdatePermissions(theme.SiteId, EntityNames.Theme, theme.ThemeId, theme.PermissionList);
var settingname = $"{settingprefix}{_tenants.GetAlias().SiteKey}";
var setting = _settings.GetSetting(EntityNames.Theme, theme.ThemeId, settingname);
@ -96,6 +98,7 @@ namespace Oqtane.Repository
Theme.ThemeSettingsType = theme.ThemeSettingsType;
Theme.ContainerSettingsType = theme.ContainerSettingsType;
Theme.PackageName = theme.PackageName;
Theme.PermissionList = theme.PermissionList;
Theme.Fingerprint = Utilities.GenerateSimpleHash(theme.ModifiedOn.ToString("yyyyMMddHHmm"));
Themes.Add(Theme);
}
@ -176,6 +179,9 @@ namespace Oqtane.Repository
var siteKey = _tenants.GetAlias().SiteKey;
var assemblies = new List<string>();
// get all module definition permissions for site
List<Permission> permissions = _permissions.GetPermissions(siteId, EntityNames.Theme).ToList();
// get settings for site
var settings = _settings.GetSettings(EntityNames.Theme).ToList();
@ -212,6 +218,26 @@ namespace Oqtane.Repository
}
}
}
if (permissions.Count == 0)
{
// no module definition permissions exist for this site
theme.PermissionList = ClonePermissions(siteId, theme.PermissionList);
_permissions.UpdatePermissions(siteId, EntityNames.Theme, theme.ThemeId, theme.PermissionList);
}
else
{
if (permissions.Any(item => item.EntityId == theme.ThemeId))
{
theme.PermissionList = permissions.Where(item => item.EntityId == theme.ThemeId).ToList();
}
else
{
// permissions for theme do not exist for this site
theme.PermissionList = ClonePermissions(siteId, theme.PermissionList);
_permissions.UpdatePermissions(siteId, EntityNames.Theme, theme.ThemeId, theme.PermissionList);
}
}
}
// cache site assemblies
@ -220,6 +246,20 @@ namespace Oqtane.Repository
{
if (!serverState.Assemblies.Contains(assembly)) serverState.Assemblies.Add(assembly);
}
// clean up any orphaned permissions
var ids = new HashSet<int>(Themes.Select(item => item.ThemeId));
foreach (var permission in permissions.Where(item => !ids.Contains(item.EntityId)))
{
try
{
_permissions.DeletePermission(permission.PermissionId);
}
catch
{
// multi-threading can cause a race condition to occur
}
}
}
return Themes;
@ -295,6 +335,14 @@ namespace Oqtane.Repository
}
}
}
// default permissions
theme.PermissionList = new List<Permission>
{
new Permission(PermissionNames.Utilize, RoleNames.Admin, true),
new Permission(PermissionNames.Utilize, RoleNames.Registered, true)
};
Debug.WriteLine($"Oqtane Info: Registering Theme {theme.ThemeName}");
themes.Add(theme);
index = themes.FindIndex(item => item.ThemeName == qualifiedThemeType);
@ -335,5 +383,27 @@ namespace Oqtane.Repository
}
return themes;
}
private List<Permission> ClonePermissions(int siteId, List<Permission> permissionList)
{
var permissions = new List<Permission>();
if (permissionList != null)
{
foreach (var p in permissionList)
{
var permission = new Permission();
permission.SiteId = siteId;
permission.EntityName = p.EntityName;
permission.EntityId = p.EntityId;
permission.PermissionName = p.PermissionName;
permission.RoleId = null;
permission.RoleName = p.RoleName;
permission.UserId = p.UserId;
permission.IsAuthorized = p.IsAuthorized;
permissions.Add(permission);
}
}
return permissions;
}
}
}

View File

@ -144,7 +144,7 @@ namespace Oqtane.Services
}
// themes
site.Themes = _themes.FilterThemes(_themes.GetThemes().ToList());
site.Themes = _themes.FilterThemes(_themes.GetThemes(site.SiteId).ToList());
// installation date used for fingerprinting static assets
site.Fingerprint = Utilities.GenerateSimpleHash(_configManager.GetSetting("InstallationDate", DateTime.UtcNow.ToString("yyyyMMddHHmm")));