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

@ -14,7 +14,7 @@
@if (_initialized) @if (_initialized)
{ {
<TabStrip> <TabStrip>
<TabPanel Name="Definition" ResourceKey="Definition" Heading="Definition"> <TabPanel Name="Module" ResourceKey="Module" Heading="Module">
<form @ref="form" class="@(validated ? "was-validated" : "needs-validation")" novalidate> <form @ref="form" class="@(validated ? "was-validated" : "needs-validation")" novalidate>
<div class="container"> <div class="container">
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
@ -236,11 +236,10 @@
private DateTime _createdon; private DateTime _createdon;
private string _modifiedby; private string _modifiedby;
private DateTime _modifiedon; private DateTime _modifiedon;
private List<Page> _pagesWithModules;
#pragma warning disable 649
private PermissionGrid _permissionGrid; private PermissionGrid _permissionGrid;
#pragma warning restore 649
private List<Page> _pagesWithModules;
private List<Package> _packages; private List<Package> _packages;
private List<Language> _languages; private List<Language> _languages;

View File

@ -269,8 +269,16 @@
if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin) || (_parent != null && UserSecurity.IsAuthorized(PageState.User, PermissionNames.Edit, _parent.PermissionList))) if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin) || (_parent != null && UserSecurity.IsAuthorized(PageState.User, PermissionNames.Edit, _parent.PermissionList)))
{ {
_themetype = PageState.Site.DefaultThemeType; _themetype = PageState.Site.DefaultThemeType;
_themes = ThemeService.GetThemeControls(PageState.Site.Themes); var themes = new List<Theme>();
_containers = ThemeService.GetContainerControls(PageState.Site.Themes, _themetype); foreach (var theme in PageState.Site.Themes)
{
if (UserSecurity.IsAuthorized(PageState.User, PermissionNames.Utilize, theme.PermissionList))
{
themes.Add(theme);
}
}
_themes = ThemeService.GetThemeControls(themes);
_containers = ThemeService.GetContainerControls(themes, _themetype);
_containertype = PageState.Site.DefaultContainerType; _containertype = PageState.Site.DefaultContainerType;
_children = new List<Page>(); _children = new List<Page>();
foreach (Page p in _pages.Where(item => (_parentid == "-1" && item.ParentId == null) || (item.ParentId == int.Parse(_parentid)))) foreach (Page p in _pages.Where(item => (_parentid == "-1" && item.ParentId == null) || (item.ParentId == int.Parse(_parentid))))

View File

@ -443,8 +443,16 @@
{ {
_themetype = PageState.Site.DefaultThemeType; _themetype = PageState.Site.DefaultThemeType;
} }
_themes = ThemeService.GetThemeControls(PageState.Site.Themes); var themes = new List<Theme>();
_containers = ThemeService.GetContainerControls(PageState.Site.Themes, _themetype); foreach (var theme in PageState.Site.Themes)
{
if (UserSecurity.IsAuthorized(PageState.User, PermissionNames.Utilize, theme.PermissionList))
{
themes.Add(theme);
}
}
_themes = ThemeService.GetThemeControls(themes);
_containers = ThemeService.GetContainerControls(themes, _themetype);
_containertype = _page.DefaultContainerType; _containertype = _page.DefaultContainerType;
if (string.IsNullOrEmpty(_containertype)) if (string.IsNullOrEmpty(_containertype))
{ {

View File

@ -592,9 +592,17 @@
{ {
_faviconfileid = site.FaviconFileId.Value; _faviconfileid = site.FaviconFileId.Value;
} }
_themes = ThemeService.GetThemeControls(PageState.Site.Themes); var themes = new List<Theme>();
foreach (var theme in PageState.Site.Themes)
{
if (UserSecurity.IsAuthorized(PageState.User, PermissionNames.Utilize, theme.PermissionList))
{
themes.Add(theme);
}
}
_themes = ThemeService.GetThemeControls(themes);
_themetype = (!string.IsNullOrEmpty(site.DefaultThemeType)) ? site.DefaultThemeType : Constants.DefaultTheme; _themetype = (!string.IsNullOrEmpty(site.DefaultThemeType)) ? site.DefaultThemeType : Constants.DefaultTheme;
_containers = ThemeService.GetContainerControls(PageState.Site.Themes, _themetype); _containers = ThemeService.GetContainerControls(themes, _themetype);
_containertype = (!string.IsNullOrEmpty(site.DefaultContainerType)) ? site.DefaultContainerType : Constants.DefaultContainer; _containertype = (!string.IsNullOrEmpty(site.DefaultContainerType)) ? site.DefaultContainerType : Constants.DefaultContainer;
_admincontainertype = (!string.IsNullOrEmpty(site.AdminContainerType)) ? site.AdminContainerType : Constants.DefaultAdminContainer; _admincontainertype = (!string.IsNullOrEmpty(site.AdminContainerType)) ? site.AdminContainerType : Constants.DefaultAdminContainer;
_cookieconsent = SettingService.GetSetting(settings, "CookieConsent", string.Empty); _cookieconsent = SettingService.GetSetting(settings, "CookieConsent", string.Empty);

View File

@ -216,7 +216,7 @@ else
_tenantid = _tenants.First(item => item.Name == TenantNames.Master).TenantId.ToString(); _tenantid = _tenants.First(item => item.Name == TenantNames.Master).TenantId.ToString();
} }
_urls = PageState.Alias.Name; _urls = PageState.Alias.Name;
_themeList = await ThemeService.GetThemesAsync(); _themeList = await ThemeService.GetThemesAsync(PageState.Site.SiteId);
_themes = ThemeService.GetThemeControls(_themeList); _themes = ThemeService.GetThemeControls(_themeList);
if (_themes.Any(item => item.TypeName == Constants.DefaultTheme)) if (_themes.Any(item => item.TypeName == Constants.DefaultTheme))
{ {

View File

@ -195,7 +195,7 @@
{ {
try try
{ {
_themes = await ThemeService.GetThemesAsync(); _themes = await ThemeService.GetThemesAsync(PageState.Site.SiteId);
await LoadPackages(); await LoadPackages();
_initialized = true; _initialized = true;
} }

View File

@ -9,84 +9,98 @@
@if (_initialized) @if (_initialized)
{ {
<form @ref="form" class="@(validated ? "was-validated" : "needs-validation")" novalidate> <TabStrip>
<div class="container"> <TabPanel Name="Theme" ResourceKey="Theme" Heading="Theme">
<div class="row mb-1 align-items-center"> <form @ref="form" class="@(validated ? "was-validated" : "needs-validation")" novalidate>
<Label Class="col-sm-3" For="name" HelpText="The name of the module" ResourceKey="Name">Name: </Label> <div class="container">
<div class="col-sm-9"> <div class="row mb-1 align-items-center">
<input id="name" class="form-control" @bind="@_name" /> <Label Class="col-sm-3" For="name" HelpText="The name of the theme" ResourceKey="Name">Name: </Label>
<div class="col-sm-9">
<input id="name" class="form-control" @bind="@_name" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="isenabled" HelpText="Is theme enabled for this site?" ResourceKey="IsEnabled">Enabled? </Label>
<div class="col-sm-9">
<select id="isenabled" class="form-select" @bind="@_isenabled" required>
<option value="True">@SharedLocalizer["Yes"]</option>
<option value="False">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
</div>
</form>
<Section Name="Information" ResourceKey="Information" Heading="Information">
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="themename" HelpText="The internal name of the module" ResourceKey="InternalName">Internal Name: </Label>
<div class="col-sm-9">
<input id="themename" class="form-control" @bind="@_themeName" disabled />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="version" HelpText="The version of the theme" ResourceKey="Version">Version: </Label>
<div class="col-sm-9">
<input id="version" class="form-control" @bind="@_version" disabled />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="packagename" HelpText="The unique name of the package from which this theme was installed. This value must be specified within the theme's ITheme interface specification." ResourceKey="PackageName">Package Name: </Label>
<div class="col-sm-9">
<input id="packagename" class="form-control" @bind="@_packagename" disabled />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="owner" HelpText="The owner or creator of the theme" ResourceKey="Owner">Owner: </Label>
<div class="col-sm-9">
<input id="owner" class="form-control" @bind="@_owner" disabled />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="url" HelpText="The url of the theme" ResourceKey="Url">Url: </Label>
<div class="col-sm-9">
<input id="url" class="form-control" @bind="@_url" disabled />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="contact" HelpText="The contact for the theme" ResourceKey="Contact">Contact: </Label>
<div class="col-sm-9">
<input id="contact" class="form-control" @bind="@_contact" disabled />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="license" HelpText="The license of the theme" ResourceKey="License">License: </Label>
<div class="col-sm-9">
@if (_license.StartsWith("http") || _license.StartsWith("/") || _license.StartsWith("~"))
{
<a href="@_license.Replace("~", PageState?.Alias.BaseUrl + "/Themes/" + Utilities.GetTypeName(_themeName))" class="btn btn-info" style="text-decoration: none !important" target="_new">@Localizer["View License"]</a>
}
else
{
<textarea id="license" class="form-control" @bind="@_license" rows="5" disabled></textarea>
}
</div>
</div>
</div>
</Section>
<br />
<button type="button" class="btn btn-success" @onclick="SaveTheme">@SharedLocalizer["Save"]</button>
<NavLink class="btn btn-secondary" href="@NavigateUrl()">@SharedLocalizer["Cancel"]</NavLink>
<br />
<br />
<AuditInfo CreatedBy="@_createdby" CreatedOn="@_createdon" ModifiedBy="@_modifiedby" ModifiedOn="@_modifiedon"></AuditInfo>
</TabPanel>
<TabPanel Name="Permissions" ResourceKey="Permissions" Heading="Permissions">
<div class="container">
<div class="row mb-1 align-items-center">
<PermissionGrid EntityName="@EntityNames.Theme" PermissionNames="@PermissionNames.Utilize" PermissionList="@_permissions" @ref="_permissionGrid" />
</div> </div>
</div> </div>
<div class="row mb-1 align-items-center"> <br />
<Label Class="col-sm-3" For="isenabled" HelpText="Is theme enabled for this site?" ResourceKey="IsEnabled">Enabled? </Label> <button type="button" class="btn btn-success" @onclick="SaveTheme">@SharedLocalizer["Save"]</button>
<div class="col-sm-9"> <NavLink class="btn btn-secondary" href="@NavigateUrl()">@SharedLocalizer["Cancel"]</NavLink>
<select id="isenabled" class="form-select" @bind="@_isenabled" required> </TabPanel>
<option value="True">@SharedLocalizer["Yes"]</option> </TabStrip>
<option value="False">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
</div>
</form>
<Section Name="Information" ResourceKey="Information" Heading="Information">
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="themename" HelpText="The internal name of the module" ResourceKey="InternalName">Internal Name: </Label>
<div class="col-sm-9">
<input id="themename" class="form-control" @bind="@_themeName" disabled />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="version" HelpText="The version of the theme" ResourceKey="Version">Version: </Label>
<div class="col-sm-9">
<input id="version" class="form-control" @bind="@_version" disabled />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="packagename" HelpText="The unique name of the package from which this theme was installed. This value must be specified within the theme's ITheme interface specification." ResourceKey="PackageName">Package Name: </Label>
<div class="col-sm-9">
<input id="packagename" class="form-control" @bind="@_packagename" disabled />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="owner" HelpText="The owner or creator of the theme" ResourceKey="Owner">Owner: </Label>
<div class="col-sm-9">
<input id="owner" class="form-control" @bind="@_owner" disabled />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="url" HelpText="The url of the theme" ResourceKey="Url">Url: </Label>
<div class="col-sm-9">
<input id="url" class="form-control" @bind="@_url" disabled />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="contact" HelpText="The contact for the theme" ResourceKey="Contact">Contact: </Label>
<div class="col-sm-9">
<input id="contact" class="form-control" @bind="@_contact" disabled />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="license" HelpText="The license of the theme" ResourceKey="License">License: </Label>
<div class="col-sm-9">
@if (_license.StartsWith("http") || _license.StartsWith("/") || _license.StartsWith("~"))
{
<a href="@_license.Replace("~", PageState?.Alias.BaseUrl + "/Themes/" + Utilities.GetTypeName(_themeName))" class="btn btn-info" style="text-decoration: none !important" target="_new">@Localizer["View License"]</a>
}
else
{
<textarea id="license" class="form-control" @bind="@_license" rows="5" disabled></textarea>
}
</div>
</div>
</div>
</Section>
<br />
<button type="button" class="btn btn-success" @onclick="SaveTheme">@SharedLocalizer["Save"]</button>
<NavLink class="btn btn-secondary" href="@NavigateUrl()">@SharedLocalizer["Cancel"]</NavLink>
<br />
<br />
<AuditInfo CreatedBy="@_createdby" CreatedOn="@_createdon" ModifiedBy="@_modifiedby" ModifiedOn="@_modifiedon"></AuditInfo>
} }
@code { @code {
@ -103,11 +117,14 @@
private string _url = ""; private string _url = "";
private string _contact = ""; private string _contact = "";
private string _license = ""; private string _license = "";
private List<Permission> _permissions = null;
private string _createdby; private string _createdby;
private DateTime _createdon; private DateTime _createdon;
private string _modifiedby; private string _modifiedby;
private DateTime _modifiedon; private DateTime _modifiedon;
private PermissionGrid _permissionGrid;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Host; public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Host;
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
@ -126,6 +143,7 @@
_url = theme.Url; _url = theme.Url;
_contact = theme.Contact; _contact = theme.Contact;
_license = theme.License; _license = theme.License;
_permissions = theme.PermissionList;
_createdby = theme.CreatedBy; _createdby = theme.CreatedBy;
_createdon = theme.CreatedOn; _createdon = theme.CreatedOn;
_modifiedby = theme.ModifiedBy; _modifiedby = theme.ModifiedBy;
@ -152,6 +170,7 @@
var theme = await ThemeService.GetThemeAsync(_themeId, ModuleState.SiteId); var theme = await ThemeService.GetThemeAsync(_themeId, ModuleState.SiteId);
theme.Name = _name; theme.Name = _name;
theme.IsEnabled = (_isenabled == null ? true : bool.Parse(_isenabled)); theme.IsEnabled = (_isenabled == null ? true : bool.Parse(_isenabled));
theme.PermissionList = _permissionGrid.GetPermissionList();
await ThemeService.UpdateThemeAsync(theme); await ThemeService.UpdateThemeAsync(theme);
await logger.LogInformation("Theme Saved {Theme}", theme); await logger.LogInformation("Theme Saved {Theme}", theme);
NavigationManager.NavigateTo(NavigateUrl()); NavigationManager.NavigateTo(NavigateUrl());

View File

@ -78,7 +78,7 @@ else
{ {
try try
{ {
_themes = await ThemeService.GetThemesAsync(); _themes = await ThemeService.GetThemesAsync(PageState.Site.SiteId);
_packages = await PackageService.GetPackageUpdatesAsync("theme"); _packages = await PackageService.GetPackageUpdatesAsync("theme");
} }
catch (Exception ex) catch (Exception ex)
@ -161,7 +161,7 @@ else
{ {
try try
{ {
await ThemeService.DeleteThemeAsync(Theme.ThemeName); await ThemeService.DeleteThemeAsync(Theme.ThemeId, PageState.Site.SiteId);
AddModuleMessage(Localizer["Success.Theme.Delete"], MessageType.Success); AddModuleMessage(Localizer["Success.Theme.Delete"], MessageType.Success);
NavigationManager.NavigateTo(NavigateUrl(PageState.Page.Path, true)); NavigationManager.NavigateTo(NavigateUrl(PageState.Page.Path, true));
} }

View File

@ -183,8 +183,8 @@
<data name="Runtimes.Text" xml:space="preserve"> <data name="Runtimes.Text" xml:space="preserve">
<value>Runtimes: </value> <value>Runtimes: </value>
</data> </data>
<data name="Definition.Heading" xml:space="preserve"> <data name="Module.Heading" xml:space="preserve">
<value>Definition</value> <value>Module</value>
</data> </data>
<data name="Information.Heading" xml:space="preserve"> <data name="Information.Heading" xml:space="preserve">
<value>Information</value> <value>Information</value>

View File

@ -180,4 +180,10 @@
<data name="View License" xml:space="preserve"> <data name="View License" xml:space="preserve">
<value>View License</value> <value>View License</value>
</data> </data>
<data name="Theme.Heading" xml:space="preserve">
<value>Themex</value>
</data>
<data name="Permissions.Heading" xml:space="preserve">
<value>Permissionsx</value>
</data>
</root> </root>

View File

@ -17,8 +17,9 @@ namespace Oqtane.Services
/// <summary> /// <summary>
/// Returns a list of available themes /// Returns a list of available themes
/// </summary> /// </summary>
/// <param name="siteId"></param>
/// <returns></returns> /// <returns></returns>
Task<List<Theme>> GetThemesAsync(); Task<List<Theme>> GetThemesAsync(int siteId);
/// <summary> /// <summary>
/// Returns a specific theme /// Returns a specific theme
@ -69,9 +70,10 @@ namespace Oqtane.Services
/// <summary> /// <summary>
/// Deletes a theme /// Deletes a theme
/// </summary> /// </summary>
/// <param name="themeName"></param> /// <param name="themeId"></param>
/// <param name="siteId"></param>
/// <returns></returns> /// <returns></returns>
Task DeleteThemeAsync(string themeName); Task DeleteThemeAsync(int themeId, int siteId);
/// <summary> /// <summary>
/// Creates a new theme /// Creates a new theme
@ -103,9 +105,9 @@ namespace Oqtane.Services
private string ApiUrl => CreateApiUrl("Theme"); private string ApiUrl => CreateApiUrl("Theme");
public async Task<List<Theme>> GetThemesAsync() public async Task<List<Theme>> GetThemesAsync(int siteId)
{ {
List<Theme> themes = await GetJsonAsync<List<Theme>>(ApiUrl); List<Theme> themes = await GetJsonAsync<List<Theme>>($"{ApiUrl}?siteid={siteId}");
return themes.OrderBy(item => item.Name).ToList(); return themes.OrderBy(item => item.Name).ToList();
} }
public async Task<Theme> GetThemeAsync(int themeId, int siteId) public async Task<Theme> GetThemeAsync(int themeId, int siteId)
@ -139,9 +141,9 @@ namespace Oqtane.Services
await PutJsonAsync($"{ApiUrl}/{theme.ThemeId}", theme); await PutJsonAsync($"{ApiUrl}/{theme.ThemeId}", theme);
} }
public async Task DeleteThemeAsync(string themeName) public async Task DeleteThemeAsync(int themeId, int siteId)
{ {
await DeleteAsync($"{ApiUrl}/{themeName}"); await DeleteAsync($"{ApiUrl}/{themeId}?siteid={siteId}");
} }
public async Task<Theme> CreateThemeAsync(Theme theme) public async Task<Theme> CreateThemeAsync(Theme theme)

View File

@ -252,7 +252,7 @@ namespace Oqtane.Controllers
} }
else 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; HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
} }
} }

View File

@ -14,6 +14,9 @@ using System.Text.Json;
using System.Net; using System.Net;
using System; using System;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using System.Reflection.Metadata;
using Oqtane.Security;
using System.Security.Policy;
// ReSharper disable StringIndexOfIsCultureSpecific.1 // ReSharper disable StringIndexOfIsCultureSpecific.1
@ -26,30 +29,50 @@ namespace Oqtane.Controllers
private readonly IInstallationManager _installationManager; private readonly IInstallationManager _installationManager;
private readonly IWebHostEnvironment _environment; private readonly IWebHostEnvironment _environment;
private readonly ITenantManager _tenantManager; private readonly ITenantManager _tenantManager;
private readonly IUserPermissions _userPermissions;
private readonly ISyncManager _syncManager; private readonly ISyncManager _syncManager;
private readonly ILogManager _logger; private readonly ILogManager _logger;
private readonly Alias _alias; private readonly Alias _alias;
private readonly IServiceProvider _serviceProvider; 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; _themes = themes;
_installationManager = installationManager; _installationManager = installationManager;
_environment = environment; _environment = environment;
_tenantManager = tenantManager; _tenantManager = tenantManager;
_userPermissions = userPermissions;
_syncManager = syncManager; _syncManager = syncManager;
_logger = logger; _logger = logger;
_alias = tenantManager.GetAlias(); _alias = tenantManager.GetAlias();
_serviceProvider = serviceProvider; _serviceProvider = serviceProvider;
} }
// GET: api/<controller> // GET: api/<controller>?siteid=x
[HttpGet] [HttpGet]
[Authorize(Roles = RoleNames.Registered)] [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 // GET api/<controller>/5?siteid=x
[HttpGet("{id}")] [HttpGet("{id}")]
@ -58,7 +81,24 @@ namespace Oqtane.Controllers
int SiteId; int SiteId;
if (int.TryParse(siteid, out SiteId) && SiteId == _alias.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 else
{ {
@ -86,14 +126,13 @@ namespace Oqtane.Controllers
} }
} }
// DELETE api/<controller>/xxx // DELETE api/<controller>/5?siteid=x
[HttpDelete("{themename}")] [HttpDelete("{themename}")]
[Authorize(Roles = RoleNames.Host)] [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.GetTheme(id, siteid);
Theme theme = themes.Where(item => item.ThemeName == themename).FirstOrDefault(); if (theme != null && theme.SiteId == _alias.SiteId && Utilities.GetAssemblyName(theme.ThemeName) != Constants.ClientId)
if (theme != null && Utilities.GetAssemblyName(theme.ThemeName) != Constants.ClientId)
{ {
// remove theme assets // remove theme assets
if (_installationManager.UninstallPackage(theme.PackageName)) if (_installationManager.UninstallPackage(theme.PackageName))
@ -126,7 +165,7 @@ namespace Oqtane.Controllers
} }
else 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; HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
} }
} }

