Added support for per site options and OpenID Connect

This commit is contained in:
Shaun Walker
2022-03-13 22:55:52 -04:00
parent a47ecbdea9
commit 9bbbff31f8
31 changed files with 1064 additions and 180 deletions

View File

@ -0,0 +1,18 @@
using Microsoft.AspNetCore.Http;
using Oqtane.Extensions;
using Oqtane.Models;
namespace Oqtane.Infrastructure
{
public class AliasAccessor : IAliasAccessor
{
private readonly IHttpContextAccessor _httpContextAccessor;
public AliasAccessor(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}
public Alias Alias => _httpContextAccessor.HttpContext.GetAlias();
}
}

View File

@ -0,0 +1,9 @@
using Oqtane.Models;
namespace Oqtane.Infrastructure
{
public interface IAliasAccessor
{
Alias Alias { get; }
}
}

View File

@ -0,0 +1,112 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Options;
namespace Oqtane.Infrastructure
{
internal class SiteAuthenticationSchemeProvider : IAuthenticationSchemeProvider
{
public SiteAuthenticationSchemeProvider(IOptions<AuthenticationOptions> options)
: this(options, new Dictionary<string, AuthenticationScheme>(StringComparer.Ordinal))
{
}
public SiteAuthenticationSchemeProvider(IOptions<AuthenticationOptions> options, IDictionary<string, AuthenticationScheme> schemes)
{
_optionsProvider = options;
_schemes = schemes ?? throw new ArgumentNullException(nameof(schemes));
_requestHandlers = new List<AuthenticationScheme>();
foreach (var builder in _optionsProvider.Value.Schemes)
{
var scheme = builder.Build();
AddScheme(scheme);
}
}
private readonly IOptions<AuthenticationOptions> _optionsProvider;
private readonly object _lock = new object();
private readonly IDictionary<string, AuthenticationScheme> _schemes;
private readonly List<AuthenticationScheme> _requestHandlers;
private Task<AuthenticationScheme> GetDefaultSchemeAsync()
=> _optionsProvider.Value.DefaultScheme != null
? GetSchemeAsync(_optionsProvider.Value.DefaultScheme)
: Task.FromResult<AuthenticationScheme>(null);
public virtual Task<AuthenticationScheme> GetDefaultAuthenticateSchemeAsync()
=> _optionsProvider.Value.DefaultAuthenticateScheme != null
? GetSchemeAsync(_optionsProvider.Value.DefaultAuthenticateScheme)
: GetDefaultSchemeAsync();
public virtual Task<AuthenticationScheme> GetDefaultChallengeSchemeAsync()
=> _optionsProvider.Value.DefaultChallengeScheme != null
? GetSchemeAsync(_optionsProvider.Value.DefaultChallengeScheme)
: GetDefaultSchemeAsync();
public virtual Task<AuthenticationScheme> GetDefaultForbidSchemeAsync()
=> _optionsProvider.Value.DefaultForbidScheme != null
? GetSchemeAsync(_optionsProvider.Value.DefaultForbidScheme)
: GetDefaultChallengeSchemeAsync();
public virtual Task<AuthenticationScheme> GetDefaultSignInSchemeAsync()
=> _optionsProvider.Value.DefaultSignInScheme != null
? GetSchemeAsync(_optionsProvider.Value.DefaultSignInScheme)
: GetDefaultSchemeAsync();
public virtual Task<AuthenticationScheme> GetDefaultSignOutSchemeAsync()
=> _optionsProvider.Value.DefaultSignOutScheme != null
? GetSchemeAsync(_optionsProvider.Value.DefaultSignOutScheme)
: GetDefaultSignInSchemeAsync();
public virtual Task<AuthenticationScheme> GetSchemeAsync(string name)
=> Task.FromResult(_schemes.ContainsKey(name) ? _schemes[name] : null);
public virtual Task<IEnumerable<AuthenticationScheme>> GetRequestHandlerSchemesAsync()
=> Task.FromResult<IEnumerable<AuthenticationScheme>>(_requestHandlers);
public virtual void AddScheme(AuthenticationScheme scheme)
{
if (_schemes.ContainsKey(scheme.Name))
{
throw new InvalidOperationException("Scheme already exists: " + scheme.Name);
}
lock (_lock)
{
if (_schemes.ContainsKey(scheme.Name))
{
throw new InvalidOperationException("Scheme already exists: " + scheme.Name);
}
if (typeof(IAuthenticationRequestHandler).IsAssignableFrom(scheme.HandlerType))
{
_requestHandlers.Add(scheme);
}
_schemes[scheme.Name] = scheme;
}
}
public virtual void RemoveScheme(string name)
{
if (!_schemes.ContainsKey(name))
{
return;
}
lock (_lock)
{
if (_schemes.ContainsKey(name))
{
var scheme = _schemes[name];
_requestHandlers.Remove(scheme);
_schemes.Remove(name);
}
}
}
public virtual Task<IEnumerable<AuthenticationScheme>> GetAllSchemesAsync()
=> Task.FromResult<IEnumerable<AuthenticationScheme>>(_schemes.Values);
}
}

