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

@ -49,7 +49,6 @@ namespace Microsoft.Extensions.DependencyInjection
services.AddScoped<IUrlMappingService, UrlMappingService>();
services.AddScoped<IVisitorService, VisitorService>();
services.AddScoped<ISyncService, SyncService>();
services.AddScoped<IApiService, ApiService>();
return services;
}

View File

@ -1,75 +0,0 @@
@namespace Oqtane.Modules.Admin.Apis
@inherits ModuleBase
@inject IApiService ApiService
@inject NavigationManager NavigationManager
@inject IStringLocalizer<Edit> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="entityname" HelpText="The name of the entity" ResourceKey="EntityName">Entity: </Label>
<div class="col-sm-9">
<input id="entityname" class="form-control" @bind="@_entityname" readonly />
</div>
</div>
<br />
<div class="row mb-1 align-items-center">
@if (_permissions != null)
{
<PermissionGrid EntityName="@_entityname" PermissionNames="@_permissionnames" Permissions="@_permissions" @ref="_permissionGrid" />
}
</div>
</div>
<button type="button" class="btn btn-success" @onclick="SaveModuleDefinition">@SharedLocalizer["Save"]</button>
<NavLink class="btn btn-secondary" href="@NavigateUrl()">@SharedLocalizer["Cancel"]</NavLink>
@code {
private string _entityname;
private string _permissionnames;
private string _permissions;
#pragma warning disable 649
private PermissionGrid _permissionGrid;
#pragma warning restore 649
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin;
protected override async Task OnInitializedAsync()
{
try
{
_entityname = PageState.QueryString["entity"];
var api = await ApiService.GetApiAsync(PageState.Site.SiteId, _entityname);
if (api != null)
{
var apis = await ApiService.GetApisAsync(PageState.Site.SiteId);
_permissionnames = apis.SingleOrDefault(item => item.EntityName == _entityname).Permissions;
_permissions = api.Permissions;
}
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Loading API {EntityName} {Error}", _entityname, ex.Message);
AddModuleMessage(Localizer["Error.Module.Load"], MessageType.Error);
}
}
private async Task SaveModuleDefinition()
{
try
{
var api = new Api();
api.SiteId = PageState.Site.SiteId;
api.EntityName = _entityname;
api.Permissions = _permissionGrid.GetPermissions();
await ApiService.UpdateApiAsync(api);
await logger.LogInformation("API Saved {Api}", api);
NavigationManager.NavigateTo(NavigateUrl());
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Saving Api {EntityName} {Error}", _entityname, ex.Message);
AddModuleMessage(Localizer["Error.Module.Save"], MessageType.Error);
}
}
}

View File

@ -1,36 +0,0 @@
@namespace Oqtane.Modules.Admin.Apis
@inherits ModuleBase
@inject IApiService ApiService
@inject IStringLocalizer<Index> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer
@if (_apis == null)
{
<p><em>@SharedLocalizer["Loading"]</em></p>
}
else
{
<Pager Items="@_apis">
<Header>
<th style="width: 1px;">&nbsp;</th>
<th>@Localizer["Entity"]</th>
<th>@Localizer["Permissions"]</th>
</Header>
<Row>
<td><ActionLink Action="Edit" Parameters="@($"entity=" + context.EntityName)" ResourceKey="Edit" /></td>
<td>@context.EntityName</td>
<td>@context.Permissions</td>
</Row>
</Pager>
}
@code {
private List<Api> _apis;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin;
protected override async Task OnParametersSetAsync()
{
_apis = await ApiService.GetApisAsync(PageState.Site.SiteId);
}
}

View File

@ -20,13 +20,16 @@
</div>
@code {
private List<Page> _pages;
private List<Page> _pages;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Anonymous;
protected override void OnInitialized()
{
var admin = PageState.Pages.FirstOrDefault(item => item.Path == "admin");
_pages = PageState.Pages.Where(item => item.ParentId == admin?.PageId).ToList();
protected override void OnInitialized()
{
var admin = PageState.Pages.FirstOrDefault(item => item.Path == "admin");
if (admin != null)
{
_pages = PageState.Pages.Where(item => item.ParentId == admin?.PageId).ToList();
}
}
}

View File

@ -131,7 +131,7 @@ else
moduleDefinition = await ModuleDefinitionService.CreateModuleDefinitionAsync(moduleDefinition);
var settings = await SettingService.GetModuleSettingsAsync(ModuleState.ModuleId);
SettingService.SetSetting(settings, "ModuleDefinitionName", moduleDefinition.ModuleDefinitionName);
settings = SettingService.SetSetting(settings, "ModuleDefinitionName", moduleDefinition.ModuleDefinitionName);
await SettingService.UpdateModuleSettingsAsync(settings, ModuleState.ModuleId);
GetLocation();

View File

@ -157,7 +157,8 @@
</TabPanel>
}
</TabStrip>
<button type="button" class="btn btn-success" @onclick="SavePage">@SharedLocalizer["Save"]</button>
<br />
<button type="button" class="btn btn-success" @onclick="SavePage">@SharedLocalizer["Save"]</button>
<button type="button" class="btn btn-secondary" @onclick="Cancel">@SharedLocalizer["Cancel"]</button>
</form>

View File

@ -148,7 +148,8 @@
</div>
</div>
</Section>
<br /><br />
<br />
<br />
<AuditInfo CreatedBy="@_createdby" CreatedOn="@_createdon" ModifiedBy="@_modifiedby" ModifiedOn="@_modifiedon" DeletedBy="@_deletedby" DeletedOn="@_deletedon"></AuditInfo>
}
</TabPanel>
@ -189,7 +190,8 @@
<br />
}
</TabStrip>
<button type="button" class="btn btn-success" @onclick="SavePage">@SharedLocalizer["Save"]</button>
<br />
<button type="button" class="btn btn-success" @onclick="SavePage">@SharedLocalizer["Save"]</button>
<button type="button" class="btn btn-secondary" @onclick="Cancel">@SharedLocalizer["Cancel"]</button>
</form>

View File

@ -0,0 +1,19 @@
using Oqtane.Documentation;
using Oqtane.Models;
using Oqtane.Shared;
namespace Oqtane.Modules.Admin.Profiles
{
[PrivateApi("Mark this as private, since it's not very useful in the public docs")]
public class ModuleInfo : IModule
{
public ModuleDefinition ModuleDefinition => new ModuleDefinition
{
Name = "Profiles",
Description = "Manage Profiles",
Categories = "Admin",
Version = Constants.Version,
PermissionNames = $"{PermissionNames.View},{PermissionNames.Edit},{EntityNames.Profile}:{PermissionNames.Write}:{RoleNames.Admin}"
};
}
}

View File

