refactoring, enhancements, and some fixes

This commit is contained in:
Shaun Walker
2021-06-10 08:16:02 -04:00
parent 82c05a841f
commit bc720555c4
30 changed files with 436 additions and 244 deletions

View File

@ -3,10 +3,7 @@ using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Authorization;
using Oqtane.Models;
using Oqtane.Shared;
using System.Linq;
using System;
using System.Net;
using System.Globalization;
using Oqtane.Enums;
using Oqtane.Infrastructure;
using Oqtane.Repository;
@ -18,21 +15,17 @@ namespace Oqtane.Controllers
public class AliasController : Controller
{
private readonly IAliasRepository _aliases;
private readonly IHttpContextAccessor _accessor;
private readonly ISyncManager _syncManager;
private readonly ILogManager _logger;
public AliasController(IAliasRepository aliases, IHttpContextAccessor accessor, ISyncManager syncManager, ILogManager logger)
public AliasController(IAliasRepository aliases, ILogManager logger)
{
_aliases = aliases;
_accessor = accessor;
_syncManager = syncManager;
_logger = logger;
}
// GET: api/<controller>
[HttpGet]
[Authorize(Roles = RoleNames.Admin)]
[Authorize(Roles = RoleNames.Host)]
public IEnumerable<Alias> Get()
{
return _aliases.GetAliases();
@ -40,37 +33,15 @@ namespace Oqtane.Controllers
// GET api/<controller>/5
[HttpGet("{id}")]
[Authorize(Roles = RoleNames.Admin)]
[Authorize(Roles = RoleNames.Host)]
public Alias Get(int id)
{
return _aliases.GetAlias(id);
}
// GET api/<controller>/name/?path=xxx&sync=yyyyMMddHHmmssfff
[HttpGet("name")]
public Alias Get(string path, string sync)
{
Alias alias = null;
if (_accessor.HttpContext != null)
{
path = _accessor.HttpContext.Request.Host.Value + "/" + WebUtility.UrlDecode(path);
alias = _aliases.GetAlias(path);
}
// get sync events
if (alias != null)
{
alias.SyncDate = DateTime.UtcNow;
alias.SyncEvents = _syncManager.GetSyncEvents(alias.TenantId, DateTime.ParseExact(sync, "yyyyMMddHHmmssfff", CultureInfo.InvariantCulture));
}
return alias;
}
// POST api/<controller>
[HttpPost]
[Authorize(Roles = RoleNames.Admin)]
[Authorize(Roles = RoleNames.Host)]
public Alias Post([FromBody] Alias alias)
{
if (ModelState.IsValid)
@ -78,12 +49,18 @@ namespace Oqtane.Controllers
alias = _aliases.AddAlias(alias);
_logger.Log(LogLevel.Information, this, LogFunction.Create, "Alias Added {Alias}", alias);
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Alias Post Attempt {Alias}", alias);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
alias = null;
}
return alias;
}
// PUT api/<controller>/5
[HttpPut("{id}")]
[Authorize(Roles = RoleNames.Admin)]
[Authorize(Roles = RoleNames.Host)]
public Alias Put(int id, [FromBody] Alias alias)
{
if (ModelState.IsValid)
@ -91,12 +68,18 @@ namespace Oqtane.Controllers
alias = _aliases.UpdateAlias(alias);
_logger.Log(LogLevel.Information, this, LogFunction.Update, "Alias Updated {Alias}", alias);
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Alias Put Attempt {Alias}", alias);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
alias = null;
}
return alias;
}
// DELETE api/<controller>/5
[HttpDelete("{id}")]
[Authorize(Roles = RoleNames.Admin)]
[Authorize(Roles = RoleNames.Host)]
public void Delete(int id)
{
_aliases.DeleteAlias(id);

View File

@ -13,6 +13,8 @@ using Oqtane.Shared;
using Oqtane.Themes;
using Microsoft.Extensions.Caching.Memory;
using System.Net;
using Oqtane.Repository;
using Microsoft.AspNetCore.Http;
namespace Oqtane.Controllers
{
@ -24,14 +26,18 @@ namespace Oqtane.Controllers
private readonly IDatabaseManager _databaseManager;
private readonly ILocalizationManager _localizationManager;
private readonly IMemoryCache _cache;
private readonly IHttpContextAccessor _accessor;
private readonly IAliasRepository _aliases;
public InstallationController(IConfigurationRoot config, IInstallationManager installationManager, IDatabaseManager databaseManager, ILocalizationManager localizationManager, IMemoryCache cache)
public InstallationController(IConfigurationRoot config, IInstallationManager installationManager, IDatabaseManager databaseManager, ILocalizationManager localizationManager, IMemoryCache cache, IHttpContextAccessor accessor, IAliasRepository aliases)
{
_config = config;
_installationManager = installationManager;
_databaseManager = databaseManager;
_localizationManager = localizationManager;
_cache = cache;
_accessor = accessor;
_aliases = aliases;
}
// POST api/<controller>
@ -52,11 +58,17 @@ namespace Oqtane.Controllers
return installation;
}
// GET api/<controller>/installed
// GET api/<controller>/installed/?path=xxx
[HttpGet("installed")]
public Installation IsInstalled()
public Installation IsInstalled(string path)
{
return _databaseManager.IsInstalled();
var installation = _databaseManager.IsInstalled();
if (installation.Success)
{
path = _accessor.HttpContext.Request.Host.Value + "/" + WebUtility.UrlDecode(path);
installation.Alias = _aliases.GetAlias(path);
}
return installation;
}
[HttpGet("upgrade")]

View File

@ -3,7 +3,6 @@ using Microsoft.AspNetCore.Http;
using Oqtane.Infrastructure;
using System.Collections.Generic;
using System;
using Oqtane.Shared;
namespace Oqtane.Controllers
{
@ -18,20 +17,41 @@ namespace Oqtane.Controllers
{
_logger = logger;
// populate policy authorization dictionary
// populate policy authorization dictionary from querystring and headers
int value;
foreach (var param in accessor.HttpContext.Request.Query)
{
if (param.Key.StartsWith("auth") && param.Key.EndsWith("id") && int.TryParse(param.Value, out value))
{
_authEntityId.Add(param.Key.Substring(4, param.Key.Length - 6), value);
}
}
foreach (var param in accessor.HttpContext.Request.Headers)
{
if (param.Key.StartsWith("auth") && param.Key.EndsWith("id") && int.TryParse(param.Value, out value))
{
_authEntityId.Add(param.Key.Substring(4, param.Key.Length - 6), value);
}
}
// legacy support
if (_authEntityId.Count == 0 && accessor.HttpContext.Request.Query.ContainsKey("entityid"))
{
_entityId = int.Parse(accessor.HttpContext.Request.Query["entityid"]);
}
}
protected int AuthEntityId(string entityname)
{
if (_authEntityId.ContainsKey(entityname))
{
return _authEntityId[entityname];
}
else
{
return -1;
}
}
}

View File

@ -0,0 +1,34 @@
using Microsoft.AspNetCore.Mvc;
using Oqtane.Models;
using Oqtane.Shared;
using System;
using System.Globalization;
using Oqtane.Infrastructure;
namespace Oqtane.Controllers
{
[Route(ControllerRoutes.ApiRoute)]
public class SyncController : Controller
{
private readonly ISyncManager _syncManager;
private readonly Alias _alias;
public SyncController(ISyncManager syncManager, ITenantManager tenantManager)
{
_syncManager = syncManager;
_alias = tenantManager.GetAlias();
}
// GET api/<controller>/yyyyMMddHHmmssfff
[HttpGet("{lastSyncDate}")]
public Sync Get(string lastSyncDate)
{
Sync sync = new Sync
{
SyncDate = DateTime.UtcNow,
SyncEvents = _syncManager.GetSyncEvents(_alias.TenantId, DateTime.ParseExact(lastSyncDate, "yyyyMMddHHmmssfff", CultureInfo.InvariantCulture))
};
return sync;
}
}
}

View File

@ -3,11 +3,10 @@ using Microsoft.AspNetCore.Authorization;
using Oqtane.Modules.HtmlText.Repository;
using Microsoft.AspNetCore.Http;
using Oqtane.Shared;
using System;
using System.Collections.Generic;
using Oqtane.Enums;
using Oqtane.Infrastructure;
using Oqtane.Controllers;
using System.Net;
namespace Oqtane.Modules.HtmlText.Controllers
{
@ -24,85 +23,75 @@ namespace Oqtane.Modules.HtmlText.Controllers
// GET api/<controller>/5
[HttpGet("{id}")]
[Authorize(Policy = PolicyNames.ViewModule)]
public List<Models.HtmlText> Get(int id)
public Models.HtmlText Get(int id)
{
var list = new List<Models.HtmlText>();
try
if (AuthEntityId(EntityNames.Module) == id)
{
Models.HtmlText htmlText = null;
if (_authEntityId[EntityNames.Module] == id)
{
htmlText = _htmlText.GetHtmlText(id);
list.Add(htmlText);
}
return _htmlText.GetHtmlText(id);
}
catch (Exception ex)
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Read, ex, "Get Error {Error}", ex.Message);
throw;
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized HtmlText Get Attempt {ModuleId}", id);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
return null;
}
return list;
}
// POST api/<controller>
[ValidateAntiForgeryToken]
[HttpPost]
[Authorize(Policy = PolicyNames.EditModule)]
public Models.HtmlText Post([FromBody] Models.HtmlText htmlText)
{
try
if (ModelState.IsValid && AuthEntityId(EntityNames.Module) == htmlText.ModuleId)
{
if (ModelState.IsValid && htmlText.ModuleId == _authEntityId[EntityNames.Module])
{
htmlText = _htmlText.AddHtmlText(htmlText);
_logger.Log(LogLevel.Information, this, LogFunction.Create, "Html/Text Added {HtmlText}", htmlText);
}
htmlText = _htmlText.AddHtmlText(htmlText);
_logger.Log(LogLevel.Information, this, LogFunction.Create, "Html/Text Added {HtmlText}", htmlText);
return htmlText;
}
catch (Exception ex)
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Create, ex, "Post Error {Error}", ex.Message);
throw;
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized HtmlText Post Attempt {HtmlText}", htmlText);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
return null;
}
}
// PUT api/<controller>/5
[ValidateAntiForgeryToken]
[HttpPut("{id}")]
[Authorize(Policy = PolicyNames.EditModule)]
public Models.HtmlText Put(int id, [FromBody] Models.HtmlText htmlText)
{
try
if (ModelState.IsValid && AuthEntityId(EntityNames.Module) == htmlText.ModuleId)
{
if (ModelState.IsValid && htmlText.ModuleId == _authEntityId[EntityNames.Module])
{
htmlText = _htmlText.UpdateHtmlText(htmlText);
_logger.Log(LogLevel.Information, this, LogFunction.Update, "Html/Text Updated {HtmlText}", htmlText);
}
htmlText = _htmlText.UpdateHtmlText(htmlText);
_logger.Log(LogLevel.Information, this, LogFunction.Update, "Html/Text Updated {HtmlText}", htmlText);
return htmlText;
}
catch (Exception ex)
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Update, ex, "Put Error {Error}", ex.Message);
throw;
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized HtmlText Put Attempt {HtmlText}", htmlText);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
return null;
}
}
// DELETE api/<controller>/5
[ValidateAntiForgeryToken]
[HttpDelete("{id}")]
[Authorize(Policy = PolicyNames.EditModule)]
public void Delete(int id)
{
try
if (AuthEntityId(EntityNames.Module) == id)
{
if (id == _authEntityId[EntityNames.Module])
{
_htmlText.DeleteHtmlText(id);
_logger.Log(LogLevel.Information, this, LogFunction.Delete, "Html/Text Deleted {HtmlTextId}", id);
}
_htmlText.DeleteHtmlText(id);
_logger.Log(LogLevel.Information, this, LogFunction.Delete, "Html/Text Deleted {HtmlTextId}", id);
}
catch (Exception ex)
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Delete, ex, "Delete Error {Error}", ex.Message);
throw;
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized HtmlText Delete Attempt {ModuleId}", id);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
}
}
}