View File

@ -0,0 +1,62 @@
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Http;
using Oqtane.Extensions;
using Oqtane.Models;
using Oqtane.Shared;
namespace Oqtane.Infrastructure
{
internal class SiteAuthenticationService<TAlias> : IAuthenticationService
where TAlias : class, IAlias, new()
{
private readonly IAuthenticationService _inner;
public SiteAuthenticationService(IAuthenticationService inner)
{
_inner = inner ?? throw new System.ArgumentNullException(nameof(inner));
}
private static void AddTenantIdentifierToProperties(HttpContext context, ref AuthenticationProperties properties)
{
// add site identifier to the authentication properties so on the callback we can use it to set context
var alias = context.GetAlias();
if (alias != null)
{
properties ??= new AuthenticationProperties();
if (!properties.Items.Keys.Contains(Constants.SiteToken))
{
properties.Items.Add(Constants.SiteToken, alias.SiteKey);
}
}
}
public Task<AuthenticateResult> AuthenticateAsync(HttpContext context, string scheme)
=> _inner.AuthenticateAsync(context, scheme);
public async Task ChallengeAsync(HttpContext context, string scheme, AuthenticationProperties properties)
{
AddTenantIdentifierToProperties(context, ref properties);
await _inner.ChallengeAsync(context, scheme, properties);
}
public async Task ForbidAsync(HttpContext context, string scheme, AuthenticationProperties properties)
{
AddTenantIdentifierToProperties(context, ref properties);
await _inner.ForbidAsync(context, scheme, properties);
}
public async Task SignInAsync(HttpContext context, string scheme, ClaimsPrincipal principal, AuthenticationProperties properties)
{
AddTenantIdentifierToProperties(context, ref properties);
await _inner.SignInAsync(context, scheme, principal, properties);
}
public async Task SignOutAsync(HttpContext context, string scheme, AuthenticationProperties properties)
{
AddTenantIdentifierToProperties(context, ref properties);
await _inner.SignOutAsync(context, scheme, properties);
}
}
}

View File