@ -42,7 +42,7 @@
private string _description = string.Empty;
private string _isautoassigned = "False";
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Edit;
private async Task SaveRole()
{

View File

@ -49,7 +49,7 @@
private string _modifiedby;
private DateTime _modifiedon;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Edit;
protected override async Task OnInitializedAsync()
{

View File

@ -10,7 +10,7 @@
}
else
{
<ActionLink Action="Add" Text="Add Role" ResourceKey="AddRole" />
<ActionLink Action="Add" Text="Add Role" Security="SecurityAccessLevel.Edit" ResourceKey="AddRole" />
<Pager Items="@_roles">
<Header>
@ -20,9 +20,9 @@ else
<th>@SharedLocalizer["Name"]</th>
</Header>
<Row>
<td><ActionLink Action="Edit" Parameters="@($"id=" + context.RoleId.ToString())" Disabled="@(context.IsSystem)" ResourceKey="Edit" /></td>
<td><ActionDialog Header="Delete Role" Message="@string.Format(Localizer["Confirm.DeleteUser"], context.Name)" Action="Delete" Security="SecurityAccessLevel.Admin" Class="btn btn-danger" OnClick="@(async () => await DeleteRole(context))" Disabled="@(context.IsSystem)" ResourceKey="DeleteRole" /></td>
<td><ActionLink Action="Users" Parameters="@($"id=" + context.RoleId.ToString())" ResourceKey="Users" /></td>
<td><ActionLink Action="Edit" Parameters="@($"id=" + context.RoleId.ToString())" Security="SecurityAccessLevel.Edit" Disabled="@(context.IsSystem)" ResourceKey="Edit" /></td>
<td><ActionDialog Header="Delete Role" Message="@string.Format(Localizer["Confirm.DeleteUser"], context.Name)" Action="Delete" Security="SecurityAccessLevel.Edit" Class="btn btn-danger" OnClick="@(async () => await DeleteRole(context))" Disabled="@(context.IsSystem)" ResourceKey="DeleteRole" /></td>
<td><ActionLink Action="Users" Parameters="@($"id=" + context.RoleId.ToString())" Security="SecurityAccessLevel.Edit" ResourceKey="Users" /></td>
<td>@context.Name</td>
</Row>
</Pager>
@ -31,7 +31,7 @@ else
@code {
private List<Role> _roles;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.View;
protected override async Task OnParametersSetAsync()
{

View File

@ -0,0 +1,19 @@
using Oqtane.Documentation;
using Oqtane.Models;
using Oqtane.Shared;
namespace Oqtane.Modules.Admin.Roles
{
[PrivateApi("Mark this as private, since it's not very useful in the public docs")]
public class ModuleInfo : IModule
{
public ModuleDefinition ModuleDefinition => new ModuleDefinition
{
Name = "Roles",
Description = "Manage Roles",
Categories = "Admin",
Version = Constants.Version,
PermissionNames = $"{PermissionNames.View},{PermissionNames.Edit},{EntityNames.Role}:{PermissionNames.Write}:{RoleNames.Admin},{EntityNames.UserRole}:{PermissionNames.Write}:{RoleNames.Admin}"
};
}
}

View File

@ -23,13 +23,7 @@ else
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="user" HelpText="Select a user" ResourceKey="User">User: </Label>
<div class="col-sm-9">
<select id="user" class="form-select" @bind="@userid" required>
<option value="-1">&lt;@Localizer["User.Select"]&gt;</option>
@foreach (UserRole userrole in users)
{
<option value="@(userrole.UserId)">@userrole.User.DisplayName</option>
}
</select>
<AutoComplete OnSearch="GetUsers" Placeholder="@Localizer["User.Select"]" @ref="user" />
</div>
</div>
<div class="row mb-1 align-items-center">
@ -64,7 +58,7 @@ else
<td>@context.EffectiveDate</td>
<td>@context.ExpiryDate</td>
<td>
<ActionDialog Header="Remove User" Message="@string.Format(Localizer["Confirm.User.DeleteRole"], context.User.DisplayName)" Action="Delete" Security="SecurityAccessLevel.Admin" Class="btn btn-danger" OnClick="@(async () => await DeleteUserRole(context.UserRoleId))" Disabled="@(context.Role.IsAutoAssigned || PageState.User.Username == UserNames.Host)" ResourceKey="DeleteUserRole" />
<ActionDialog Header="Remove User" Message="@string.Format(Localizer["Confirm.User.DeleteRole"], context.User.DisplayName)" Action="Delete" Security="SecurityAccessLevel.Edit" Class="btn btn-danger" OnClick="@(async () => await DeleteUserRole(context.UserRoleId))" Disabled="@(context.Role.IsAutoAssigned || context.User.Username == UserNames.Host || context.User.UserId == PageState.User.UserId)" ResourceKey="DeleteUserRole" />
</td>
</Row>
</Pager>
@ -75,81 +69,96 @@ else
}
@code {
private ElementReference form;
private bool validated = false;
private ElementReference form;
private bool validated = false;
private int roleid;
private string name = string.Empty;
private List<UserRole> users;
private int userid = -1;
private DateTime? effectivedate = null;
private DateTime? expirydate = null;
private List<UserRole> userroles;
private int roleid;
private string name = string.Empty;
private AutoComplete user;
private DateTime? effectivedate = null;
private DateTime? expirydate = null;
private List<UserRole> userroles;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Edit;
protected override async Task OnInitializedAsync()
{
try
{
roleid = Int32.Parse(PageState.QueryString["id"]);
Role role = await RoleService.GetRoleAsync(roleid);
name = role.Name;
users = await UserRoleService.GetUserRolesAsync(PageState.Site.SiteId, RoleNames.Registered);
await GetUserRoles();
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Loading Users {Error}", ex.Message);
AddModuleMessage(Localizer["Error.User.Load"], MessageType.Error);
}
}
protected override async Task OnInitializedAsync()
{
try
{
roleid = Int32.Parse(PageState.QueryString["id"]);
Role role = await RoleService.GetRoleAsync(roleid);
name = role.Name;
await GetUserRoles();
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Loading Users {Error}", ex.Message);
AddModuleMessage(Localizer["Error.User.Load"], MessageType.Error);
}
}
private async Task GetUserRoles()
{
try
{
userroles = await UserRoleService.GetUserRolesAsync(PageState.Site.SiteId, name);
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Loading User Roles {RoleId} {Error}", roleid, ex.Message);
AddModuleMessage(Localizer["Error.User.LoadRole"], MessageType.Error);
}
}
private async Task<Dictionary<string, string>> GetUsers(string filter)
{
try
{
var users = await UserRoleService.GetUserRolesAsync(PageState.Site.SiteId, RoleNames.Registered);
return users.Where(item => item.User.DisplayName.Contains(filter, StringComparison.OrdinalIgnoreCase))
.ToDictionary(item => item.UserId.ToString(), item => item.User.DisplayName);
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Loading Users {filter} {Error}", filter, ex.Message);
AddModuleMessage(Localizer["Error.User.Load"], MessageType.Error);
}
return new Dictionary<string, string>();
}
private async Task SaveUserRole()
{
validated = true;
var interop = new Interop(JSRuntime);
if (await interop.FormValid(form))
{
try
{
if (userid != -1)
{
var userrole = userroles.Where(item => item.UserId == userid && item.RoleId == roleid).FirstOrDefault();
if (userrole != null)
{
userrole.EffectiveDate = effectivedate;
userrole.ExpiryDate = expirydate;
await UserRoleService.UpdateUserRoleAsync(userrole);
}
else
{
userrole = new UserRole();
userrole.UserId = userid;
userrole.RoleId = roleid;
userrole.EffectiveDate = effectivedate;
userrole.ExpiryDate = expirydate;
private async Task GetUserRoles()
{
try
{
userroles = await UserRoleService.GetUserRolesAsync(PageState.Site.SiteId, name);
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Loading User Roles {RoleId} {Error}", roleid, ex.Message);
AddModuleMessage(Localizer["Error.User.LoadRole"], MessageType.Error);
}
}
await UserRoleService.AddUserRoleAsync(userrole);
}
private async Task SaveUserRole()
{
validated = true;
var interop = new Interop(JSRuntime);
if (await interop.FormValid(form))
{
try
{
if (!string.IsNullOrEmpty(user.Key) && int.TryParse(user.Key, out int userid))
{
var userrole = userroles.Where(item => item.UserId == userid && item.RoleId == roleid).FirstOrDefault();
if (userrole != null)
{
userrole.EffectiveDate = effectivedate;
userrole.ExpiryDate = expirydate;
await UserRoleService.UpdateUserRoleAsync(userrole);
}
else
{
userrole = new UserRole();
userrole.UserId = userid;
userrole.RoleId = roleid;
userrole.EffectiveDate = effectivedate;
userrole.ExpiryDate = expirydate;
await logger.LogInformation("User Assigned To Role {UserRole}", userrole);
AddModuleMessage(Localizer["Success.User.AssignedRole"], MessageType.Success);
await GetUserRoles();
StateHasChanged();
await UserRoleService.AddUserRoleAsync(userrole);
}
await logger.LogInformation("User Assigned To Role {UserRole}", userrole);
AddModuleMessage(Localizer["Success.User.AssignedRole"], MessageType.Success);
await GetUserRoles();
user.Clear();
StateHasChanged();
}
else
{

View File

@ -602,12 +602,12 @@
try
{
var settings = await SettingService.GetSiteSettingsAsync(PageState.Site.SiteId);
SettingService.SetSetting(settings, "SMTPHost", _smtphost, true);
SettingService.SetSetting(settings, "SMTPPort", _smtpport, true);
SettingService.SetSetting(settings, "SMTPSSL", _smtpssl, true);
SettingService.SetSetting(settings, "SMTPUsername", _smtpusername, true);
SettingService.SetSetting(settings, "SMTPPassword", _smtppassword, true);
SettingService.SetSetting(settings, "SMTPSender", _smtpsender, true);
settings = SettingService.SetSetting(settings, "SMTPHost", _smtphost, true);
settings = SettingService.SetSetting(settings, "SMTPPort", _smtpport, true);
settings = SettingService.SetSetting(settings, "SMTPSSL", _smtpssl, true);
settings = SettingService.SetSetting(settings, "SMTPUsername", _smtpusername, true);
settings = SettingService.SetSetting(settings, "SMTPPassword", _smtppassword, true);
settings = SettingService.SetSetting(settings, "SMTPSender", _smtpsender, true);
await SettingService.UpdateSiteSettingsAsync(settings, PageState.Site.SiteId);
await logger.LogInformation("Site SMTP Settings Saved");

View File

@ -305,7 +305,14 @@ else
}
private string GetProfileValue(string SettingName, string DefaultValue)
=> SettingService.GetSetting(settings, SettingName, DefaultValue);
{
string value = SettingService.GetSetting(settings, SettingName, DefaultValue);
if (value.Contains("]"))
{
value = value.Substring(value.IndexOf("]") + 1);
}
return value;
}
private async Task Save()
{

View File

@ -104,7 +104,7 @@
private Dictionary<string, string> settings;
private string category = string.Empty;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Edit;
protected override async Task OnInitializedAsync()
{
@ -121,8 +121,15 @@
}
}
private string GetProfileValue(string SettingName, string DefaultValue)
=> SettingService.GetSetting(settings, SettingName, DefaultValue);
private string GetProfileValue(string SettingName, string DefaultValue)
{
string value = SettingService.GetSetting(settings, SettingName, DefaultValue);
if (value.Contains("]"))
{
value = value.Substring(value.IndexOf("]") + 1);
}
return value;
}
private async Task SaveUser()
{

View File

@ -174,7 +174,7 @@ else
private string deletedby;
private DateTime? deletedon;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Edit;
protected override async Task OnParametersSetAsync()
{
@ -223,7 +223,14 @@ else
}
private string GetProfileValue(string SettingName, string DefaultValue)
=> SettingService.GetSetting(settings, SettingName, DefaultValue);
{
string value = SettingService.GetSetting(settings, SettingName, DefaultValue);
if (value.Contains("]"))
{
value = value.Substring(value.IndexOf("]") + 1);
}
return value;
}
private async Task SaveUser()
{

View File

@ -20,7 +20,7 @@ else
<div class="container">
<div class="row mb-1 align-items-center">
<div class="col-sm-4">
<ActionLink Action="Add" Text="Add User" ResourceKey="AddUser" />
<ActionLink Action="Add" Text="Add User" Security="SecurityAccessLevel.Edit" ResourceKey="AddUser" />
</div>
<div class="col-sm-4">
<input class="form-control" @bind="@_search" />
@ -41,21 +41,21 @@ else
</Header>
<Row>
<td>
<ActionLink Action="Edit" Parameters="@($"id=" + context.UserId.ToString())" ResourceKey="EditUser" />
<ActionLink Action="Edit" Parameters="@($"id=" + context.UserId.ToString())" Security="SecurityAccessLevel.Edit" ResourceKey="EditUser" />
</td>
<td>
<ActionDialog Header="Delete User" Message="@string.Format(Localizer["Confirm.User.Delete"], context.User.DisplayName)" Action="Delete" Security="SecurityAccessLevel.Admin" Class="btn btn-danger" OnClick="@(async () => await DeleteUser(context))" Disabled="@(context.UserId == PageState.User.UserId)" ResourceKey="DeleteUser" />
<ActionDialog Header="Delete User" Message="@string.Format(Localizer["Confirm.User.Delete"], context.User.DisplayName)" Action="Delete" Security="SecurityAccessLevel.Edit" Class="btn btn-danger" OnClick="@(async () => await DeleteUser(context))" Disabled="@(context.UserId == PageState.User.UserId)" ResourceKey="DeleteUser" />
</td>
<td>
<ActionLink Action="Roles" Parameters="@($"id=" + context.UserId.ToString())" ResourceKey="Roles" />
<ActionLink Action="Roles" Parameters="@($"id=" + context.UserId.ToString())" Security="SecurityAccessLevel.Edit" ResourceKey="Roles" />
</td>
<td>@context.User.Username</td>
<td>@((MarkupString)string.Format("<a href=\"mailto:{0}\">{1}</a>", @context.User.Email, @context.User.DisplayName))</td>
<td>@string.Format("{0:dd-MMM-yyyy HH:mm:ss}", context.User.LastLoginOn)</td>
<td>@((context.User.LastLoginOn != DateTime.MinValue) ? string.Format("{0:dd-MMM-yyyy HH:mm:ss}", context.User.LastLoginOn) : "")</td>
</Row>
</Pager>
</TabPanel>
<TabPanel Name="Settings" Heading="Settings" ResourceKey="Settings">
</TabPanel>
<TabPanel Name="Settings" Heading="Settings" ResourceKey="Settings" Security="SecurityAccessLevel.Admin">
<div class="container">
<Section Name="User" Heading="User Settings" ResourceKey="UserSettings">
<div class="row mb-1 align-items-center">
@ -406,7 +406,7 @@ else
private string _lifetime;
private string _token;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.View;
protected override async Task OnInitializedAsync()
{
@ -456,7 +456,8 @@ else
_togglesecret = SharedLocalizer["ShowPassword"];
_issuer = SettingService.GetSetting(settings, "JwtOptions:Issuer", PageState.Uri.Scheme + "://" + PageState.Alias.Name);
_audience = SettingService.GetSetting(settings, "JwtOptions:Audience", "");
_lifetime = SettingService.GetSetting(settings, "JwtOptions:Lifetime", "20"); }
_lifetime = SettingService.GetSetting(settings, "JwtOptions:Lifetime", "20");
}
}
private async Task LoadUsersAsync(bool load)
@ -522,7 +523,7 @@ else
private async Task UpdateUserSettingsAsync()
{
Dictionary<string, string> settings = await SettingService.GetUserSettingsAsync(PageState.User.UserId);
SettingService.SetSetting(settings, settingSearch, _search);
settings = SettingService.SetSetting(settings, settingSearch, _search);
await SettingService.UpdateUserSettingsAsync(settings, PageState.User.UserId);
}

View File

@ -0,0 +1,19 @@
using Oqtane.Documentation;
using Oqtane.Models;
using Oqtane.Shared;
namespace Oqtane.Modules.Admin.Users
{
[PrivateApi("Mark this as private, since it's not very useful in the public docs")]
public class ModuleInfo : IModule
{
public ModuleDefinition ModuleDefinition => new ModuleDefinition
{
Name = "Users",
Description = "Manage Users",
Categories = "Admin",
Version = Constants.Version,
PermissionNames = $"{PermissionNames.View},{PermissionNames.Edit},{EntityNames.User}:{PermissionNames.Write}:{RoleNames.Admin},{EntityNames.UserRole}:{PermissionNames.Write}:{RoleNames.Admin}"
};
}
}

View File

@ -63,7 +63,7 @@ else
<td>@context.EffectiveDate</td>
<td>@context.ExpiryDate</td>
<td>
<ActionDialog Header="Remove Role" Message="@string.Format(Localizer["Confirm.User.RemoveRole"], context.Role.Name)" Action="Delete" Security="SecurityAccessLevel.Admin" Class="btn btn-danger" OnClick="@(async () => await DeleteUserRole(context.UserRoleId))" Disabled="@(context.Role.IsAutoAssigned || (context.Role.Name == RoleNames.Host && userid == PageState.User.UserId))" ResourceKey="DeleteUserRole" />
<ActionDialog Header="Remove Role" Message="@string.Format(Localizer["Confirm.User.RemoveRole"], context.Role.Name)" Action="Delete" Security="SecurityAccessLevel.Edit" Class="btn btn-danger" OnClick="@(async () => await DeleteUserRole(context.UserRoleId))" Disabled="@(context.Role.IsAutoAssigned || (context.Role.Name == RoleNames.Host && userid == PageState.User.UserId))" ResourceKey="DeleteUserRole" />
</td>
</Row>
</Pager>
@ -79,7 +79,7 @@ else
private string expirydate = string.Empty;
private List<UserRole> userroles;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Edit;
protected override async Task OnInitializedAsync()
{

View File

@ -17,8 +17,8 @@
<th scope="col">@Localizer["Role"]</th>
@foreach (PermissionString permission in _permissions)
{
<th style="text-align: center; width: 1px;">@Localizer[permission.PermissionName]</th>
}
<th style="text-align: center; width: 1px;">@((MarkupString)GetPermissionName(permission).Replace(" ", "<br />"))</th>
}
</tr>
@foreach (Role role in _roles)
{
@ -28,7 +28,7 @@
{
var p = permission;
<td style="text-align: center;">
<TriStateCheckBox Value=@GetPermissionValue(p.Permissions, role.Name) Disabled=@GetPermissionDisabled(role.Name) OnChange="@(e => PermissionChanged(e, p.PermissionName, role.Name))" />
<TriStateCheckBox Value=@GetPermissionValue(p.Permissions, role.Name) Disabled="@GetPermissionDisabled(p.EntityName, p.PermissionName, role.Name)" OnChange="@(e => PermissionChanged(e, p.EntityName, p.PermissionName, role.Name))" />
</td>
}
</tr>
@ -66,7 +66,7 @@
{
var p = permission;
<td style="text-align: center; width: 1px;">
<TriStateCheckBox Value=@GetPermissionValue(p.Permissions, userid) Disabled=false OnChange="@(e => PermissionChanged(e, p.PermissionName, userid))" />
<TriStateCheckBox Value=@GetPermissionValue(p.Permissions, userid) Disabled="@GetPermissionDisabled(p.EntityName, p.PermissionName, "")" OnChange="@(e => PermissionChanged(e, p.EntityName, p.PermissionName, userid))" />
</td>
}
</tr>
@ -129,10 +129,25 @@
_permissions = new List<PermissionString>();
foreach (string permissionname in _permissionnames.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries))
foreach (string permissionname in _permissionnames.Split(',', StringSplitOptions.RemoveEmptyEntries))
{
// initialize with admin role
_permissions.Add(new PermissionString { PermissionName = permissionname, Permissions = RoleNames.Admin });
// permission names can be in the form of "EntityName:PermissionName:Roles"
if (permissionname.Contains(":"))
{
var segments = permissionname.Split(':');
if (segments.Length == 3)
{
if (!segments[2].Contains(RoleNames.Admin))
{
segments[2] = RoleNames.Admin + ";" + segments[2]; // ensure admin access
}
_permissions.Add(new PermissionString { EntityName = segments[0], PermissionName = segments[1], Permissions = segments[2] });
}
}
else
{
_permissions.Add(new PermissionString { EntityName = EntityName, PermissionName = permissionname, Permissions = RoleNames.Admin });
}
}
if (!string.IsNullOrEmpty(Permissions))
@ -140,14 +155,15 @@
// populate permissions
foreach (PermissionString permissionstring in UserSecurity.GetPermissionStrings(Permissions))
{
if (_permissions.Find(item => item.PermissionName == permissionstring.PermissionName) != null)
int index = _permissions.FindIndex(item => item.EntityName == permissionstring.EntityName && item.PermissionName == permissionstring.PermissionName);
if (index != -1)
{
_permissions[_permissions.FindIndex(item => item.PermissionName == permissionstring.PermissionName)].Permissions = permissionstring.Permissions;
_permissions[index].Permissions = permissionstring.Permissions;
}
if (permissionstring.Permissions.Contains("["))
{
foreach (string user in permissionstring.Permissions.Split(new char[] { '[' }, StringSplitOptions.RemoveEmptyEntries))
foreach (string user in permissionstring.Permissions.Split('[', StringSplitOptions.RemoveEmptyEntries))
{
if (user.Contains("]"))
{
@ -163,6 +179,16 @@
}
}
private string GetPermissionName(PermissionString permission)
{
var permissionname = Localizer[permission.PermissionName].ToString();
if (!string.IsNullOrEmpty(EntityName))
{
permissionname += " " + Localizer[permission.EntityName].ToString();
}
return permissionname;
}
private bool? GetPermissionValue(string permissions, string securityKey)
{
if ((";" + permissions + ";").Contains(";" + "!" + securityKey + ";"))
@ -182,8 +208,24 @@
}
}
private bool GetPermissionDisabled(string roleName)
=> (roleName == RoleNames.Admin && !UserSecurity.IsAuthorized(PageState.User, RoleNames.Host)) ? true : false;
private bool GetPermissionDisabled(string entityName, string permissionName, string roleName)
{
if (roleName == RoleNames.Admin && !UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{
return true;
}
else
{
if (entityName != EntityName && !UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin))
{
return true;
}
else
{
return false;
}
}
}
private async Task<Dictionary<string, string>> GetUsers(string filter)
{
@ -209,14 +251,15 @@
_user.Clear();
}
private void PermissionChanged(bool? value, string permissionName, string securityId)
private void PermissionChanged(bool? value, string entityName, string permissionName, string securityId)
{
var selected = value;
var permission = _permissions.Find(item => item.PermissionName == permissionName);
if (permission != null)
int index = _permissions.FindIndex(item => item.EntityName == entityName && item.PermissionName == permissionName);
if (index != -1)
{
var ids = permission.Permissions.Split(';').ToList();
var permission = _permissions[index];
var ids = permission.Permissions.Split(';').ToList();
ids.Remove(securityId); // remove grant permission
ids.Remove("!" + securityId); // remove deny permission
@ -232,7 +275,7 @@
break; // permission not specified
}
_permissions[_permissions.FindIndex(item => item.PermissionName == permissionName)].Permissions = string.Join(";", ids.ToArray());
_permissions[index].Permissions = string.Join(";", ids.ToArray());
}
}
@ -245,9 +288,9 @@
private void ValidatePermissions()
{
PermissionString permission;
for (int i = 0; i < _permissions.Count; i++)
for (int index = 0; index < _permissions.Count; index++)
{
permission = _permissions[i];
permission = _permissions[index];
List<string> ids = permission.Permissions.Split(';', StringSplitOptions.RemoveEmptyEntries).ToList();
ids.Remove("!" + RoleNames.Everyone); // remove deny all users
ids.Remove("!" + RoleNames.Unauthenticated); // remove deny unauthenticated
@ -263,7 +306,7 @@
}
}
permission.Permissions = string.Join(";", ids.ToArray());
_permissions[i] = permission;
_permissions[index] = permission;
}
}
}

View File

@ -315,15 +315,10 @@ namespace Oqtane.Modules
{
int pageId = ModuleState.PageId;
int moduleId = ModuleState.ModuleId;
int? userId = null;
if (PageState.User != null)
{
userId = PageState.User.UserId;
}
string category = GetType().AssemblyQualifiedName;
string feature = Utilities.GetTypeNameLastSegment(category, 1);
await LoggingService.Log(alias, pageId, moduleId, userId, category, feature, function, level, exception, message, args);
await LoggingService.Log(alias, pageId, moduleId, PageState.User?.UserId, category, feature, function, level, exception, message, args);
}
public class Logger

View File

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
@ -121,7 +121,7 @@
<value>User: </value>
</data>
<data name="User.Select" xml:space="preserve">
<value>Select User</value>
<value>Enter User's Name</value>
</data>
<data name="Users" xml:space="preserve">
<value>Users</value>
@ -129,9 +129,6 @@
<data name="Error.User.Load" xml:space="preserve">
<value>Error Loading Users</value>
</data>
<data name="Error.User.LoadRole" xml:space="preserve">
<value>Error Loading User Roles</value>
</data>
<data name="Success.User.AssignedRole" xml:space="preserve">
<value>User Assigned To Role</value>
</data>
@ -151,7 +148,7 @@
<value>The role you are assigning users to</value>
</data>
<data name="User.HelpText" xml:space="preserve">
<value>Select a user</value>
<value>Enter the name of a user</value>
</data>
<data name="EffectiveDate.HelpText" xml:space="preserve">
<value>The date that this role assignment is active</value>

View File

@ -1,32 +0,0 @@
using System.Net.Http;
using System.Threading.Tasks;
using System.Collections.Generic;
using Oqtane.Documentation;
using Oqtane.Shared;
using Oqtane.Models;
namespace Oqtane.Services
{
[PrivateApi("Don't show in the documentation, as everything should use the Interface")]
public class ApiService : ServiceBase, IApiService
{
public ApiService(HttpClient http, SiteState siteState) : base(http, siteState) { }
private string Apiurl => CreateApiUrl("Api");
public async Task<List<Api>> GetApisAsync(int siteId)
{
return await GetJsonAsync<List<Api>>($"{Apiurl}?siteid={siteId}");
}
public async Task<Api> GetApiAsync(int siteId, string entityName)
{
return await GetJsonAsync<Api>($"{Apiurl}/{siteId}/{entityName}");
}
public async Task UpdateApiAsync(Api api)
{
await PostJsonAsync(Apiurl, api);
}
}
}

View File

@ -1,31 +0,0 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Oqtane.Models;
namespace Oqtane.Services
{
/// <summary>
/// Service to retrieve and update API information.
/// </summary>
public interface IApiService
{
/// <summary>
/// returns a list of APIs
/// </summary>
/// <returns></returns>
Task<List<Api>> GetApisAsync(int siteId);
/// <summary>
/// returns a specific API
/// </summary>
/// <returns></returns>
Task<Api> GetApiAsync(int siteId, string entityName);
/// <summary>
/// Updates an API
/// </summary>
/// <param name="api"></param>
/// <returns></returns>
Task UpdateApiAsync(Api api);
}
}

View File

@ -33,7 +33,7 @@
}
}
@if (UserSecurity.IsAuthorized(PageState.User, PermissionNames.Edit, PageState.Page.Permissions))
@if (_canViewAdminDashboard || UserSecurity.IsAuthorized(PageState.User, PermissionNames.Edit, PageState.Page.Permissions))
{
<button type="button" class="btn @ButtonClass" data-bs-toggle="offcanvas" data-bs-target="#offcanvasControlPanel" aria-controls="offcanvasControlPanel">
<span class="oi oi-cog"></span>
@ -46,16 +46,17 @@
</div>
<div class="@BodyClass">
<div class="container-fluid">
@if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin))
@if (_canViewAdminDashboard)
{
<div class="row d-flex">
<div class="col">
<button type="button" data-bs-dismiss="offcanvas" class="btn btn-primary col-12" @onclick=@(async () => Navigate("Admin"))>@Localizer["AdminDash"]</button>
</div>
</div>
<hr class="app-rule" />
}
@if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin))
{
<div class="row">
<div class="col text-center">
<label class="control-label">@Localizer["Page.Manage"] </label>
@ -80,144 +81,149 @@
}
</div>
</div>
}
<hr class="app-rule" />
@if (_deleteConfirmation)
{
<div class="app-admin-modal">
<div class="modal" tabindex="-1" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">@Localizer["Page.Delete"]</h5>
<button type="button" class="btn-close" aria-label="Close" @onclick="ConfirmDelete"></button>
</div>
<div class="modal-body">
<p>Are You Sure You Want To Delete This Page?</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-danger" @onclick="DeletePage">@SharedLocalizer["Delete"]</button>
<button type="button" class="btn btn-secondary" @onclick="ConfirmDelete">@SharedLocalizer["Cancel"]</button>
@if (_deleteConfirmation)
{
<div class="app-admin-modal">
<div class="modal" tabindex="-1" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">@Localizer["Page.Delete"]</h5>
<button type="button" class="btn-close" aria-label="Close" @onclick="ConfirmDelete"></button>
</div>
<div class="modal-body">
<p>Are You Sure You Want To Delete This Page?</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-danger" @onclick="DeletePage">@SharedLocalizer["Delete"]</button>
<button type="button" class="btn btn-secondary" @onclick="ConfirmDelete">@SharedLocalizer["Cancel"]</button>
</div>
</div>
</div>
</div>
</div>
</div>
}
}
<hr class="app-rule" />
<div class="row">
<div class="col text-center">
<label for="Module" class="control-label">@Localizer["Module.Manage"] </label>
<select class="form-select" @bind="@ModuleType">
<option value="new">@Localizer["Module.AddNew"]</option>
<option value="existing">@Localizer["Module.AddExisting"]</option>
</select>
@if (ModuleType == "new")
{
@if (_moduleDefinitions != null)
{
<select class="form-select" @onchange="(e => CategoryChanged(e))">
@foreach (var category in _categories)
{
if (category == Category)
{
<option value="@category" selected>@category @Localizer["Modules"]</option>
}
else
{
<option value="@category">@category @Localizer["Modules"]</option>
}
}
</select>
<select class="form-select" @onchange="(e => ModuleChanged(e))">
@if (ModuleDefinitionName == "-")
{
<option value="-" selected>&lt;@Localizer["Module.Select"]&gt;</option>
}
else
{
<option value="-">&lt;@Localizer["Module.Select"]&gt;</option>
}
@foreach (var moduledefinition in _moduleDefinitions)
{
if (UserSecurity.IsAuthorized(PageState.User, PermissionNames.Utilize, moduledefinition.Permissions))
{
if (moduledefinition.Runtimes == "" || moduledefinition.Runtimes.Contains(PageState.Runtime.ToString()))
{
<option value="@moduledefinition.ModuleDefinitionName">@moduledefinition.Name</option>
}
}
}
</select>
}
}
else
{
<select class="form-select" @onchange="(e => PageChanged(e))">
<option value="-">&lt;@Localizer["Page.Select"]&gt;</option>
@foreach (Page p in _pages)
{
<option value="@p.PageId">@p.Name</option>
}
</select>
<select class="form-select" @bind="@ModuleId">
<option value="-">&lt;@Localizer["Module.Select"]&gt;</option>
@foreach (Module module in _modules)
{
<option value="@module.ModuleId">@module.Title</option>
}
</select>
}
</div>
</div>
<div class="row">
<div class="col text-center">
<label for="Title" class="control-label">@Localizer["Title"] </label>
<input type="text" name="Title" class="form-control" @bind="@Title" />
</div>
</div>
@if (_pane.Length > 1)
@if (UserSecurity.IsAuthorized(PageState.User, PermissionNames.Edit, PageState.Page.Permissions))
{
<div class="row">
<div class="col text-center">
<label for="Pane" class="control-label">@Localizer["Pane"] </label>
<select class="form-select" @bind="@Pane">
@foreach (string pane in PageState.Page.Panes)
<label for="Module" class="control-label">@Localizer["Module.Manage"] </label>
<select class="form-select" @bind="@ModuleType">
<option value="new">@Localizer["Module.AddNew"]</option>
<option value="existing">@Localizer["Module.AddExisting"]</option>
</select>
@if (ModuleType == "new")
{
@if (_moduleDefinitions != null)
{
<option value="@pane">@pane Pane</option>
<select class="form-select" @onchange="(e => CategoryChanged(e))">
@foreach (var category in _categories)
{
if (category == Category)
{
<option value="@category" selected>@category @Localizer["Modules"]</option>
}
else
{
<option value="@category">@category @Localizer["Modules"]</option>
}
}
</select>
<select class="form-select" @onchange="(e => ModuleChanged(e))">
@if (ModuleDefinitionName == "-")
{
<option value="-" selected>&lt;@Localizer["Module.Select"]&gt;</option>
}
else
{
<option value="-">&lt;@Localizer["Module.Select"]&gt;</option>
}
@foreach (var moduledefinition in _moduleDefinitions)
{
if (UserSecurity.IsAuthorized(PageState.User, PermissionNames.Utilize, moduledefinition.Permissions))
{
if (moduledefinition.Runtimes == "" || moduledefinition.Runtimes.Contains(PageState.Runtime.ToString()))
{
<option value="@moduledefinition.ModuleDefinitionName">@moduledefinition.Name</option>
}
}
}
</select>
}
}
else
{
<select class="form-select" @onchange="(e => PageChanged(e))">
<option value="-">&lt;@Localizer["Page.Select"]&gt;</option>
@foreach (Page p in _pages)
{
<option value="@p.PageId">@p.Name</option>
}
</select>
<select class="form-select" @bind="@ModuleId">
<option value="-">&lt;@Localizer["Module.Select"]&gt;</option>
@foreach (Module module in _modules)
{
<option value="@module.ModuleId">@module.Title</option>
}
</select>
}
</div>
</div>
<div class="row">
<div class="col text-center">
<label for="Title" class="control-label">@Localizer["Title"] </label>
<input type="text" name="Title" class="form-control" @bind="@Title" />
</div>
</div>
@if (_pane.Length > 1)
{
<div class="row">
<div class="col text-center">
<label for="Pane" class="control-label">@Localizer["Pane"] </label>
<select class="form-select" @bind="@Pane">
@foreach (string pane in PageState.Page.Panes)
{
<option value="@pane">@pane Pane</option>
}
</select>
</div>
</div>
}
<div class="row">
<div class="col text-center">
<label for="Container" class="control-label">@Localizer["Container"] </label>
<select class="form-select" @bind="@ContainerType">
@foreach (var container in _containers)
{
<option value="@container.TypeName">@container.Name</option>
}
</select>
</div>
</div>
<div class="row">
<div class="col text-center">
<label for="visibility" class="control-label">@Localizer["Visibility"]</label>
<select class="form-select" @bind="@Visibility">
<option value="view">@Localizer["VisibilityView"]</option>
<option value="edit">@Localizer["VisibilityEdit"]</option>
</select>
</div>
</div>
<button type="button" class="btn btn-primary col-12 mt-4" @onclick="@AddModule">@Localizer["Page.Module.Add"]</button>
@((MarkupString)Message)
}
<div class="row">
<div class="col text-center">
<label for="Container" class="control-label">@Localizer["Container"] </label>
<select class="form-select" @bind="@ContainerType">
@foreach (var container in _containers)
{
<option value="@container.TypeName">@container.Name</option>
}
</select>
</div>
</div>
<div class="row">
<div class="col text-center">
<label for="visibility" class="control-label">@Localizer["Visibility"]</label>
<select class="form-select" @bind="@Visibility">
<option value="view">@Localizer["VisibilityView"]</option>
<option value="edit">@Localizer["VisibilityEdit"]</option>
</select>
</div>
</div>
<button type="button" class="btn btn-primary col-12 mt-4" @onclick="@AddModule">@Localizer["Page.Module.Add"]</button>
@((MarkupString) Message)
</div>
</div>
</div>
}
@code{
private bool _canViewAdminDashboard = false;
private bool _showEditMode = false;
private bool _deleteConfirmation = false;
private List<string> _categories = new List<string>();
@ -286,6 +292,7 @@
protected override async Task OnParametersSetAsync()
{
_canViewAdminDashboard = CanViewAdminDashboard();
_showEditMode = false;
if (UserSecurity.IsAuthorized(PageState.User, PermissionNames.Edit, PageState.Page.Permissions))
{
@ -321,6 +328,22 @@
}
}
private bool CanViewAdminDashboard()
{
var admin = PageState.Pages.FirstOrDefault(item => item.Path == "admin");
if (admin != null)
{
foreach (var page in PageState.Pages.Where(item => item.ParentId == admin?.PageId))
{
if (UserSecurity.IsAuthorized(PageState.User, PermissionNames.View, page.Permissions))
{
return true;
}
}
}
return false;
}
private void CategoryChanged(ChangeEventArgs e)
{
Category = (string)e.Value;
@ -465,12 +488,10 @@
case "Admin":
// get admin dashboard moduleid
module = PageState.Modules.FirstOrDefault(item => item.ModuleDefinitionName == Constants.AdminDashboardModule);
if (module != null)
{
NavigationManager.NavigateTo(EditUrl(PageState.Page.Path, module.ModuleId, "Index", ""));
}
break;
case "Add":
case "Edit":
@ -551,19 +572,19 @@
{
page.IsDeleted = true;
await PageService.UpdatePageAsync(page);
await logger.Log(page.PageId, null, PageState.User.UserId, GetType().AssemblyQualifiedName, "ControlPanel", LogFunction.Delete, LogLevel.Information, null, "Page Deleted {Page}", page);
await logger.Log(page.PageId, null, PageState.User?.UserId, GetType().AssemblyQualifiedName, "ControlPanel", LogFunction.Delete, LogLevel.Information, null, "Page Deleted {Page}", page);
NavigationManager.NavigateTo(NavigateUrl(""));
}
else // personalized page
{
await PageService.DeletePageAsync(page.PageId);
await logger.Log(page.PageId, null, PageState.User.UserId, GetType().AssemblyQualifiedName, "ControlPanel", LogFunction.Delete, LogLevel.Information, null, "Page Deleted {Page}", page);
await logger.Log(page.PageId, null, PageState.User?.UserId, GetType().AssemblyQualifiedName, "ControlPanel", LogFunction.Delete, LogLevel.Information, null, "Page Deleted {Page}", page);
NavigationManager.NavigateTo(NavigateUrl());
}
}
catch (Exception ex)
{
await logger.Log(page.PageId, null, PageState.User.UserId, GetType().AssemblyQualifiedName, "ControlPanel", LogFunction.Delete, LogLevel.Information, ex, "Page Deleted {Page} {Error}", page, ex.Message);
await logger.Log(page.PageId, null, PageState.User?.UserId, GetType().AssemblyQualifiedName, "ControlPanel", LogFunction.Delete, LogLevel.Information, ex, "Page Deleted {Page} {Error}", page, ex.Message);
}
}
@ -596,8 +617,8 @@
private async Task UpdateSettingsAsync()
{
Dictionary<string, string> settings = await SettingService.GetUserSettingsAsync(PageState.User.UserId);
SettingService.SetSetting(settings, settingCategory, _category);
SettingService.SetSetting(settings, settingPane, _pane);
settings = SettingService.SetSetting(settings, settingCategory, _category);
settings = SettingService.SetSetting(settings, settingPane, _pane);
await SettingService.UpdateUserSettingsAsync(settings, PageState.User.UserId);
}

View File

@ -32,7 +32,7 @@ namespace Oqtane.Themes.Controls
protected async Task LogoutUser()
{
await LoggingService.Log(PageState.Alias, PageState.Page.PageId, null, PageState.User.UserId, GetType().AssemblyQualifiedName, "Logout", LogFunction.Security, LogLevel.Information, null, "User Logout For Username {Username}", PageState.User.Username);
await LoggingService.Log(PageState.Alias, PageState.Page.PageId, null, PageState.User?.UserId, GetType().AssemblyQualifiedName, "Logout", LogFunction.Security, LogLevel.Information, null, "User Logout For Username {Username}", PageState.User?.Username);
// check if anonymous user can access page
var url = PageState.Alias.Path + "/" + PageState.Page.Path;

View File

@ -87,10 +87,9 @@ else
// retrieve friendly localized error
_error = Localizer["Error.Module.Exception"];
// log error
int? userId = (PageState.User != null) ? PageState.User.UserId : null;
string category = GetType().AssemblyQualifiedName;
string feature = Utilities.GetTypeNameLastSegment(category, 1);
await LoggingService.Log(null, ModuleState.PageId, ModuleState.ModuleId, userId, category, feature, LogFunction.Other, LogLevel.Error, exception, "An Unexpected Error Has Occurred In {ModuleDefinitionName}: {Error}", ModuleState.ModuleDefinitionName, exception.Message);
await LoggingService.Log(null, ModuleState.PageId, ModuleState.ModuleId, PageState.User?.UserId, category, feature, LogFunction.Other, LogLevel.Error, exception, "An Unexpected Error Has Occurred In {ModuleDefinitionName}: {Error}", ModuleState.ModuleDefinitionName, exception.Message);
await base.OnErrorAsync(exception);
return;
}

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;
@ -149,7 +151,7 @@ namespace Oqtane.Controllers
bool verified;
bool allowregistration;
if (User.IsInRole(RoleNames.Admin))
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);

View File

@ -1,4 +1,4 @@
using System.Collections.Generic;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.Json;
@ -11,20 +11,22 @@ namespace Oqtane.Extensions
public static string EncodePermissions(this IEnumerable<Permission> permissionList)
{
List<PermissionString> permissionstrings = new List<PermissionString>();
string entityname = "";
string permissionname = "";
string permissions = "";
StringBuilder permissionsbuilder = new StringBuilder();
string securityid = "";
foreach (Permission permission in permissionList.OrderBy(item => item.PermissionName))
foreach (Permission permission in permissionList.OrderBy(item => item.EntityName).ThenBy(item => item.PermissionName))
{
// permission collections are grouped by permissionname
if (permissionname != permission.PermissionName)
// permission collections are grouped by entityname and permissionname
if (entityname != permission.EntityName || permissionname != permission.PermissionName)
{
permissions = permissionsbuilder.ToString();
if (permissions != "")
{
permissionstrings.Add(new PermissionString { PermissionName = permissionname, Permissions = permissions.Substring(0, permissions.Length - 1) });
permissionstrings.Add(new PermissionString { EntityName = entityname, PermissionName = permissionname, Permissions = permissions.Substring(0, permissions.Length - 1) });
}
entityname = permission.EntityName;
permissionname = permission.PermissionName;
permissionsbuilder = new StringBuilder();
}
@ -56,7 +58,7 @@ namespace Oqtane.Extensions
permissions = permissionsbuilder.ToString();
if (permissions != "")
{
permissionstrings.Add(new PermissionString { PermissionName = permissionname, Permissions = permissions.Substring(0, permissions.Length - 1) });
permissionstrings.Add(new PermissionString { EntityName = entityname, PermissionName = permissionname, Permissions = permissions.Substring(0, permissions.Length - 1) });
}
return JsonSerializer.Serialize(permissionstrings);
}

View File

@ -308,43 +308,45 @@ namespace Oqtane.Infrastructure
private void Upgrade_3_3_0(Tenant tenant, IServiceScope scope)
{
var pageTemplates = new List<PageTemplate>();
pageTemplates.Add(new PageTemplate
try
{
Name = "API Management",
Parent = "Admin",
Order = 35,
Path = "admin/apis",
Icon = Icons.CloudDownload,
IsNavigation = true,
IsPersonalizable = false,
PagePermissions = new List<Permission>
var roles = scope.ServiceProvider.GetRequiredService<IRoleRepository>();
var pages = scope.ServiceProvider.GetRequiredService<IPageRepository>();
var modules = scope.ServiceProvider.GetRequiredService<IModuleRepository>();
var permissions = scope.ServiceProvider.GetRequiredService<IPermissionRepository>();
var siteRepository = scope.ServiceProvider.GetRequiredService<ISiteRepository>();
foreach (Site site in siteRepository.GetSites().ToList())
{
new Permission(PermissionNames.View, RoleNames.Admin, true),
new Permission(PermissionNames.Edit, RoleNames.Admin, true)
}.EncodePermissions(),
PageTemplateModules = new List<PageTemplateModule>
{
new PageTemplateModule
int roleid = roles.GetRoles(site.SiteId).FirstOrDefault(item => item.Name == RoleNames.Registered).RoleId;
int pageid = pages.GetPages(site.SiteId).FirstOrDefault(item => item.Path == "admin").PageId;
var permission = new Permission
{
ModuleDefinitionName = typeof(Oqtane.Modules.Admin.Visitors.Index).ToModuleDefinitionName(), Title = "Visitor Management", Pane = PaneNames.Default,
ModulePermissions = new List<Permission>
{
new Permission(PermissionNames.View, RoleNames.Admin, true),
new Permission(PermissionNames.Edit, RoleNames.Admin, true)
}.EncodePermissions(),
Content = ""
}
SiteId = site.SiteId,
EntityName = EntityNames.Page,
EntityId = pageid,
PermissionName = PermissionNames.View,
RoleId = roleid,
IsAuthorized = true
};
permissions.AddPermission(permission);
int moduleid = modules.GetModules(site.SiteId).FirstOrDefault(item => item.ModuleDefinitionName == "Oqtane.Modules.Admin.Dashboard, Oqtane.Client").ModuleId;
permission = new Permission
{
SiteId = site.SiteId,
EntityName = EntityNames.Module,
EntityId = moduleid,
PermissionName = PermissionNames.View,
RoleId = roleid,
IsAuthorized = true
};
permissions.AddPermission(permission);
}
});
var pages = scope.ServiceProvider.GetRequiredService<IPageRepository>();
var sites = scope.ServiceProvider.GetRequiredService<ISiteRepository>();
foreach (Site site in sites.GetSites().ToList())
}
catch (Exception ex)
{
sites.CreatePages(site, pageTemplates);
Debug.WriteLine($"Oqtane Error: Error In 3.3.0 Upgrade Logic - {ex}");
}
}
}