View File

@ -60,12 +60,13 @@ namespace Oqtane.Pages
ProcessThemeControls(assembly);
}
// if culture not specified and framework is installed
// if framework is installed
if (!string.IsNullOrEmpty(_configuration.GetConnectionString("DefaultConnection")))
{
var alias = _tenantManager.GetAlias();
if (alias != null)
{
// if culture not specified
if (HttpContext.Request.Cookies[CookieRequestCultureProvider.DefaultCookieName] == null)
{
// set default language for site if the culture is not supported

View File

@ -22,24 +22,44 @@ namespace Oqtane.Security
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, PermissionRequirement requirement)
{
// permission is scoped based on auth{entityname}id (ie ?authmoduleid ) which must be passed as a querystring parameter
// permission is scoped based on entitynames and ids passed as querystring parameters or headers
var ctx = _httpContextAccessor.HttpContext;
if (ctx != null)
{
// get entityid based on a parameter format of auth{entityname}id (ie. authmoduleid )
int entityId = -1;
if (ctx.Request.Query.ContainsKey("auth" + requirement.EntityName.ToLower() + "id"))
{
entityId = int.Parse(ctx.Request.Query["auth" + requirement.EntityName.ToLower() + "id"]);
}
else
{
// legacy support
if (ctx.Request.Query.ContainsKey("entityid"))
if (!int.TryParse(ctx.Request.Query["auth" + requirement.EntityName.ToLower() + "id"], out entityId))
{
entityId = int.Parse(ctx.Request.Query["entityid"]);
entityId = -1;
}
}
if (_userPermissions.IsAuthorized(context.User, requirement.EntityName, entityId, requirement.PermissionName))
if (entityId == -1)
{
if (ctx.Request.Headers.ContainsKey("auth" + requirement.EntityName.ToLower() + "id"))
{
if (!int.TryParse(ctx.Request.Headers["auth" + requirement.EntityName.ToLower() + "id"], out entityId))
{
entityId = -1;
}
}
}
// legacy support
if (entityId == -1)
{
if (ctx.Request.Query.ContainsKey("entityid"))
{
if (!int.TryParse(ctx.Request.Query["entityid"], out entityId))
{
entityId = -1;
}
}
}
// validate permissions
if (entityId != -1 && _userPermissions.IsAuthorized(context.User, requirement.EntityName, entityId, requirement.PermissionName))
{
context.Succeed(requirement);
}

View File

@ -29,7 +29,7 @@ namespace Oqtane.Security
{
// tenant agnostic requests must be ignored
string path = context.Request.Path.ToString().ToLower();
if (path.StartsWith("/_blazor") || path.StartsWith("/api/installation/") || path.StartsWith("/api/alias/name/"))
if (path.StartsWith("/_blazor") || path.StartsWith("/api/installation/"))
{
return Task.CompletedTask;
}

View File

@ -78,12 +78,12 @@ namespace Oqtane
var navigationManager = s.GetRequiredService<NavigationManager>();
var client = new HttpClient(new HttpClientHandler { UseCookies = false });
client.BaseAddress = new Uri(navigationManager.Uri);
// set the auth cookie to allow HttpClient API calls to be authenticated
// set the cookies to allow HttpClient API calls to be authenticated
var httpContextAccessor = s.GetRequiredService<IHttpContextAccessor>();
var authToken = httpContextAccessor.HttpContext.Request.Cookies[".AspNetCore." + Constants.AuthenticationScheme];
if (authToken != null)
foreach (var cookie in httpContextAccessor.HttpContext.Request.Cookies)
{
client.DefaultRequestHeaders.Add("Cookie", ".AspNetCore." + Constants.AuthenticationScheme + "=" + authToken);
client.DefaultRequestHeaders.Add("Cookie", cookie.Key + "=" + cookie.Value);
}
return client;
});
@ -131,6 +131,7 @@ namespace Oqtane
services.AddScoped<ILocalizationService, LocalizationService>();
services.AddScoped<ILanguageService, LanguageService>();
services.AddScoped<IDatabaseService, DatabaseService>();
services.AddScoped<ISyncService, SyncService>();
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
@ -164,14 +165,30 @@ namespace Oqtane
services.ConfigureApplicationCookie(options =>
{
options.Cookie.HttpOnly = false;
options.Cookie.SameSite = SameSiteMode.Strict;
options.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest;
options.Events.OnRedirectToLogin = context =>
{
context.Response.StatusCode = (int)HttpStatusCode.Forbidden;
return Task.CompletedTask;
};
options.Events.OnRedirectToAccessDenied = context =>
{
context.Response.StatusCode = (int)HttpStatusCode.Forbidden;
return Task.CompletedTask;
};
options.Events.OnValidatePrincipal = PrincipalValidator.ValidateAsync;
});
services.AddAntiforgery(options =>
{
options.HeaderName = Constants.AntiForgeryTokenHeaderName;
options.Cookie.HttpOnly = false;
options.Cookie.Name = Constants.AntiForgeryTokenCookieName;
options.Cookie.SameSite = SameSiteMode.Strict;
options.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest;
});
// register singleton scoped core services
services.AddSingleton(Configuration);
services.AddSingleton<IInstallationManager, InstallationManager>();