Added support for per site options and OpenID Connect
This commit is contained in:
		
							
								
								
									
										18
									
								
								Oqtane.Server/Infrastructure/AliasAccessor.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								Oqtane.Server/Infrastructure/AliasAccessor.cs
									
									
									
									
									
										Normal 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(); | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,9 @@ | ||||
| using Oqtane.Models; | ||||
|  | ||||
| namespace Oqtane.Infrastructure | ||||
| { | ||||
|     public interface IAliasAccessor  | ||||
|     { | ||||
|         Alias Alias { get; } | ||||
|     } | ||||
| } | ||||
| @ -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); | ||||
|     } | ||||
| } | ||||
| @ -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); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -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 | ||||
|  | ||||
							
								
								
									
										12
									
								
								Oqtane.Server/Infrastructure/Options/ISiteOptions.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								Oqtane.Server/Infrastructure/Options/ISiteOptions.cs
									
									
									
									
									
										Normal 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); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										22
									
								
								Oqtane.Server/Infrastructure/Options/SiteOptions.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								Oqtane.Server/Infrastructure/Options/SiteOptions.cs
									
									
									
									
									
										Normal 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); | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										70
									
								
								Oqtane.Server/Infrastructure/Options/SiteOptionsCache.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										70
									
								
								Oqtane.Server/Infrastructure/Options/SiteOptionsCache.cs
									
									
									
									
									
										Normal 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 ?? ""; | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										77
									
								
								Oqtane.Server/Infrastructure/Options/SiteOptionsFactory.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										77
									
								
								Oqtane.Server/Infrastructure/Options/SiteOptionsFactory.cs
									
									
									
									
									
										Normal 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; | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										35
									
								
								Oqtane.Server/Infrastructure/Options/SiteOptionsManager.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								Oqtane.Server/Infrastructure/Options/SiteOptionsManager.cs
									
									
									
									
									
										Normal 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(); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -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; | ||||
|             } | ||||
|  | ||||
		Reference in New Issue
	
	Block a user
	 Shaun Walker
					Shaun Walker