View File

@ -6,7 +6,6 @@ namespace Oqtane.Repository
public interface IPageModuleRepository
{
IEnumerable<PageModule> GetPageModules(int siteId);
IEnumerable<PageModule> GetPageModules(int pageId, string pane);
PageModule AddPageModule(PageModule pageModule);
PageModule UpdatePageModule(PageModule pageModule);
PageModule GetPageModule(int pageModuleId);

View File

@ -10,46 +10,28 @@ namespace Oqtane.Repository
public class PageModuleRepository : IPageModuleRepository
{
private TenantDBContext _db;
private readonly IModuleDefinitionRepository _moduleDefinitions;
private readonly IPermissionRepository _permissions;
public PageModuleRepository(TenantDBContext context, IPermissionRepository permissions)
public PageModuleRepository(TenantDBContext context, IModuleDefinitionRepository moduleDefinitions, IPermissionRepository permissions)
{
_db = context;
_moduleDefinitions = moduleDefinitions;
_permissions = permissions;
}
public IEnumerable<PageModule> GetPageModules(int siteId)
{
IEnumerable<PageModule> pagemodules = _db.PageModule
var pagemodules = _db.PageModule
.Include(item => item.Module) // eager load modules
.Where(item => item.Module.SiteId == siteId);
.Where(item => item.Module.SiteId == siteId).ToList();
if (pagemodules.Any())
{
IEnumerable<Permission> permissions = _permissions.GetPermissions(siteId, EntityNames.Module).ToList();
foreach (PageModule pagemodule in pagemodules)
var moduledefinitions = _moduleDefinitions.GetModuleDefinitions(siteId).ToList();
var permissions = _permissions.GetPermissions(siteId, EntityNames.Module).ToList();
for (int index = 0; index < pagemodules.Count; index++)
{
pagemodule.Module.Permissions = permissions.Where(item => item.EntityId == pagemodule.ModuleId).EncodePermissions();
}
}
return pagemodules;
}
public IEnumerable<PageModule> GetPageModules(int pageId, string pane)
{
IEnumerable<PageModule> pagemodules = _db.PageModule
.Include(item => item.Module) // eager load modules
.Where(item => item.PageId == pageId);
if (pane != "" && pagemodules.Any())
{
pagemodules = pagemodules.Where(item => item.Pane == pane);
}
if (pagemodules.Any())
{
var siteId = pagemodules.FirstOrDefault().Module.SiteId;
IEnumerable<Permission> permissions = _permissions.GetPermissions(siteId, EntityNames.Module).ToList();
foreach (PageModule pagemodule in pagemodules)
{
pagemodule.Module.Permissions = permissions.Where(item => item.EntityId == pagemodule.ModuleId).EncodePermissions();
pagemodules[index] = GetPageModule(pagemodules[index], moduledefinitions, permissions);
}
}
return pagemodules;
@ -89,7 +71,9 @@ namespace Oqtane.Repository
}
if (pagemodule != null)
{
pagemodule.Module.Permissions = _permissions.GetPermissions(pagemodule.Module.SiteId, EntityNames.Module, pagemodule.ModuleId)?.EncodePermissions();
var moduledefinitions = _moduleDefinitions.GetModuleDefinitions(pagemodule.Module.SiteId).ToList();
var permissions = _permissions.GetPermissions(pagemodule.Module.SiteId, EntityNames.Module).ToList();
pagemodule = GetPageModule(pagemodule, moduledefinitions, permissions);
}
return pagemodule;
}
@ -100,7 +84,9 @@ namespace Oqtane.Repository
.SingleOrDefault(item => item.PageId == pageId && item.ModuleId == moduleId);
if (pagemodule != null)
{
pagemodule.Module.Permissions = _permissions.GetPermissions(pagemodule.Module.SiteId, EntityNames.Module, pagemodule.ModuleId)?.EncodePermissions();
var moduledefinitions = _moduleDefinitions.GetModuleDefinitions(pagemodule.Module.SiteId).ToList();
var permissions = _permissions.GetPermissions(pagemodule.Module.SiteId, EntityNames.Module).ToList();
pagemodule = GetPageModule(pagemodule, moduledefinitions, permissions);
}
return pagemodule;
}
@ -111,5 +97,30 @@ namespace Oqtane.Repository
_db.PageModule.Remove(pageModule);
_db.SaveChanges();
}
private PageModule GetPageModule(PageModule pageModule, List<ModuleDefinition> moduleDefinitions, List<Permission> modulePermissions)
{
var permissions = modulePermissions.Where(item => item.EntityId == pageModule.ModuleId).ToList();
// moduledefinition permissionnames can specify permissions for other entities (ie. API permissions)
pageModule.Module.ModuleDefinition = moduleDefinitions.Find(item => item.ModuleDefinitionName == pageModule.Module.ModuleDefinitionName);
if (!string.IsNullOrEmpty(pageModule.Module.ModuleDefinition.PermissionNames) && pageModule.Module.ModuleDefinition.PermissionNames.Contains(":"))
{
foreach (var permissionname in pageModule.Module.ModuleDefinition.PermissionNames.Split(",", System.StringSplitOptions.RemoveEmptyEntries))
{
if (permissionname.Contains(":"))
{
// moduledefinition permissionnames can be in the form of "EntityName:PermissionName:Roles"
var segments = permissionname.Split(':');
if (segments.Length == 3 && segments[0] != EntityNames.Module)
{
permissions.AddRange(_permissions.GetPermissions(pageModule.Module.SiteId, segments[0], segments[1]).Where(item => item.EntityId == -1));
}
}
}
}
pageModule.Module.Permissions = permissions?.EncodePermissions();
return pageModule;
}
}
}

