diff --git a/Oqtane.Client/AssemblyInfo.cs b/Oqtane.Client/AssemblyInfo.cs index d598bfb9..159ad127 100644 --- a/Oqtane.Client/AssemblyInfo.cs +++ b/Oqtane.Client/AssemblyInfo.cs @@ -1,3 +1,5 @@ -using Microsoft.Extensions.Localization; +using System.Runtime.CompilerServices; +using Microsoft.Extensions.Localization; [assembly: RootNamespace("Oqtane")] +[assembly: InternalsVisibleTo("Oqtane.Server")] diff --git a/Oqtane.Client/Extensions/OqtaneServiceCollectionExtensions.cs b/Oqtane.Client/Extensions/OqtaneServiceCollectionExtensions.cs new file mode 100644 index 00000000..40db5da5 --- /dev/null +++ b/Oqtane.Client/Extensions/OqtaneServiceCollectionExtensions.cs @@ -0,0 +1,54 @@ +using Microsoft.AspNetCore.Components.Authorization; +using Oqtane.Providers; +using Oqtane.Services; +using Oqtane.Shared; + +namespace Microsoft.Extensions.DependencyInjection +{ + public static class OqtaneServiceCollectionExtensions + { + public static IServiceCollection AddOqtaneAuthorization(this IServiceCollection services) + { + services.AddAuthorizationCore(); + services.AddScoped(); + services.AddScoped(s => s.GetRequiredService()); + + return services; + } + + internal static IServiceCollection AddOqtaneScopedServices(this IServiceCollection services) + { + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + + return services; + } + } +} diff --git a/Oqtane.Client/Modules/Admin/Dashboard/Index.razor b/Oqtane.Client/Modules/Admin/Dashboard/Index.razor index 0c7cd842..6c665ba2 100644 --- a/Oqtane.Client/Modules/Admin/Dashboard/Index.razor +++ b/Oqtane.Client/Modules/Admin/Dashboard/Index.razor @@ -2,7 +2,7 @@ @inherits ModuleBase @inject IPageService PageService @inject IUserService UserService -@inject IStringLocalizer Localizer +@inject IStringLocalizer SharedLocalizer
@foreach (var p in _pages) @@ -12,7 +12,7 @@ string url = NavigateUrl(p.Path);
-

@Localizer[p.Name] +