@ -1,5 +1,8 @@
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Oqtane.Repository;
using Oqtane.Shared;
namespace Oqtane.Infrastructure
{
@ -18,19 +21,30 @@ namespace Oqtane.Infrastructure
var config = context.RequestServices.GetService(typeof(IConfigManager)) as IConfigManager;
if (config.IsInstalled())
{
// get alias
// get alias (note that this also sets SiteState.Alias)
var tenantManager = context.RequestServices.GetService(typeof(ITenantManager)) as ITenantManager;
var alias = tenantManager.GetAlias();
// rewrite path by removing alias path prefix from api and pages requests
if (alias != null && !string.IsNullOrEmpty(alias.Path))
if (alias != null)
{
string path = context.Request.Path.ToString();
if (path.StartsWith("/" + alias.Path) && (path.Contains("/api/") || path.Contains("/pages/")))
// get site settings and store alias in HttpContext
var settingRepository = context.RequestServices.GetService(typeof(ISettingRepository)) as ISettingRepository;
alias.SiteSettings = settingRepository.GetSettings(EntityNames.Site)
.ToDictionary(setting => setting.SettingName, setting => setting.SettingValue);
context.Items.Add(Constants.HttpContextAliasKey, alias);
// rewrite path by removing alias path prefix from api and pages requests (for consistent routing)
if (!string.IsNullOrEmpty(alias.Path))
{
context.Request.Path = path.Replace("/" + alias.Path, "");
string path = context.Request.Path.ToString();
if (path.StartsWith("/" + alias.Path) && (path.Contains("/api/") || path.Contains("/pages/")))
{
context.Request.Path = path.Replace("/" + alias.Path, "");
}
}
}
}
// continue processing

View File

@ -0,0 +1,12 @@
using Oqtane.Models;
namespace Oqtane.Infrastructure
{
public interface ISiteOptions<TOptions, TAlias>
where TOptions : class, new()
where TAlias : class, IAlias, new()
{
void Configure(TOptions options, TAlias siteOptions);
}
}

View File

@ -0,0 +1,22 @@
using System;
using Oqtane.Models;
namespace Oqtane.Infrastructure
{
public class SiteOptions<TOptions, TAlias> : ISiteOptions<TOptions, TAlias>
where TOptions : class, new()
where TAlias : class, IAlias, new()
{
private readonly Action<TOptions, TAlias> configureOptions;
public SiteOptions(Action<TOptions, TAlias> configureOptions)
{
this.configureOptions = configureOptions;
}
public void Configure(TOptions options, TAlias siteOptions)
{
configureOptions(options, siteOptions);
}
}
}

View File

@ -0,0 +1,70 @@
using System;
using System.Collections.Concurrent;
using Microsoft.Extensions.Options;
using Oqtane.Models;
namespace Oqtane.Infrastructure
{
public class SiteOptionsCache<TOptions, TAlias> : IOptionsMonitorCache<TOptions>
where TOptions : class
where TAlias : class, IAlias, new()
{
private readonly IAliasAccessor _aliasAccessor;
private readonly ConcurrentDictionary<string, IOptionsMonitorCache<TOptions>> map = new ConcurrentDictionary<string, IOptionsMonitorCache<TOptions>>();
public SiteOptionsCache(IAliasAccessor aliasAccessor)
{
_aliasAccessor = aliasAccessor;
}
public void Clear()
{
var cache = map.GetOrAdd(GetKey(), new OptionsCache<TOptions>());
cache.Clear();
}
public void Clear(Alias alias)
{
var cache = map.GetOrAdd(alias.SiteKey, new OptionsCache<TOptions>());
cache.Clear();
}
public void ClearAll()
{
foreach (var cache in map.Values)
{
cache.Clear();
}
}
public TOptions GetOrAdd(string name, Func<TOptions> createOptions)
{
name = name ?? Options.DefaultName;
var cache = map.GetOrAdd(GetKey(), new OptionsCache<TOptions>());
return cache.GetOrAdd(name, createOptions);
}
public bool TryAdd(string name, TOptions options)
{
name = name ?? Options.DefaultName;
var cache = map.GetOrAdd(GetKey(), new OptionsCache<TOptions>());
return cache.TryAdd(name, options);
}
public bool TryRemove(string name)
{
name = name ?? Options.DefaultName;
var cache = map.GetOrAdd(GetKey(), new OptionsCache<TOptions>());
return cache.TryRemove(name);
}
private string GetKey()
{
return _aliasAccessor?.Alias?.SiteKey ?? "";
}
}
}

View File

@ -0,0 +1,77 @@
using System.Collections.Generic;
using Microsoft.Extensions.Options;
using Oqtane.Models;
namespace Oqtane.Infrastructure
{
public class SiteOptionsFactory<TOptions, TAlias> : IOptionsFactory<TOptions>
where TOptions : class, new()
where TAlias : class, IAlias, new()
{
private readonly IConfigureOptions<TOptions>[] _configureOptions;
private readonly IPostConfigureOptions<TOptions>[] _postConfigureOptions;
private readonly IValidateOptions<TOptions>[] _validations;
private readonly ISiteOptions<TOptions, TAlias>[] _siteOptions;
private readonly IAliasAccessor _aliasAccessor;
public SiteOptionsFactory(IEnumerable<IConfigureOptions<TOptions>> configureOptions, IEnumerable<IPostConfigureOptions<TOptions>> postConfigureOptions, IEnumerable<IValidateOptions<TOptions>> validations, IEnumerable<ISiteOptions<TOptions, TAlias>> siteOptions, IAliasAccessor aliasAccessor)
{
_configureOptions = configureOptions as IConfigureOptions<TOptions>[] ?? new List<IConfigureOptions<TOptions>>(configureOptions).ToArray();
_postConfigureOptions = postConfigureOptions as IPostConfigureOptions<TOptions>[] ?? new List<IPostConfigureOptions<TOptions>>(postConfigureOptions).ToArray();
_validations = validations as IValidateOptions<TOptions>[] ?? new List<IValidateOptions<TOptions>>(validations).ToArray();
_siteOptions = siteOptions as ISiteOptions<TOptions, TAlias>[] ?? new List<ISiteOptions<TOptions, TAlias>>(siteOptions).ToArray();
_aliasAccessor = aliasAccessor;
}
public TOptions Create(string name)
{
// default options
var options = new TOptions();
foreach (var setup in _configureOptions)
{
if (setup is IConfigureNamedOptions<TOptions> namedSetup)
{
namedSetup.Configure(name, options);
}
else if (name == Options.DefaultName)
{
setup.Configure(options);
}
}
// override with site specific options
if (_aliasAccessor?.Alias != null)
{
foreach (var siteOption in _siteOptions)
{
siteOption.Configure(options, _aliasAccessor.Alias as TAlias);
}
}
// post configuration
foreach (var post in _postConfigureOptions)
{
post.PostConfigure(name, options);
}
//if (_validations.Length > 0)
//{
// var failures = new List<string>();
// foreach (IValidateOptions<TOptions> validate in _validations)
// {
// ValidateOptionsResult result = validate.Validate(name, options);
// if (result != null && result.Failed)
// {
// failures.AddRange(result.Failures);
// }
// }
// if (failures.Count > 0)
// {
// throw new OptionsValidationException(name, typeof(TOptions), failures);
// }
//}
return options;
}
}
}

View File

@ -0,0 +1,35 @@
using Microsoft.Extensions.Options;
namespace Oqtane.Infrastructure
{
public class SiteOptionsManager<TOptions> : IOptions<TOptions>, IOptionsSnapshot<TOptions> where TOptions : class, new()
{
private readonly IOptionsFactory<TOptions> _factory;
private readonly IOptionsMonitorCache<TOptions> _cache; // private cache
public SiteOptionsManager(IOptionsFactory<TOptions> factory, IOptionsMonitorCache<TOptions> cache)
{
_factory = factory;
_cache = cache;
}
public TOptions Value
{
get
{
return Get(Options.DefaultName);
}
}
public virtual TOptions Get(string name)
{
name = name ?? Options.DefaultName;
return _cache.GetOrAdd(name, () => _factory.Create(name));
}
public void Reset()
{
_cache.Clear();
}
}
}

View File

@ -26,7 +26,7 @@ namespace Oqtane.Infrastructure
{
Alias alias = null;
if (_siteState != null && _siteState.Alias != null)
if (_siteState != null && _siteState.Alias != null && _siteState.Alias.AliasId != -1)
{
alias = _siteState.Alias;
}