View File

@ -79,23 +79,52 @@ namespace Oqtane.Repository
public void UpdatePermissions(int siteId, string entityName, int entityId, string permissionStrings)
{
// get current permissions and delete
IEnumerable<Permission> permissions = _db.Permission
.Where(item => item.EntityName == entityName)
.Where(item => item.EntityId == entityId)
.Where(item => item.SiteId == siteId);
foreach (Permission permission in permissions)
bool modified = false;
var existing = new List<Permission>();
var permissions = DecodePermissions(permissionStrings, siteId, entityName, entityId);
foreach (var permission in permissions)
{
_db.Permission.Remove(permission);
if (!existing.Any(item => item.EntityName == permission.EntityName && item.PermissionName == permission.PermissionName))
{
existing.AddRange(GetPermissions(siteId, permission.EntityName, permission.PermissionName)
.Where(item => item.EntityId == entityId || item.EntityId == -1));
}
var current = existing.FirstOrDefault(item => item.EntityName == permission.EntityName && item.EntityId == permission.EntityId
&& item.PermissionName == permission.PermissionName && item.RoleId == permission.RoleId && item.UserId == permission.UserId);
if (current != null)
{
if (current.IsAuthorized != permission.IsAuthorized)
{
current.IsAuthorized = permission.IsAuthorized;
_db.Entry(current).State = EntityState.Modified;
modified = true;
}
}
else
{
_db.Permission.Add(permission);
modified = true;
}
}
// add permissions
permissions = DecodePermissions(permissionStrings, siteId, entityName, entityId);
foreach (Permission permission in permissions)
foreach (var permission in existing)
{
_db.Permission.Add(permission);
if (!permissions.Any(item => item.EntityName == permission.EntityName && item.PermissionName == permission.PermissionName
&& item.EntityId == permission.EntityId && item.RoleId == permission.RoleId && item.UserId == permission.UserId))
{
permission.Role = null; // remove linked reference to Role which can cause errors in EF Core change tracking
_db.Permission.Remove(permission);
modified = true;
}
}
if (modified)
{
_db.SaveChanges();
foreach (var entityname in permissions.Select(item => item.EntityName).Distinct())
{
ClearCache(siteId, entityname);
}
}
_db.SaveChanges();
ClearCache(siteId, entityName);
}
public Permission GetPermission(int permissionId)
@ -200,8 +229,22 @@ namespace Oqtane.Repository
securityid = id;
Permission permission = new Permission();
permission.SiteId = siteId;
permission.EntityName = entityName;
permission.EntityId = entityId;
if (!string.IsNullOrEmpty(permissionstring.EntityName))
{
permission.EntityName = permissionstring.EntityName;
}
else
{
permission.EntityName = entityName;
}
if (permission.EntityName == entityName)
{
permission.EntityId = entityId;
}
else
{
permission.EntityId = -1;
}
permission.PermissionName = permissionstring.PermissionName;
permission.RoleId = null;
permission.UserId = null;