@SharedLocalizer[p.Name]
} diff --git a/Oqtane.Client/Oqtane.Client.csproj b/Oqtane.Client/Oqtane.Client.csproj index 24e2efb7..f589591a 100644 --- a/Oqtane.Client/Oqtane.Client.csproj +++ b/Oqtane.Client/Oqtane.Client.csproj @@ -33,4 +33,8 @@ + + + + diff --git a/Oqtane.Client/Program.cs b/Oqtane.Client/Program.cs index 26f7a286..ff27f5c0 100644 --- a/Oqtane.Client/Program.cs +++ b/Oqtane.Client/Program.cs @@ -8,13 +8,11 @@ using System.Net.Http; using System.Reflection; using System.Runtime.Loader; using System.Threading.Tasks; -using Microsoft.AspNetCore.Components.Authorization; using Microsoft.AspNetCore.Components.WebAssembly.Hosting; using Microsoft.AspNetCore.Localization; using Microsoft.Extensions.DependencyInjection; using Microsoft.JSInterop; using Oqtane.Modules; -using Oqtane.Providers; using Oqtane.Services; using Oqtane.Shared; using Oqtane.UI; @@ -27,7 +25,8 @@ namespace Oqtane.Client { var builder = WebAssemblyHostBuilder.CreateDefault(args); builder.RootComponents.Add("app"); - HttpClient httpClient = new HttpClient {BaseAddress = new Uri(builder.HostEnvironment.BaseAddress)}; + + var httpClient = new HttpClient {BaseAddress = new Uri(builder.HostEnvironment.BaseAddress)}; builder.Services.AddSingleton(httpClient); builder.Services.AddOptions(); @@ -36,40 +35,10 @@ namespace Oqtane.Client builder.Services.AddLocalization(options => options.ResourcesPath = "Resources"); // register auth services - builder.Services.AddAuthorizationCore(); - builder.Services.AddScoped(); - builder.Services.AddScoped(s => s.GetRequiredService()); + builder.Services.AddOqtaneAuthorization(); // register scoped core services - builder.Services.AddScoped(); - builder.Services.AddScoped(); - builder.Services.AddScoped(); - builder.Services.AddScoped(); - builder.Services.AddScoped(); - builder.Services.AddScoped(); - builder.Services.AddScoped(); - builder.Services.AddScoped(); - builder.Services.AddScoped(); - builder.Services.AddScoped(); - builder.Services.AddScoped(); - builder.Services.AddScoped(); - builder.Services.AddScoped(); - builder.Services.AddScoped(); - builder.Services.AddScoped(); - builder.Services.AddScoped(); - builder.Services.AddScoped(); - builder.Services.AddScoped(); - builder.Services.AddScoped(); - builder.Services.AddScoped(); - builder.Services.AddScoped(); - builder.Services.AddScoped(); - builder.Services.AddScoped(); - builder.Services.AddScoped(); - builder.Services.AddScoped(); - builder.Services.AddScoped(); - builder.Services.AddScoped(); - builder.Services.AddScoped(); - builder.Services.AddScoped(); + builder.Services.AddOqtaneScopedServices(); await LoadClientAssemblies(httpClient); @@ -77,38 +46,15 @@ namespace Oqtane.Client foreach (var assembly in assemblies) { // dynamically register module services - var implementationTypes = assembly.GetInterfaces(); - foreach (var implementationType in implementationTypes) - { - if (implementationType.AssemblyQualifiedName != null) - { - var serviceType = Type.GetType(implementationType.AssemblyQualifiedName.Replace(implementationType.Name, $"I{implementationType.Name}")); - builder.Services.AddScoped(serviceType ?? implementationType, implementationType); - } - } + RegisterModuleServices(assembly, builder.Services); // register client startup services - var startUps = assembly.GetInstances(); - foreach (var startup in startUps) - { - startup.ConfigureServices(builder.Services); - } + RegisterClientStartups(assembly, builder.Services); } var host = builder.Build(); - var jsRuntime = host.Services.GetRequiredService(); - var interop = new Interop(jsRuntime); - var localizationCookie = await interop.GetCookie(CookieRequestCultureProvider.DefaultCookieName); - var culture = CookieRequestCultureProvider.ParseCookieValue(localizationCookie).UICultures[0].Value; - var localizationService = host.Services.GetRequiredService(); - var cultures = await localizationService.GetCulturesAsync(); - if (culture == null || !cultures.Any(c => c.Name.Equals(culture, StringComparison.OrdinalIgnoreCase))) - { - culture = cultures.Single(c => c.IsDefault).Name; - } - - SetCulture(culture); + await SetCultureFromLocalizationCookie(host.Services); ServiceActivator.Configure(host.Services); @@ -164,6 +110,45 @@ namespace Oqtane.Client } } + private static void RegisterModuleServices(Assembly assembly, IServiceCollection services) + { + var implementationTypes = assembly.GetInterfaces(); + foreach (var implementationType in implementationTypes) + { + if (implementationType.AssemblyQualifiedName != null) + { + var serviceType = Type.GetType(implementationType.AssemblyQualifiedName.Replace(implementationType.Name, $"I{implementationType.Name}")); + services.AddScoped(serviceType ?? implementationType, implementationType); + } + } + } + + private static void RegisterClientStartups(Assembly assembly, IServiceCollection services) + { + var startUps = assembly.GetInstances(); + foreach (var startup in startUps) + { + startup.ConfigureServices(services); + } + } + + private static async Task SetCultureFromLocalizationCookie(IServiceProvider serviceProvider) + { + var jsRuntime = serviceProvider.GetRequiredService(); + var interop = new Interop(jsRuntime); + var localizationCookie = await interop.GetCookie(CookieRequestCultureProvider.DefaultCookieName); + var culture = CookieRequestCultureProvider.ParseCookieValue(localizationCookie).UICultures[0].Value; + var localizationService = serviceProvider.GetRequiredService(); + var cultures = await localizationService.GetCulturesAsync(); + + if (culture == null || !cultures.Any(c => c.Name.Equals(culture, StringComparison.OrdinalIgnoreCase))) + { + culture = cultures.Single(c => c.IsDefault).Name; + } + + SetCulture(culture); + } + private static void SetCulture(string culture) { var cultureInfo = CultureInfo.GetCultureInfo(culture); diff --git a/Oqtane.Client/Resources/SharedResources.cs b/Oqtane.Client/Resources/SharedResources.cs deleted file mode 100644 index 2c1d6f25..00000000 --- a/Oqtane.Client/Resources/SharedResources.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Oqtane -{ - public class SharedResources - { - - } -} diff --git a/Oqtane.Client/Resources/SharedResources.resx b/Oqtane.Client/Resources/SharedResources.resx new file mode 100644 index 00000000..5296443a --- /dev/null +++ b/Oqtane.Client/Resources/SharedResources.resx @@ -0,0 +1,207 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + True + + + False + + + Yes + + + No + + + Save + + + Update + + + Delete + + + Cancel + + + Admin Dashboard + + + User Login + + + User Registration + + + Password Reset + + + User Profile + + + Site Settings + + + Page Management + + + User Management + + + Profile Management + + + Role Management + + + File Management + + + Recycle Bin + + + Event Log + + + Site Management + + + Module Management + + + Theme Management + + + Language Management + + + Scheduled Jobs + + + Sql Management + + + System Info + + + System Update + + \ No newline at end of file diff --git a/Oqtane.Client/Services/Interfaces/IJobLogService.cs b/Oqtane.Client/Services/Interfaces/IJobLogService.cs index 015d83f0..e198bf9f 100644 --- a/Oqtane.Client/Services/Interfaces/IJobLogService.cs +++ b/Oqtane.Client/Services/Interfaces/IJobLogService.cs @@ -1,4 +1,4 @@ -using Oqtane.Models; +using Oqtane.Models; using System.Collections.Generic; using System.Threading.Tasks; @@ -9,11 +9,5 @@ namespace Oqtane.Services Task> GetJobLogsAsync(); Task GetJobLogAsync(int jobLogId); - - Task AddJobLogAsync(JobLog jobLog); - - Task UpdateJobLogAsync(JobLog jobLog); - - Task DeleteJobLogAsync(int jobLogId); } } diff --git a/Oqtane.Client/Services/Interfaces/ITenantService.cs b/Oqtane.Client/Services/Interfaces/ITenantService.cs index 12738f58..cbb4e526 100644 --- a/Oqtane.Client/Services/Interfaces/ITenantService.cs +++ b/Oqtane.Client/Services/Interfaces/ITenantService.cs @@ -21,26 +21,5 @@ namespace Oqtane.Services /// ID-reference of the /// Task GetTenantAsync(int tenantId); - - /// - /// Add / save another to the database - /// - /// A object containing the configuration - /// - Task AddTenantAsync(Tenant tenant); - - /// - /// Update the information in the database. - /// - /// - /// - Task UpdateTenantAsync(Tenant tenant); - - /// - /// Delete / remove a - /// - /// - /// - Task DeleteTenantAsync(int tenantId); } } diff --git a/Oqtane.Client/Services/JobLogService.cs b/Oqtane.Client/Services/JobLogService.cs index 6d9a3e56..6c2f0a73 100644 --- a/Oqtane.Client/Services/JobLogService.cs +++ b/Oqtane.Client/Services/JobLogService.cs @@ -30,19 +30,5 @@ namespace Oqtane.Services { return await GetJsonAsync($"{Apiurl}/{jobLogId}"); } - - public async Task AddJobLogAsync(JobLog joblog) - { - return await PostJsonAsync(Apiurl, joblog); - } - - public async Task UpdateJobLogAsync(JobLog joblog) - { - return await PutJsonAsync($"{Apiurl}/{joblog.JobLogId}", joblog); - } - public async Task DeleteJobLogAsync(int jobLogId) - { - await DeleteAsync($"{Apiurl}/{jobLogId}"); - } } } diff --git a/Oqtane.Client/Services/TenantService.cs b/Oqtane.Client/Services/TenantService.cs index 52b3d345..6c02b9a9 100644 --- a/Oqtane.Client/Services/TenantService.cs +++ b/Oqtane.Client/Services/TenantService.cs @@ -30,20 +30,5 @@ namespace Oqtane.Services { return await GetJsonAsync($"{Apiurl}/{tenantId}"); } - - public async Task AddTenantAsync(Tenant tenant) - { - return await PostJsonAsync(Apiurl, tenant); - } - - public async Task UpdateTenantAsync(Tenant tenant) - { - return await PutJsonAsync($"{Apiurl}/{tenant.TenantId}", tenant); - } - - public async Task DeleteTenantAsync(int tenantId) - { - await DeleteAsync($"{Apiurl}/{tenantId}"); - } } } diff --git a/Oqtane.Client/SharedResources.cs b/Oqtane.Client/SharedResources.cs new file mode 100644 index 00000000..da203b14 --- /dev/null +++ b/Oqtane.Client/SharedResources.cs @@ -0,0 +1,14 @@ +namespace Oqtane.Client +{ + /// + /// Dummy class used to collect shared resource strings for this application + /// + /// + /// This class is mostly used with IStringLocalizer and IHtmlLocalizer interfaces. + /// The class must reside at the project root. + /// + public class SharedResources + { + + } +} diff --git a/Oqtane.Client/_Imports.razor b/Oqtane.Client/_Imports.razor index cbf43e76..45403934 100644 --- a/Oqtane.Client/_Imports.razor +++ b/Oqtane.Client/_Imports.razor @@ -10,6 +10,7 @@ @using Microsoft.Extensions.Localization @using Microsoft.JSInterop +@using Oqtane.Client @using Oqtane.Models @using Oqtane.Modules @using Oqtane.Modules.Controls @@ -22,3 +23,4 @@ @using Oqtane.UI @using Oqtane.Enums @using Oqtane.Installer +@using Oqtane.Interfaces diff --git a/Oqtane.Server/Controllers/AliasController.cs b/Oqtane.Server/Controllers/AliasController.cs index a61f2ed4..644fa278 100644 --- a/Oqtane.Server/Controllers/AliasController.cs +++ b/Oqtane.Server/Controllers/AliasController.cs @@ -72,7 +72,7 @@ namespace Oqtane.Controllers [Authorize(Roles = RoleNames.Host)] public Alias Put(int id, [FromBody] Alias alias) { - if (ModelState.IsValid) + if (ModelState.IsValid && _aliases.GetAlias(alias.AliasId, false) != null) { alias = _aliases.UpdateAlias(alias); _logger.Log(LogLevel.Information, this, LogFunction.Update, "Alias Updated {Alias}", alias); @@ -91,8 +91,17 @@ namespace Oqtane.Controllers [Authorize(Roles = RoleNames.Host)] public void Delete(int id) { - _aliases.DeleteAlias(id); - _logger.Log(LogLevel.Information, this, LogFunction.Delete, "Alias Deleted {AliasId}", id); + var alias = _aliases.GetAlias(id); + if (alias != null) + { + _aliases.DeleteAlias(id); + _logger.Log(LogLevel.Information, this, LogFunction.Delete, "Alias Deleted {AliasId}", id); + } + else + { + _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Alias Delete Attempt {AliasId}", id); + HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; + } } } } diff --git a/Oqtane.Server/Controllers/JobController.cs b/Oqtane.Server/Controllers/JobController.cs index db4f4bfb..356fd75f 100644 --- a/Oqtane.Server/Controllers/JobController.cs +++ b/Oqtane.Server/Controllers/JobController.cs @@ -9,6 +9,7 @@ using Microsoft.Extensions.DependencyInjection; using Oqtane.Enums; using Oqtane.Infrastructure; using Oqtane.Repository; +using System.Net; namespace Oqtane.Controllers { @@ -52,6 +53,12 @@ namespace Oqtane.Controllers job = _jobs.AddJob(job); _logger.Log(LogLevel.Information, this, LogFunction.Create, "Job Added {Job}", job); } + else + { + _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Job Post Attempt {Alias}", job); + HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; + job = null; + } return job; } @@ -60,11 +67,17 @@ namespace Oqtane.Controllers [Authorize(Roles = RoleNames.Host)] public Job Put(int id, [FromBody] Job job) { - if (ModelState.IsValid) + if (ModelState.IsValid && _jobs.GetJob(job.JobId, false) != null) { job = _jobs.UpdateJob(job); _logger.Log(LogLevel.Information, this, LogFunction.Update, "Job Updated {Job}", job); } + else + { + _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Job Put Attempt {Alias}", job); + HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; + job = null; + } return job; } @@ -73,8 +86,17 @@ namespace Oqtane.Controllers [Authorize(Roles = RoleNames.Host)] public void Delete(int id) { - _jobs.DeleteJob(id); - _logger.Log(LogLevel.Information, this, LogFunction.Delete, "Job Deleted {JobId}", id); + var job = _jobs.GetJob(id); + if (job != null) + { + _jobs.DeleteJob(id); + _logger.Log(LogLevel.Information, this, LogFunction.Delete, "Job Deleted {JobId}", id); + } + else + { + _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Job Delete Attempt {JobId}", id); + HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; + } } // GET api//start @@ -83,12 +105,17 @@ namespace Oqtane.Controllers public void Start(int id) { Job job = _jobs.GetJob(id); - Type jobtype = Type.GetType(job.JobType); - if (jobtype != null) + if (job != null) { + Type jobtype = Type.GetType(job.JobType); var jobobject = ActivatorUtilities.CreateInstance(_serviceProvider, jobtype); ((IHostedService)jobobject).StartAsync(new System.Threading.CancellationToken()); } + else + { + _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Job Start Attempt {JobId}", id); + HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; + } } // GET api//stop @@ -97,12 +124,17 @@ namespace Oqtane.Controllers public void Stop(int id) { Job job = _jobs.GetJob(id); - Type jobtype = Type.GetType(job.JobType); - if (jobtype != null) + if (job != null) { + Type jobtype = Type.GetType(job.JobType); var jobobject = ActivatorUtilities.CreateInstance(_serviceProvider, jobtype); ((IHostedService)jobobject).StopAsync(new System.Threading.CancellationToken()); } + else + { + _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Job Stop Attempt {JobId}", id); + HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; + } } } } diff --git a/Oqtane.Server/Controllers/JobLogController.cs b/Oqtane.Server/Controllers/JobLogController.cs index 39fd8ac9..5f711e4f 100644 --- a/Oqtane.Server/Controllers/JobLogController.cs +++ b/Oqtane.Server/Controllers/JobLogController.cs @@ -1,10 +1,8 @@ using System.Collections.Generic; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Authorization; -using Oqtane.Enums; using Oqtane.Models; using Oqtane.Shared; -using Oqtane.Infrastructure; using Oqtane.Repository; namespace Oqtane.Controllers @@ -13,12 +11,10 @@ namespace Oqtane.Controllers public class JobLogController : Controller { private readonly IJobLogRepository _jobLogs; - private readonly ILogManager _logger; - public JobLogController(IJobLogRepository jobLogs, ILogManager logger) + public JobLogController(IJobLogRepository jobLogs) { _jobLogs = jobLogs; - _logger = logger; } // GET: api/ @@ -36,40 +32,5 @@ namespace Oqtane.Controllers { return _jobLogs.GetJobLog(id); } - - // POST api/ - [HttpPost] - [Authorize(Roles = RoleNames.Host)] - public JobLog Post([FromBody] JobLog jobLog) - { - if (ModelState.IsValid) - { - jobLog = _jobLogs.AddJobLog(jobLog); - _logger.Log(LogLevel.Information, this, LogFunction.Create, "Job Log Added {JobLog}", jobLog); - } - return jobLog; - } - - // PUT api//5 - [HttpPut("{id}")] - [Authorize(Roles = RoleNames.Host)] - public JobLog Put(int id, [FromBody] JobLog jobLog) - { - if (ModelState.IsValid) - { - jobLog = _jobLogs.UpdateJobLog(jobLog); - _logger.Log(LogLevel.Information, this, LogFunction.Update, "Job Log Updated {JobLog}", jobLog); - } - return jobLog; - } - - // DELETE api//5 - [HttpDelete("{id}")] - [Authorize(Roles = RoleNames.Host)] - public void Delete(int id) - { - _jobLogs.DeleteJobLog(id); - _logger.Log(LogLevel.Information, this, LogFunction.Delete, "Job Log Deleted {JobLogId}", id); - } } } diff --git a/Oqtane.Server/Controllers/TenantController.cs b/Oqtane.Server/Controllers/TenantController.cs index 6f6fb4e8..1b01d0f1 100644 --- a/Oqtane.Server/Controllers/TenantController.cs +++ b/Oqtane.Server/Controllers/TenantController.cs @@ -36,40 +36,5 @@ namespace Oqtane.Controllers { return _tenants.GetTenant(id); } - - // POST api/ - [HttpPost] - [Authorize(Roles = RoleNames.Host)] - public Tenant Post([FromBody] Tenant tenant) - { - if (ModelState.IsValid) - { - tenant = _tenants.AddTenant(tenant); - _logger.Log(LogLevel.Information, this, LogFunction.Create, "Tenant Added {TenantId}", tenant.TenantId); - } - return tenant; - } - - // PUT api//5 - [HttpPut("{id}")] - [Authorize(Roles = RoleNames.Host)] - public Tenant Put(int id, [FromBody] Tenant tenant) - { - if (ModelState.IsValid) - { - tenant = _tenants.UpdateTenant(tenant); - _logger.Log(LogLevel.Information, this, LogFunction.Update, "Tenant Updated {TenantId}", tenant.TenantId); - } - return tenant; - } - - // DELETE api//5 - [HttpDelete("{id}")] - [Authorize(Roles = RoleNames.Host)] - public void Delete(int id) - { - _tenants.DeleteTenant(id); - _logger.Log(LogLevel.Information, this, LogFunction.Delete, "Tenant Deleted {TenantId}", id); - } } } diff --git a/Oqtane.Server/Controllers/ThemeController.cs b/Oqtane.Server/Controllers/ThemeController.cs index 7c05b0ba..dd8bae6f 100644 --- a/Oqtane.Server/Controllers/ThemeController.cs +++ b/Oqtane.Server/Controllers/ThemeController.cs @@ -11,6 +11,7 @@ using Oqtane.Enums; using Oqtane.Infrastructure; using Oqtane.Repository; using System.Text.Json; +using System.Net; // ReSharper disable StringIndexOfIsCultureSpecific.1 @@ -84,6 +85,11 @@ namespace Oqtane.Controllers _themes.DeleteTheme(theme.ThemeName); _logger.Log(LogLevel.Information, this, LogFunction.Delete, "Theme Removed For {ThemeName}", theme.ThemeName); } + else + { + _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Theme Delete Attempt {Themename}", themename); + HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; + } } // GET: api//templates @@ -141,6 +147,12 @@ namespace Oqtane.Controllers ProcessTemplatesRecursively(new DirectoryInfo(templatePath), rootPath, rootFolder.Name, templatePath, theme); _logger.Log(LogLevel.Information, this, LogFunction.Create, "Theme Created {Theme}", theme); } + else + { + _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Theme Post Attempt {Theme}", theme); + HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; + theme = null; + } return theme; } diff --git a/Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs b/Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs index 55f5eb8a..0f8ba283 100644 --- a/Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs +++ b/Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs @@ -1,16 +1,23 @@ using System; using System.IO; using System.Linq; +using System.Net; +using System.Net.Http; using System.Reflection; using System.Runtime.Loader; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Hosting; +using Microsoft.OpenApi.Models; using Oqtane.Infrastructure; -using Oqtane.Interfaces; using Oqtane.Modules; +using Oqtane.Repository; +using Oqtane.Security; using Oqtane.Services; using Oqtane.Shared; -// ReSharper disable once CheckNamespace namespace Microsoft.Extensions.DependencyInjection { public static class OqtaneServiceCollectionExtensions @@ -24,6 +31,161 @@ namespace Microsoft.Extensions.DependencyInjection return services; } + public static IServiceCollection AddOqtaneDbContext(this IServiceCollection services) + { + services.AddDbContext(options => { }); + services.AddDbContext(options => { }); + + return services; + } + + public static IServiceCollection AddOqtaneAuthorizationPolicies(this IServiceCollection services) + { + services.AddAuthorizationCore(options => + { + options.AddPolicy(PolicyNames.ViewPage, policy => policy.Requirements.Add(new PermissionRequirement(EntityNames.Page, PermissionNames.View))); + options.AddPolicy(PolicyNames.EditPage, policy => policy.Requirements.Add(new PermissionRequirement(EntityNames.Page, PermissionNames.Edit))); + options.AddPolicy(PolicyNames.ViewModule, policy => policy.Requirements.Add(new PermissionRequirement(EntityNames.Module, PermissionNames.View))); + options.AddPolicy(PolicyNames.EditModule, policy => policy.Requirements.Add(new PermissionRequirement(EntityNames.Module, PermissionNames.Edit))); + options.AddPolicy(PolicyNames.ViewFolder, policy => policy.Requirements.Add(new PermissionRequirement(EntityNames.Folder, PermissionNames.View))); + options.AddPolicy(PolicyNames.EditFolder, policy => policy.Requirements.Add(new PermissionRequirement(EntityNames.Folder, PermissionNames.Edit))); + options.AddPolicy(PolicyNames.ListFolder, policy => policy.Requirements.Add(new PermissionRequirement(EntityNames.Folder, PermissionNames.Browse))); + }); + + return services; + } + + internal static IServiceCollection AddOqtaneSingletonServices(this IServiceCollection services) + { + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + + return services; + } + + internal static IServiceCollection AddOqtaneTransientServices(this IServiceCollection services) + { + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + // obsolete - replaced by ITenantManager + services.AddTransient(); + + return services; + } + + public static IServiceCollection ConfigureOqtaneCookieOptions(this IServiceCollection services) + { + 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; + }); + + return services; + } + + public static IServiceCollection ConfigureOqtaneIdentityOptions(this IServiceCollection services) + { + services.Configure(options => + { + // Password settings + options.Password.RequireDigit = false; + options.Password.RequiredLength = 6; + options.Password.RequireNonAlphanumeric = false; + options.Password.RequireUppercase = false; + options.Password.RequireLowercase = false; + + // Lockout settings + options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(30); + options.Lockout.MaxFailedAccessAttempts = 10; + options.Lockout.AllowedForNewUsers = true; + + // User settings + options.User.RequireUniqueEmail = false; + }); + + return services; + } + + internal static IServiceCollection TryAddHttpClientWithAuthenticationCookie(this IServiceCollection services) + { + if (!services.Any(x => x.ServiceType == typeof(HttpClient))) + { + services.AddScoped(s => + { + // creating the URI helper needs to wait until the JS Runtime is initialized, so defer it. + var navigationManager = s.GetRequiredService(); + var client = new HttpClient(new HttpClientHandler { UseCookies = false }); + client.BaseAddress = new Uri(navigationManager.Uri); + + // set the cookies to allow HttpClient API calls to be authenticated + var httpContextAccessor = s.GetRequiredService(); + foreach (var cookie in httpContextAccessor.HttpContext.Request.Cookies) + { + client.DefaultRequestHeaders.Add("Cookie", cookie.Key + "=" + cookie.Value); + } + + return client; + }); + } + + return services; + } + + internal static IServiceCollection TryAddSwagger(this IServiceCollection services, bool useSwagger) + { + if (useSwagger) + { + services.AddSwaggerGen(c => + { + c.SwaggerDoc("v1", new OpenApiInfo { Title = "Oqtane", Version = "v1" }); + }); + } + + return services; + } + private static IServiceCollection AddOqtaneServices(this IServiceCollection services, Runtime runtime) { if (services is null) diff --git a/Oqtane.Server/Repository/AliasRepository.cs b/Oqtane.Server/Repository/AliasRepository.cs index 619724dc..9e4e82d9 100644 --- a/Oqtane.Server/Repository/AliasRepository.cs +++ b/Oqtane.Server/Repository/AliasRepository.cs @@ -45,15 +45,28 @@ namespace Oqtane.Repository public Alias GetAlias(int aliasId) { - return _db.Alias.Find(aliasId); + return GetAlias(aliasId, true); } - public Alias GetAlias(string name) + public Alias GetAlias(int aliasId, bool tracking) + { + if (tracking) + { + return _db.Alias.Find(aliasId); + } + else + { + return _db.Alias.AsNoTracking().FirstOrDefault(item => item.AliasId == aliasId); + } + } + + // lookup alias based on url - note that alias values are hierarchical + public Alias GetAlias(string url) { Alias alias = null; List aliases = GetAliases().ToList(); - var segments = name.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries); + var segments = url.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries); // iterate segments to find keywords int start = segments.Length; diff --git a/Oqtane.Server/Repository/Interfaces/IAliasRepository.cs b/Oqtane.Server/Repository/Interfaces/IAliasRepository.cs index 5422ab07..81a42d68 100644 --- a/Oqtane.Server/Repository/Interfaces/IAliasRepository.cs +++ b/Oqtane.Server/Repository/Interfaces/IAliasRepository.cs @@ -9,7 +9,8 @@ namespace Oqtane.Repository Alias AddAlias(Alias alias); Alias UpdateAlias(Alias alias); Alias GetAlias(int aliasId); - Alias GetAlias(string name); + Alias GetAlias(int aliasId, bool tracking); + Alias GetAlias(string url); void DeleteAlias(int aliasId); } } diff --git a/Oqtane.Server/Repository/Interfaces/IJobRepository.cs b/Oqtane.Server/Repository/Interfaces/IJobRepository.cs index 12be85c1..e45b0231 100644 --- a/Oqtane.Server/Repository/Interfaces/IJobRepository.cs +++ b/Oqtane.Server/Repository/Interfaces/IJobRepository.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using Oqtane.Models; namespace Oqtane.Repository @@ -9,6 +9,7 @@ namespace Oqtane.Repository Job AddJob(Job job); Job UpdateJob(Job job); Job GetJob(int jobId); + Job GetJob(int jobId, bool tracking); void DeleteJob(int jobId); } } diff --git a/Oqtane.Server/Repository/JobRepository.cs b/Oqtane.Server/Repository/JobRepository.cs index cbe51c19..37b38521 100644 --- a/Oqtane.Server/Repository/JobRepository.cs +++ b/Oqtane.Server/Repository/JobRepository.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using Microsoft.EntityFrameworkCore; @@ -48,6 +48,19 @@ namespace Oqtane.Repository return _db.Job.Find(jobId); } + public Job GetJob(int jobId, bool tracking) + { + if (tracking) + { + return _db.Job.Find(jobId); + } + else + { + return _db.Job.AsNoTracking().FirstOrDefault(item => item.JobId == jobId); + } + + } + public void DeleteJob(int jobId) { Job job = _db.Job.Find(jobId); diff --git a/Oqtane.Server/Startup.cs b/Oqtane.Server/Startup.cs index 5faa9d06..d1fa176f 100644 --- a/Oqtane.Server/Startup.cs +++ b/Oqtane.Server/Startup.cs @@ -1,36 +1,29 @@ using System; using System.Collections.Generic; using System.IO; -using System.Linq; -using System.Net; -using System.Net.Http; -using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; -using Microsoft.OpenApi.Models; using Oqtane.Extensions; using Oqtane.Infrastructure; using Oqtane.Models; using Oqtane.Repository; using Oqtane.Security; -using Oqtane.Services; using Oqtane.Shared; namespace Oqtane { public class Startup { - private Runtime _runtime; - private bool _useSwagger; - private IWebHostEnvironment _env; - private string[] _supportedCultures; + private readonly Runtime _runtime; + private readonly bool _useSwagger; + private readonly IWebHostEnvironment _env; + private readonly string[] _supportedCultures; public IConfigurationRoot Configuration { get; } @@ -61,77 +54,24 @@ namespace Oqtane services.AddOptions>().Bind(Configuration.GetSection(SettingKeys.AvailableDatabasesSection)); - services.AddServerSideBlazor().AddCircuitOptions(options => - { - if (_env.IsDevelopment()) + services.AddServerSideBlazor() + .AddCircuitOptions(options => { - options.DetailedErrors = true; - } - }); + if (_env.IsDevelopment()) + { + options.DetailedErrors = true; + } + }); // setup HttpClient for server side in a client side compatible fashion ( with auth cookie ) - if (!services.Any(x => x.ServiceType == typeof(HttpClient))) - { - services.AddScoped(s => - { - // creating the URI helper needs to wait until the JS Runtime is initialized, so defer it. - var navigationManager = s.GetRequiredService(); - var client = new HttpClient(new HttpClientHandler { UseCookies = false }); - client.BaseAddress = new Uri(navigationManager.Uri); - - // set the cookies to allow HttpClient API calls to be authenticated - var httpContextAccessor = s.GetRequiredService(); - foreach (var cookie in httpContextAccessor.HttpContext.Request.Cookies) - { - client.DefaultRequestHeaders.Add("Cookie", cookie.Key + "=" + cookie.Value); - } - return client; - }); - } + services.TryAddHttpClientWithAuthenticationCookie(); // register custom authorization policies - services.AddAuthorizationCore(options => - { - options.AddPolicy(PolicyNames.ViewPage, policy => policy.Requirements.Add(new PermissionRequirement(EntityNames.Page, PermissionNames.View))); - options.AddPolicy(PolicyNames.EditPage, policy => policy.Requirements.Add(new PermissionRequirement(EntityNames.Page, PermissionNames.Edit))); - options.AddPolicy(PolicyNames.ViewModule, policy => policy.Requirements.Add(new PermissionRequirement(EntityNames.Module, PermissionNames.View))); - options.AddPolicy(PolicyNames.EditModule, policy => policy.Requirements.Add(new PermissionRequirement(EntityNames.Module, PermissionNames.Edit))); - options.AddPolicy(PolicyNames.ViewFolder, policy => policy.Requirements.Add(new PermissionRequirement(EntityNames.Folder, PermissionNames.View))); - options.AddPolicy(PolicyNames.EditFolder, policy => policy.Requirements.Add(new PermissionRequirement(EntityNames.Folder, PermissionNames.Edit))); - options.AddPolicy(PolicyNames.ListFolder, policy => policy.Requirements.Add(new PermissionRequirement(EntityNames.Folder, PermissionNames.Browse))); - }); + services.AddOqtaneAuthorizationPolicies(); // register scoped core services - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); + services.AddScoped() + .AddOqtaneScopedServices(); services.AddSingleton(); @@ -141,44 +81,12 @@ namespace Oqtane .AddDefaultTokenProviders() .AddClaimsPrincipalFactory>(); // role claims - services.Configure(options => - { - // Password settings - options.Password.RequireDigit = false; - options.Password.RequiredLength = 6; - options.Password.RequireNonAlphanumeric = false; - options.Password.RequireUppercase = false; - options.Password.RequireLowercase = false; - - // Lockout settings - options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(30); - options.Lockout.MaxFailedAccessAttempts = 10; - options.Lockout.AllowedForNewUsers = true; - - // User settings - options.User.RequireUniqueEmail = false; - }); + services.ConfigureOqtaneIdentityOptions(); services.AddAuthentication(Constants.AuthenticationScheme) .AddCookie(Constants.AuthenticationScheme); - 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.ConfigureOqtaneCookieOptions(); services.AddAntiforgery(options => { @@ -190,51 +98,18 @@ namespace Oqtane }); // register singleton scoped core services - services.AddSingleton(Configuration); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); + services.AddSingleton(Configuration) + .AddOqtaneSingletonServices(); // install any modules or themes ( this needs to occur BEFORE the assemblies are loaded into the app domain ) InstallationManager.InstallPackages(_env.WebRootPath, _env.ContentRootPath); // register transient scoped core services - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - // obsolete - replaced by ITenantManager - services.AddTransient(); + services.AddOqtaneTransientServices(); // load the external assemblies into the app domain, install services services.AddOqtane(_runtime, _supportedCultures); - services.AddDbContext(options => { }); - services.AddDbContext(options => { }); + services.AddOqtaneDbContext(); services.AddMvc() @@ -242,10 +117,7 @@ namespace Oqtane .AddOqtaneApplicationParts() // register any Controllers from custom modules .ConfigureOqtaneMvc(); // any additional configuration from IStart classes. - if (_useSwagger) - { - services.AddSwaggerGen(c => { c.SwaggerDoc("v1", new OpenApiInfo {Title = "Oqtane", Version = "v1"}); }); - } + services.TryAddSwagger(_useSwagger); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. diff --git a/README.md b/README.md index c36daac5..782b97c3 100644 --- a/README.md +++ b/README.md @@ -65,7 +65,6 @@ V.2.1.0 ( Jun 4, 2021 ) - [x] Centralize package installation and uninstall - [x] Enable pre-rendering support for Blazor Server - [x] Allow run-time installation of Language packages -- [x] Add support for Shared localization resources V.2.0.2 ( Apr 19, 2021 ) - [x] Assorted fixes and user experience improvements