View File

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

View File

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

View File

@ -15,7 +15,7 @@ namespace Oqtane.Repository
{ {
public interface IThemeRepository public interface IThemeRepository
{ {
IEnumerable<Theme> GetThemes(); IEnumerable<Theme> GetThemes(int siteId);
Theme GetTheme(int themeId, int siteId); Theme GetTheme(int themeId, int siteId);
void UpdateTheme(Theme theme); void UpdateTheme(Theme theme);
void DeleteTheme(int themeId); void DeleteTheme(int themeId);
@ -26,24 +26,25 @@ namespace Oqtane.Repository
{ {
private MasterDBContext _db; private MasterDBContext _db;
private readonly IMemoryCache _cache; private readonly IMemoryCache _cache;
private readonly IPermissionRepository _permissions;
private readonly ITenantManager _tenants; private readonly ITenantManager _tenants;
private readonly ISettingRepository _settings; private readonly ISettingRepository _settings;
private readonly IServerStateManager _serverState; private readonly IServerStateManager _serverState;
private readonly string settingprefix = "SiteEnabled:"; 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; _db = context;
_cache = cache; _cache = cache;
_permissions = permissions;
_tenants = tenants; _tenants = tenants;
_settings = settings; _settings = settings;
_serverState = serverState; _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(siteId);
return LoadThemes(_tenants.GetAlias().SiteId);
} }
public Theme GetTheme(int themeId, int siteId) public Theme GetTheme(int themeId, int siteId)
@ -56,6 +57,7 @@ namespace Oqtane.Repository
{ {
_db.Entry(theme).State = EntityState.Modified; _db.Entry(theme).State = EntityState.Modified;
_db.SaveChanges(); _db.SaveChanges();
_permissions.UpdatePermissions(theme.SiteId, EntityNames.Theme, theme.ThemeId, theme.PermissionList);
var settingname = $"{settingprefix}{_tenants.GetAlias().SiteKey}"; var settingname = $"{settingprefix}{_tenants.GetAlias().SiteKey}";
var setting = _settings.GetSetting(EntityNames.Theme, theme.ThemeId, settingname); var setting = _settings.GetSetting(EntityNames.Theme, theme.ThemeId, settingname);
@ -96,6 +98,7 @@ namespace Oqtane.Repository
Theme.ThemeSettingsType = theme.ThemeSettingsType; Theme.ThemeSettingsType = theme.ThemeSettingsType;
Theme.ContainerSettingsType = theme.ContainerSettingsType; Theme.ContainerSettingsType = theme.ContainerSettingsType;
Theme.PackageName = theme.PackageName; Theme.PackageName = theme.PackageName;
Theme.PermissionList = theme.PermissionList;
Theme.Fingerprint = Utilities.GenerateSimpleHash(theme.ModifiedOn.ToString("yyyyMMddHHmm")); Theme.Fingerprint = Utilities.GenerateSimpleHash(theme.ModifiedOn.ToString("yyyyMMddHHmm"));
Themes.Add(Theme); Themes.Add(Theme);
} }
@ -176,6 +179,9 @@ namespace Oqtane.Repository
var siteKey = _tenants.GetAlias().SiteKey; var siteKey = _tenants.GetAlias().SiteKey;
var assemblies = new List<string>(); 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 // get settings for site
var settings = _settings.GetSettings(EntityNames.Theme).ToList(); 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 // cache site assemblies
@ -220,6 +246,20 @@ namespace Oqtane.Repository
{ {
if (!serverState.Assemblies.Contains(assembly)) serverState.Assemblies.Add(assembly); 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; 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}"); Debug.WriteLine($"Oqtane Info: Registering Theme {theme.ThemeName}");
themes.Add(theme); themes.Add(theme);
index = themes.FindIndex(item => item.ThemeName == qualifiedThemeType); index = themes.FindIndex(item => item.ThemeName == qualifiedThemeType);
@ -335,5 +383,27 @@ namespace Oqtane.Repository
} }
return themes; 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 // themes
site.Themes = _themes.FilterThemes(_themes.GetThemes().ToList()); site.Themes = _themes.FilterThemes(_themes.GetThemes(site.SiteId).ToList());
// installation date used for fingerprinting static assets // installation date used for fingerprinting static assets
site.Fingerprint = Utilities.GenerateSimpleHash(_configManager.GetSetting("InstallationDate", DateTime.UtcNow.ToString("yyyyMMddHHmm"))); site.Fingerprint = Utilities.GenerateSimpleHash(_configManager.GetSetting("InstallationDate", DateTime.UtcNow.ToString("yyyyMMddHHmm")));

View File

@ -94,6 +94,9 @@ namespace Oqtane.Models
[NotMapped] [NotMapped]
public List<ThemeControl> Containers { get; set; } public List<ThemeControl> Containers { get; set; }
[NotMapped]
public List<Permission> PermissionList { get; set; }
[NotMapped] [NotMapped]
public string Template { get; set; } public string Template { get; set; }