View File

@ -431,6 +431,7 @@ namespace Oqtane.Repository
PagePermissions = new List<Permission>
{
new Permission(PermissionNames.View, RoleNames.Admin, true),
new Permission(PermissionNames.View, RoleNames.Registered, true),
new Permission(PermissionNames.Edit, RoleNames.Admin, true)
}.EncodePermissions(),
PageTemplateModules = new List<PageTemplateModule>
@ -441,6 +442,7 @@ namespace Oqtane.Repository
ModulePermissions = new List<Permission>
{
new Permission(PermissionNames.View, RoleNames.Admin, true),
new Permission(PermissionNames.View, RoleNames.Registered, true),
new Permission(PermissionNames.Edit, RoleNames.Admin, true)
}.EncodePermissions(),
Content = ""

View File

@ -22,22 +22,15 @@ namespace Oqtane.Security
if (policy == null)
{
// policy names must be in the form of "EntityName:PermissionName:Roles" ie. "Module:Edit:Administrators" (roles are comma delimited)
// policy names must be in the form of "EntityName:PermissionName:Roles"
if (policyName.Contains(':'))
{
var policySegments = policyName.Split(':');
if (policySegments.Length >= 3)
var segments = policyName.Split(':');
if (segments.Length == 3)
{
// check for optional RequireEntityId segment
var requireEntityId = false;
if (policySegments.Length == 4 && policySegments[3] == Constants.RequireEntityId)
{
requireEntityId = true;
}
// create policy
var builder = new AuthorizationPolicyBuilder();
builder.AddRequirements(new PermissionRequirement(policySegments[0], policySegments[1], policySegments[2], requireEntityId));
builder.AddRequirements(new PermissionRequirement(segments[0], segments[1], segments[2]));
policy = builder.Build();
// add policy to the AuthorizationOptions
@ -59,8 +52,8 @@ namespace Oqtane.Security
private string GetPolicyName(string policyName)
{
// backward compatibility for legacy static policy names
if (policyName == PolicyNames.ViewModule) policyName = $"{EntityNames.Module}:{PermissionNames.View}:{RoleNames.Admin}:{Constants.RequireEntityId}";
if (policyName == PolicyNames.EditModule) policyName = $"{EntityNames.Module}:{PermissionNames.Edit}:{RoleNames.Admin}:{Constants.RequireEntityId}";
if (policyName == PolicyNames.ViewModule) policyName = $"{EntityNames.Module}:{PermissionNames.View}:{RoleNames.Admin}";
if (policyName == PolicyNames.EditModule) policyName = $"{EntityNames.Module}:{PermissionNames.Edit}:{RoleNames.Admin}";
return policyName;
}
}

View File

@ -34,28 +34,26 @@ namespace Oqtane.Security
}
int entityId = -1;
if (requirement.RequireEntityId)
// get entityid from querystring based on a parameter format of auth{entityname}id (ie. authmoduleid )
if (ctx.Request.Query.ContainsKey("auth" + requirement.EntityName.ToLower() + "id"))
{
// get entityid from querystring based on a parameter format of auth{entityname}id (ie. authmoduleid )
if (ctx.Request.Query.ContainsKey("auth" + requirement.EntityName.ToLower() + "id"))
if (!int.TryParse(ctx.Request.Query["auth" + requirement.EntityName.ToLower() + "id"], out entityId))
{
if (!int.TryParse(ctx.Request.Query["auth" + requirement.EntityName.ToLower() + "id"], out entityId))
entityId = -1;
}
}
// legacy support for deprecated CreateAuthorizationPolicyUrl(string url, int entityId)
if (entityId == -1)
{
if (ctx.Request.Query.ContainsKey("entityid"))
{
if (!int.TryParse(ctx.Request.Query["entityid"], out entityId))
{
entityId = -1;
}
}
// legacy support for deprecated CreateAuthorizationPolicyUrl(string url, int entityId)
if (entityId == -1)
{
if (ctx.Request.Query.ContainsKey("entityid"))
{
if (!int.TryParse(ctx.Request.Query["entityid"], out entityId))
{
entityId = -1;
}
}
}
}
// validate permissions

View File

@ -8,16 +8,13 @@ namespace Oqtane.Security
public string PermissionName { get; }
public string Roles { get; }
public string Roles { get; } // semi-colon delimited
public bool RequireEntityId { get; }
public PermissionRequirement(string entityName, string permissionName, string roles, bool requireEntityId)
public PermissionRequirement(string entityName, string permissionName, string roles)
{
EntityName = entityName;
PermissionName = permissionName;
Roles = roles;
RequireEntityId = requireEntityId;
}
}
}

View File

@ -40,7 +40,7 @@ namespace Oqtane.Security
}
else
{
return UserSecurity.IsAuthorized(GetUser(principal), roles.Replace(",",";"));
return UserSecurity.IsAuthorized(GetUser(principal), roles);
}
}

View File

@ -1,23 +0,0 @@
namespace Oqtane.Models
{
/// <summary>
/// API management
/// </summary>
public class Api
{
/// <summary>
/// Reference to a <see cref="Site"/>
/// </summary>
public int SiteId { get; set; }
/// <summary>
/// The Entity Name
/// </summary>
public string EntityName { get; set; }
/// <summary>
/// The permissions for the entity
/// </summary>
public string Permissions { get; set; }
}
}

View File

@ -5,13 +5,18 @@ namespace Oqtane.Models
/// </summary>
public class PermissionString
{
/// <summary>
/// A term describing the entity
/// </summary>
public string EntityName { get; set; }
/// <summary>
/// A term describing a set of permissions
/// </summary>
public string PermissionName { get; set; }
/// <summary>
/// The permissions addressed with this name
/// The permissions
/// </summary>
public string Permissions { get; set; }
}

View File

@ -4,8 +4,8 @@ namespace Oqtane.Shared
{
public class Constants
{
public static readonly string Version = "3.2.1";
public const string ReleaseVersions = "1.0.0,1.0.1,1.0.2,1.0.3,1.0.4,2.0.0,2.0.1,2.0.2,2.1.0,2.2.0,2.3.0,2.3.1,3.0.0,3.0.1,3.0.2,3.0.3,3.1.0,3.1.1,3.1.2,3.1.3,3.1.4,3.2.0,3.2.1";
public static readonly string Version = "3.3.0";
public const string ReleaseVersions = "1.0.0,1.0.1,1.0.2,1.0.3,1.0.4,2.0.0,2.0.1,2.0.2,2.1.0,2.2.0,2.3.0,2.3.1,3.0.0,3.0.1,3.0.2,3.0.3,3.1.0,3.1.1,3.1.2,3.1.3,3.1.4,3.2.0,3.3.0";
public const string PackageId = "Oqtane.Framework";
public const string ClientId = "Oqtane.Client";
public const string UpdaterPackageId = "Oqtane.Updater";
@ -74,8 +74,6 @@ namespace Oqtane.Shared
public static readonly string MauiUserAgent = "MAUI";
public static readonly string RequireEntityId = "RequireEntityId";
// Obsolete constants
const string RoleObsoleteMessage = "Use the corresponding member from Oqtane.Shared.RoleNames";