@@ -100,8 +100,8 @@ public override List Resources => new List() { - new Resource { ResourceType = ResourceType.Stylesheet, Url = "https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css", Integrity = "sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC", CrossOrigin = "anonymous" }, + new Resource { ResourceType = ResourceType.Stylesheet, Url = "https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css", Integrity = "sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3", CrossOrigin = "anonymous" }, new Resource { ResourceType = ResourceType.Stylesheet, Url = ThemePath() + "Theme.css" }, - new Resource { ResourceType = ResourceType.Script, Bundle = "Bootstrap", Url = "https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js", Integrity = "sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM", CrossOrigin = "anonymous" } + new Resource { ResourceType = ResourceType.Script, Bundle = "Bootstrap", Url = "https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js", Integrity = "sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p", CrossOrigin = "anonymous" } }; } From b3967b36c003dbdff2f2614e0f09d739d76ba649 Mon Sep 17 00:00:00 2001 From: Leigh Pointer Date: Fri, 18 Feb 2022 14:52:51 +0100 Subject: [PATCH 02/68] Corrected incorrect CDN --- .../Themes/Templates/External/Client/Themes/Theme1.razor | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Oqtane.Server/wwwroot/Themes/Templates/External/Client/Themes/Theme1.razor b/Oqtane.Server/wwwroot/Themes/Templates/External/Client/Themes/Theme1.razor index 3fabac55..8f8f2456 100644 --- a/Oqtane.Server/wwwroot/Themes/Templates/External/Client/Themes/Theme1.razor +++ b/Oqtane.Server/wwwroot/Themes/Templates/External/Client/Themes/Theme1.razor @@ -98,10 +98,11 @@ public override string Panes => PaneNames.Admin + ",Top Full Width,Top 100%,Left 50%,Right 50%,Left 33%,Center 33%,Right 33%,Left Outer 25%,Left Inner 25%,Right Inner 25%,Right Outer 25%,Left 25%,Center 50%,Right 25%,Left Sidebar 66%,Right Sidebar 33%,Left Sidebar 33%,Right Sidebar 66%,Bottom 100%,Bottom Full Width"; - public override List Resources => new List() + public override List Resources => new List + () { - new Resource { ResourceType = ResourceType.Stylesheet, Url = "https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css", Integrity = "sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3", CrossOrigin = "anonymous" }, + new Resource { ResourceType = ResourceType.Stylesheet, Url = "https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.1.3/css/bootstrap.min.css", Integrity = "sha512-GQGU0fMMi238uA+a/bdWJfpUGKUkBdgfFdgBm72SUQ6BeyWjoY/ton0tEjH+OSH9iP4Dfh+7HM0I9f5eR0L/4w==", CrossOrigin = "anonymous" }, new Resource { ResourceType = ResourceType.Stylesheet, Url = ThemePath() + "Theme.css" }, - new Resource { ResourceType = ResourceType.Script, Bundle = "Bootstrap", Url = "https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js", Integrity = "sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p", CrossOrigin = "anonymous" } + new Resource { ResourceType = ResourceType.Script, Bundle = "Bootstrap", Url = "https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.1.3/js/bootstrap.bundle.min.js", Integrity = "sha512-pax4MlgXjHEPfCwcJLQhigY7+N8rt6bVvWLFyUMuxShv170X53TRzGPmPkZmGBhk+jikR8WBM4yl7A9WMHHqvg==", CrossOrigin = "anonymous" } }; } From b68e3cb10fcb6ec8676317430ec4516751d721e5 Mon Sep 17 00:00:00 2001 From: Shaun Walker Date: Sat, 19 Feb 2022 17:24:41 -0500 Subject: [PATCH 03/68] Add support for ES6 module JavaScript resources --- Oqtane.Client/Modules/ModuleBase.cs | 2 +- Oqtane.Client/Themes/ThemeBase.cs | 2 +- Oqtane.Server/wwwroot/js/interop.js | 3 +++ Oqtane.Shared/Models/Resource.cs | 8 ++++++-- 4 files changed, 11 insertions(+), 4 deletions(-) diff --git a/Oqtane.Client/Modules/ModuleBase.cs b/Oqtane.Client/Modules/ModuleBase.cs index f3f06aee..2efe7b42 100644 --- a/Oqtane.Client/Modules/ModuleBase.cs +++ b/Oqtane.Client/Modules/ModuleBase.cs @@ -55,7 +55,7 @@ namespace Oqtane.Modules var scripts = new List(); foreach (Resource resource in Resources.Where(item => item.ResourceType == ResourceType.Script && item.Declaration != ResourceDeclaration.Global)) { - scripts.Add(new { href = resource.Url, bundle = resource.Bundle ?? "", integrity = resource.Integrity ?? "", crossorigin = resource.CrossOrigin ?? "" }); + scripts.Add(new { href = resource.Url, bundle = resource.Bundle ?? "", integrity = resource.Integrity ?? "", crossorigin = resource.CrossOrigin ?? "", ismodule = resource.IsModule ?? false}); } if (scripts.Any()) { diff --git a/Oqtane.Client/Themes/ThemeBase.cs b/Oqtane.Client/Themes/ThemeBase.cs index ccf80e04..aa823628 100644 --- a/Oqtane.Client/Themes/ThemeBase.cs +++ b/Oqtane.Client/Themes/ThemeBase.cs @@ -34,7 +34,7 @@ namespace Oqtane.Themes var scripts = new List(); foreach (Resource resource in Resources.Where(item => item.ResourceType == ResourceType.Script && item.Declaration != ResourceDeclaration.Global)) { - scripts.Add(new { href = resource.Url, bundle = resource.Bundle ?? "", integrity = resource.Integrity ?? "", crossorigin = resource.CrossOrigin ?? "" }); + scripts.Add(new { href = resource.Url, bundle = resource.Bundle ?? "", integrity = resource.Integrity ?? "", crossorigin = resource.CrossOrigin ?? "", ismodule = resource.IsModule ?? false }); } if (scripts.Any()) { diff --git a/Oqtane.Server/wwwroot/js/interop.js b/Oqtane.Server/wwwroot/js/interop.js index dcecde3c..8d89c4c7 100644 --- a/Oqtane.Server/wwwroot/js/interop.js +++ b/Oqtane.Server/wwwroot/js/interop.js @@ -232,6 +232,9 @@ Oqtane.Interop = { if (path === scripts[s].href && scripts[s].crossorigin !== '') { element.crossOrigin = scripts[s].crossorigin; } + if (path === scripts[s].href && scripts[s].ismodule === true) { + element.type = "module"; + } } } }) diff --git a/Oqtane.Shared/Models/Resource.cs b/Oqtane.Shared/Models/Resource.cs index b87e32ec..42674075 100644 --- a/Oqtane.Shared/Models/Resource.cs +++ b/Oqtane.Shared/Models/Resource.cs @@ -33,8 +33,7 @@ namespace Oqtane.Models public string Bundle { get; set; } /// - /// Determines if the Resource is global, meaning that the entire solution uses it or just some modules. - /// TODO: VERIFY that this explanation is correct. + /// Determines if the Resource is global or local, meaning that the entire solution uses it or just some modules. /// public ResourceDeclaration Declaration { get; set; } @@ -42,5 +41,10 @@ namespace Oqtane.Models /// If the Resource should be included in the `head` of the HTML document or the `body` /// public ResourceLocation Location { get; set; } + + /// + /// For Scripts this allows type="module" registrations - not applicable to Stylesheets + /// + public bool? IsModule { get; set; } } } From 99986c1b94bceb87f64f564ae4e93bee3d47241f Mon Sep 17 00:00:00 2001 From: Shaun Walker Date: Sun, 20 Feb 2022 08:53:04 -0500 Subject: [PATCH 04/68] changed IsModule property name to ES6Module for clarity --- Oqtane.Client/Modules/ModuleBase.cs | 2 +- Oqtane.Client/Themes/ThemeBase.cs | 2 +- Oqtane.Server/wwwroot/js/interop.js | 2 +- Oqtane.Shared/Models/Resource.cs | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Oqtane.Client/Modules/ModuleBase.cs b/Oqtane.Client/Modules/ModuleBase.cs index 2efe7b42..1f4f060c 100644 --- a/Oqtane.Client/Modules/ModuleBase.cs +++ b/Oqtane.Client/Modules/ModuleBase.cs @@ -55,7 +55,7 @@ namespace Oqtane.Modules var scripts = new List(); foreach (Resource resource in Resources.Where(item => item.ResourceType == ResourceType.Script && item.Declaration != ResourceDeclaration.Global)) { - scripts.Add(new { href = resource.Url, bundle = resource.Bundle ?? "", integrity = resource.Integrity ?? "", crossorigin = resource.CrossOrigin ?? "", ismodule = resource.IsModule ?? false}); + scripts.Add(new { href = resource.Url, bundle = resource.Bundle ?? "", integrity = resource.Integrity ?? "", crossorigin = resource.CrossOrigin ?? "", es6module = resource.ES6Module }); } if (scripts.Any()) { diff --git a/Oqtane.Client/Themes/ThemeBase.cs b/Oqtane.Client/Themes/ThemeBase.cs index aa823628..883eb940 100644 --- a/Oqtane.Client/Themes/ThemeBase.cs +++ b/Oqtane.Client/Themes/ThemeBase.cs @@ -34,7 +34,7 @@ namespace Oqtane.Themes var scripts = new List(); foreach (Resource resource in Resources.Where(item => item.ResourceType == ResourceType.Script && item.Declaration != ResourceDeclaration.Global)) { - scripts.Add(new { href = resource.Url, bundle = resource.Bundle ?? "", integrity = resource.Integrity ?? "", crossorigin = resource.CrossOrigin ?? "", ismodule = resource.IsModule ?? false }); + scripts.Add(new { href = resource.Url, bundle = resource.Bundle ?? "", integrity = resource.Integrity ?? "", crossorigin = resource.CrossOrigin ?? "", es6module = resource.ES6Module }); } if (scripts.Any()) { diff --git a/Oqtane.Server/wwwroot/js/interop.js b/Oqtane.Server/wwwroot/js/interop.js index 8d89c4c7..e2122912 100644 --- a/Oqtane.Server/wwwroot/js/interop.js +++ b/Oqtane.Server/wwwroot/js/interop.js @@ -232,7 +232,7 @@ Oqtane.Interop = { if (path === scripts[s].href && scripts[s].crossorigin !== '') { element.crossOrigin = scripts[s].crossorigin; } - if (path === scripts[s].href && scripts[s].ismodule === true) { + if (path === scripts[s].href && scripts[s].es6module === true) { element.type = "module"; } } diff --git a/Oqtane.Shared/Models/Resource.cs b/Oqtane.Shared/Models/Resource.cs index 42674075..2a80ac56 100644 --- a/Oqtane.Shared/Models/Resource.cs +++ b/Oqtane.Shared/Models/Resource.cs @@ -45,6 +45,6 @@ namespace Oqtane.Models /// /// For Scripts this allows type="module" registrations - not applicable to Stylesheets /// - public bool? IsModule { get; set; } + public bool ES6Module { get; set; } } } From 3d0cbdd1a7cb8d80a5e5baefa65b97cbd45076ec Mon Sep 17 00:00:00 2001 From: Shaun Walker Date: Tue, 22 Feb 2022 10:01:52 -0500 Subject: [PATCH 05/68] expand Url column in Visitor and UrlMapping to accomodate maximum url size of 2048 characters --- ...03010001_ExpandVisitorAndUrlMappingUrls.cs | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 Oqtane.Server/Migrations/Tenant/03010001_ExpandVisitorAndUrlMappingUrls.cs diff --git a/Oqtane.Server/Migrations/Tenant/03010001_ExpandVisitorAndUrlMappingUrls.cs b/Oqtane.Server/Migrations/Tenant/03010001_ExpandVisitorAndUrlMappingUrls.cs new file mode 100644 index 00000000..947fc6ec --- /dev/null +++ b/Oqtane.Server/Migrations/Tenant/03010001_ExpandVisitorAndUrlMappingUrls.cs @@ -0,0 +1,52 @@ +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Oqtane.Databases.Interfaces; +using Oqtane.Migrations.EntityBuilders; +using Oqtane.Repository; + +namespace Oqtane.Migrations.Tenant +{ + [DbContext(typeof(TenantDBContext))] + [Migration("Tenant.03.01.00.01")] + public class ExpandVisitorAndUrlMappingUrls : MultiDatabaseMigration + { + public ExpandVisitorAndUrlMappingUrls(IDatabase database) : base(database) + { + } + + protected override void Up(MigrationBuilder migrationBuilder) + { + if (ActiveDatabase.Name != "Sqlite") + { + var visitorEntityBuilder = new VisitorEntityBuilder(migrationBuilder, ActiveDatabase); + visitorEntityBuilder.AlterStringColumn("Url", 2048); + + // Drop the index is needed because the Url is already associated with IX_UrlMapping + var urlMappingEntityBuilder = new UrlMappingEntityBuilder(migrationBuilder, ActiveDatabase); + urlMappingEntityBuilder.DropForeignKey("FK_UrlMapping_Site"); + urlMappingEntityBuilder.DropIndex("IX_UrlMapping"); + urlMappingEntityBuilder.AlterStringColumn("Url", 2048); + urlMappingEntityBuilder.AlterStringColumn("MappedUrl", 2048); + urlMappingEntityBuilder.AddIndex("IX_UrlMapping", new[] { "SiteId", "Url" }, true); + urlMappingEntityBuilder.AddForeignKey("FK_UrlMapping_Site"); + } + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + if (ActiveDatabase.Name != "Sqlite") + { + var visitorEntityBuilder = new VisitorEntityBuilder(migrationBuilder, ActiveDatabase); + visitorEntityBuilder.AlterStringColumn("Url", 500); + + var urlMappingEntityBuilder = new UrlMappingEntityBuilder(migrationBuilder, ActiveDatabase); + urlMappingEntityBuilder.DropForeignKey("FK_UrlMapping_Site"); + urlMappingEntityBuilder.DropIndex("IX_UrlMapping"); + urlMappingEntityBuilder.AlterStringColumn("Url", 500); + urlMappingEntityBuilder.AlterStringColumn("MappedUrl", 500); + urlMappingEntityBuilder.AddIndex("IX_UrlMapping", new[] { "SiteId", "Url" }, true); + urlMappingEntityBuilder.AddForeignKey("FK_UrlMapping_Site"); + } + } + } +} From 9ba356c47ef79dabbe279796a91c4430f9461b4d Mon Sep 17 00:00:00 2001 From: Shaun Walker Date: Tue, 22 Feb 2022 14:47:31 -0500 Subject: [PATCH 06/68] moved AlterStringColumn to IDatabase interface so that it can be overridden in the Sqlite provider rather than requiring conditional logic in the migrations --- Oqtane.Database.Sqlite/SqliteDatabase.cs | 5 +++ Oqtane.Server/Databases/DatabaseBase.cs | 5 +++ .../Databases/Interfaces/IDatabase.cs | 2 + .../EntityBuilders/BaseEntityBuilder.cs | 2 +- ...0001_ChangeFolderNameAndPathColumnsSize.cs | 35 +++++---------- .../03000101_ChangeFileNameColumnsSize.cs | 29 +++++-------- ...03010001_ExpandVisitorAndUrlMappingUrls.cs | 41 +++++++----------- .../Packages/Oqtane.Database.MySQL.nupkg.bak | Bin 701739 -> 701742 bytes .../Oqtane.Database.PostgreSQL.nupkg.bak | Bin 606188 -> 606188 bytes .../Oqtane.Database.SqlServer.nupkg.bak | Bin 181780 -> 181776 bytes .../Packages/Oqtane.Database.Sqlite.nupkg.bak | Bin 105505 -> 105567 bytes 11 files changed, 51 insertions(+), 68 deletions(-) diff --git a/Oqtane.Database.Sqlite/SqliteDatabase.cs b/Oqtane.Database.Sqlite/SqliteDatabase.cs index 8b91ffba..803bfc69 100644 --- a/Oqtane.Database.Sqlite/SqliteDatabase.cs +++ b/Oqtane.Database.Sqlite/SqliteDatabase.cs @@ -35,6 +35,11 @@ namespace Oqtane.Database.Sqlite // not implemented as SQLite does not support dropping columns } + public override void AlterStringColumn(MigrationBuilder builder, string name, string table, int length, bool nullable, bool unicode) + { + // not implemented as SQLite does not support altering columns + } + public override string ConcatenateSql(params string[] values) { var returnValue = String.Empty; diff --git a/Oqtane.Server/Databases/DatabaseBase.cs b/Oqtane.Server/Databases/DatabaseBase.cs index d9a25371..6a515a2a 100644 --- a/Oqtane.Server/Databases/DatabaseBase.cs +++ b/Oqtane.Server/Databases/DatabaseBase.cs @@ -81,6 +81,11 @@ namespace Oqtane.Databases builder.DropColumn(name, table); } + public virtual void AlterStringColumn(MigrationBuilder builder, string name, string table, int length, bool nullable, bool unicode) + { + builder.AlterColumn(RewriteName(name), RewriteName(table), maxLength: length, nullable: nullable, unicode: unicode); + } + public abstract DbContextOptionsBuilder UseDatabase(DbContextOptionsBuilder optionsBuilder, string connectionString); } } diff --git a/Oqtane.Server/Databases/Interfaces/IDatabase.cs b/Oqtane.Server/Databases/Interfaces/IDatabase.cs index 8d80e39e..e85a3bd0 100644 --- a/Oqtane.Server/Databases/Interfaces/IDatabase.cs +++ b/Oqtane.Server/Databases/Interfaces/IDatabase.cs @@ -34,6 +34,8 @@ namespace Oqtane.Databases.Interfaces public void DropColumn(MigrationBuilder builder, string name, string table); + public void AlterStringColumn(MigrationBuilder builder, string name, string table, int length, bool nullable, bool unicode); + public DbContextOptionsBuilder UseDatabase(DbContextOptionsBuilder optionsBuilder, string connectionString); } } diff --git a/Oqtane.Server/Migrations/EntityBuilders/BaseEntityBuilder.cs b/Oqtane.Server/Migrations/EntityBuilders/BaseEntityBuilder.cs index d9275734..24ded671 100644 --- a/Oqtane.Server/Migrations/EntityBuilders/BaseEntityBuilder.cs +++ b/Oqtane.Server/Migrations/EntityBuilders/BaseEntityBuilder.cs @@ -109,7 +109,7 @@ namespace Oqtane.Migrations.EntityBuilders public void AlterStringColumn(string name, int length, bool nullable = false, bool unicode = true) { - _migrationBuilder.AlterColumn(RewriteName(name), RewriteName(EntityTableName), maxLength: length, nullable: nullable, unicode: unicode); + ActiveDatabase.AlterStringColumn(_migrationBuilder, RewriteName(name), RewriteName(EntityTableName), length, nullable, unicode); } public void AddDecimalColumn(string name, int precision, int scale, bool nullable = false) diff --git a/Oqtane.Server/Migrations/Tenant/02010001_ChangeFolderNameAndPathColumnsSize.cs b/Oqtane.Server/Migrations/Tenant/02010001_ChangeFolderNameAndPathColumnsSize.cs index c19f3511..17d10d07 100644 --- a/Oqtane.Server/Migrations/Tenant/02010001_ChangeFolderNameAndPathColumnsSize.cs +++ b/Oqtane.Server/Migrations/Tenant/02010001_ChangeFolderNameAndPathColumnsSize.cs @@ -16,33 +16,22 @@ namespace Oqtane.Migrations.Tenant protected override void Up(MigrationBuilder migrationBuilder) { - if (ActiveDatabase.Name != "Sqlite") - { - var folderEntityBuilder = new FolderEntityBuilder(migrationBuilder, ActiveDatabase); - - folderEntityBuilder.AlterStringColumn("Name", 256); - - // Drop the index is needed because the Path is already associated with IX_Folder - folderEntityBuilder.DropForeignKey("FK_Folder_Site"); - folderEntityBuilder.DropIndex("IX_Folder"); - folderEntityBuilder.AlterStringColumn("Path", 512); - folderEntityBuilder.AddIndex("IX_Folder", new[] { "SiteId", "Path" }, true); - folderEntityBuilder.AddForeignKey("FK_Folder_Site"); - } + var folderEntityBuilder = new FolderEntityBuilder(migrationBuilder, ActiveDatabase); + // Drop the index is needed because the Path is already associated with IX_Folder + folderEntityBuilder.DropIndex("IX_Folder"); + folderEntityBuilder.AlterStringColumn("Name", 256); + folderEntityBuilder.AlterStringColumn("Path", 512); + folderEntityBuilder.AddIndex("IX_Folder", new[] { "SiteId", "Path" }, true); } protected override void Down(MigrationBuilder migrationBuilder) { - if (ActiveDatabase.Name != "Sqlite") - { - var folderEntityBuilder = new FolderEntityBuilder(migrationBuilder, ActiveDatabase); - - folderEntityBuilder.AlterStringColumn("Name", 50); - - folderEntityBuilder.DropIndex("IX_Folder"); - folderEntityBuilder.AlterStringColumn("Path", 50); - folderEntityBuilder.AddIndex("IX_Folder", new[] { "SiteId", "Path" }, true); - } + var folderEntityBuilder = new FolderEntityBuilder(migrationBuilder, ActiveDatabase); + // Drop the index is needed because the Path is already associated with IX_Folder + folderEntityBuilder.DropIndex("IX_Folder"); + folderEntityBuilder.AlterStringColumn("Path", 50); + folderEntityBuilder.AlterStringColumn("Name", 50); + folderEntityBuilder.AddIndex("IX_Folder", new[] { "SiteId", "Path" }, true); } } } diff --git a/Oqtane.Server/Migrations/Tenant/03000101_ChangeFileNameColumnsSize.cs b/Oqtane.Server/Migrations/Tenant/03000101_ChangeFileNameColumnsSize.cs index c1b898a0..4363b013 100644 --- a/Oqtane.Server/Migrations/Tenant/03000101_ChangeFileNameColumnsSize.cs +++ b/Oqtane.Server/Migrations/Tenant/03000101_ChangeFileNameColumnsSize.cs @@ -16,29 +16,20 @@ namespace Oqtane.Migrations.Tenant protected override void Up(MigrationBuilder migrationBuilder) { - if (ActiveDatabase.Name != "Sqlite") - { - var fileEntityBuilder = new FileEntityBuilder(migrationBuilder, ActiveDatabase); - - // Drop the index is needed because the Name is already associated with IX_File - fileEntityBuilder.DropForeignKey("FK_File_Folder"); - fileEntityBuilder.DropIndex("IX_File"); - fileEntityBuilder.AlterStringColumn("Name", 256); - fileEntityBuilder.AddIndex("IX_File", new[] { "FolderId", "Name" }, true); - fileEntityBuilder.AddForeignKey("FK_File_Folder"); - } + var fileEntityBuilder = new FileEntityBuilder(migrationBuilder, ActiveDatabase); + // Drop the index is needed because the Name is already associated with IX_File + fileEntityBuilder.DropIndex("IX_File"); + fileEntityBuilder.AlterStringColumn("Name", 256); + fileEntityBuilder.AddIndex("IX_File", new[] { "FolderId", "Name" }, true); } protected override void Down(MigrationBuilder migrationBuilder) { - if (ActiveDatabase.Name != "Sqlite") - { - var fileEntityBuilder = new FileEntityBuilder(migrationBuilder, ActiveDatabase); - - fileEntityBuilder.DropIndex("IX_File"); - fileEntityBuilder.AlterStringColumn("Name", 50); - fileEntityBuilder.AddIndex("IX_File", new[] { "FolderId", "Name" }, true); - } + var fileEntityBuilder = new FileEntityBuilder(migrationBuilder, ActiveDatabase); + // Drop the index is needed because the Name is already associated with IX_File + fileEntityBuilder.DropIndex("IX_File"); + fileEntityBuilder.AlterStringColumn("Name", 50); + fileEntityBuilder.AddIndex("IX_File", new[] { "FolderId", "Name" }, true); } } } diff --git a/Oqtane.Server/Migrations/Tenant/03010001_ExpandVisitorAndUrlMappingUrls.cs b/Oqtane.Server/Migrations/Tenant/03010001_ExpandVisitorAndUrlMappingUrls.cs index 947fc6ec..754a5b7a 100644 --- a/Oqtane.Server/Migrations/Tenant/03010001_ExpandVisitorAndUrlMappingUrls.cs +++ b/Oqtane.Server/Migrations/Tenant/03010001_ExpandVisitorAndUrlMappingUrls.cs @@ -16,37 +16,28 @@ namespace Oqtane.Migrations.Tenant protected override void Up(MigrationBuilder migrationBuilder) { - if (ActiveDatabase.Name != "Sqlite") - { - var visitorEntityBuilder = new VisitorEntityBuilder(migrationBuilder, ActiveDatabase); - visitorEntityBuilder.AlterStringColumn("Url", 2048); + var visitorEntityBuilder = new VisitorEntityBuilder(migrationBuilder, ActiveDatabase); + visitorEntityBuilder.AlterStringColumn("Url", 2048); - // Drop the index is needed because the Url is already associated with IX_UrlMapping - var urlMappingEntityBuilder = new UrlMappingEntityBuilder(migrationBuilder, ActiveDatabase); - urlMappingEntityBuilder.DropForeignKey("FK_UrlMapping_Site"); - urlMappingEntityBuilder.DropIndex("IX_UrlMapping"); - urlMappingEntityBuilder.AlterStringColumn("Url", 2048); - urlMappingEntityBuilder.AlterStringColumn("MappedUrl", 2048); - urlMappingEntityBuilder.AddIndex("IX_UrlMapping", new[] { "SiteId", "Url" }, true); - urlMappingEntityBuilder.AddForeignKey("FK_UrlMapping_Site"); - } + var urlMappingEntityBuilder = new UrlMappingEntityBuilder(migrationBuilder, ActiveDatabase); + // Drop the index is needed because the Url is already associated with IX_UrlMapping + urlMappingEntityBuilder.DropIndex("IX_UrlMapping"); + urlMappingEntityBuilder.AlterStringColumn("Url", 2048); + urlMappingEntityBuilder.AlterStringColumn("MappedUrl", 2048); + urlMappingEntityBuilder.AddIndex("IX_UrlMapping", new[] { "SiteId", "Url" }, true); } protected override void Down(MigrationBuilder migrationBuilder) { - if (ActiveDatabase.Name != "Sqlite") - { - var visitorEntityBuilder = new VisitorEntityBuilder(migrationBuilder, ActiveDatabase); - visitorEntityBuilder.AlterStringColumn("Url", 500); + var visitorEntityBuilder = new VisitorEntityBuilder(migrationBuilder, ActiveDatabase); + visitorEntityBuilder.AlterStringColumn("Url", 500); - var urlMappingEntityBuilder = new UrlMappingEntityBuilder(migrationBuilder, ActiveDatabase); - urlMappingEntityBuilder.DropForeignKey("FK_UrlMapping_Site"); - urlMappingEntityBuilder.DropIndex("IX_UrlMapping"); - urlMappingEntityBuilder.AlterStringColumn("Url", 500); - urlMappingEntityBuilder.AlterStringColumn("MappedUrl", 500); - urlMappingEntityBuilder.AddIndex("IX_UrlMapping", new[] { "SiteId", "Url" }, true); - urlMappingEntityBuilder.AddForeignKey("FK_UrlMapping_Site"); - } + var urlMappingEntityBuilder = new UrlMappingEntityBuilder(migrationBuilder, ActiveDatabase); + // Drop the index is needed because the Url is already associated with IX_UrlMapping + urlMappingEntityBuilder.DropIndex("IX_UrlMapping"); + urlMappingEntityBuilder.AlterStringColumn("Url", 500); + urlMappingEntityBuilder.AlterStringColumn("MappedUrl", 500); + urlMappingEntityBuilder.AddIndex("IX_UrlMapping", new[] { "SiteId", "Url" }, true); } } } diff --git a/Oqtane.Server/wwwroot/Packages/Oqtane.Database.MySQL.nupkg.bak b/Oqtane.Server/wwwroot/Packages/Oqtane.Database.MySQL.nupkg.bak index a4866e07e5160544c82567b8329bdecb906072e7..5cd839967bce9136bee7dfdf61091ab9635e2de6 100644 GIT binary patch delta 16412 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 T&mmJ0o8hpa^c&#*oASQ^(SF** delta 16409 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 UKp<&204!&x^Z#e~jR7F>FWoAzD*ylh delta 18978 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 delta 16852 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�vbB_WklmDt;7sb8l|H@ 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 diff --git a/Oqtane.Server/wwwroot/Packages/Oqtane.Database.Sqlite.nupkg.bak b/Oqtane.Server/wwwroot/Packages/Oqtane.Database.Sqlite.nupkg.bak index acf13f1c19d43e7991f741cf26c1f4d07470df77..5a9e5fbc6f68336c140763ff015dbaac5cc015b1 100644 GIT binary patch delta 16736 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

!)BZcNs&ybyf~8`>&0?Q6-Pe zAE0T_<+11duFc6UrUx0ie+N%_J`Az0A>8ghb*IGp0HnZ8x$5t(GBBD)Q^P81%`$sj<78~aD4PWB=z*C z?n;<0qUMVB5C}E2FMsuG?!@L$Y{8@OhgTd=1t5(;aud7k=ap)iM;HZUyOXkhZ0n@0 z(EB3uS7sNbEh1fD9UT&p9{maTN!SN}fX47?lAMkW_Q==>sp`|TyGr(;#ya+cHYf0z zXrAJ;mF$#Vf@7Y6`$PdfO=ElGy=VNjDn>&H$>E^qkYsMg?XhzgRN<02Ra?;Rp{Ca3Z4O{I4twkvVm3%#F1GTXRA zCdbF0QYWkQ zkuGMT$j;#_BzHqRFoB(Iv8Mtg2fydy3G$aSI+h1wq5t26AQ(+CM20~Da`@9AxpBPs zVuCnvgA>zUP~O?9y1CIY8qq`j-8^9gi`x+v+}lT4_JhrSA;;f>c@+J3X274ycK_$e zU(Va#0OZ!$aX$UxqbaoTkSjPj=@1Z*Mk3~c3Z(2#h4p-N<#_^Vk>b#CK}c$LlqKw~ zbOyUdn7{DOcaAES@e1~V0}^*R+VCX%R@GZf@kl<}u5e<=d*sAzj0^EuXb;|058ANQ z>~M)=R0aVX{RYB}@qz8hV*rEWP*4as7JOLBJ3mRst70N_iw>F(&lN?qKMvZ+MWGEt zqc+1c1VN5zV&l`uPR7;QG>M=ju<&DrPf&*>)!85{AU(+tb#*}`PKXQJoN@@Swj5g1`mazy*QJ zRYCtV&$rd50)qoX|IbG6Q+WDVlh&IC%u2{)YRJIE#KvUI%F4;Y#Kd64&SK2LXvA!2 zXkyC7Vq)Benhs0_3?K*Z@DZyCFfJdrEg`NW9Ezimw%TUm@$tV8Gj4g%AJ~VaC;1PM zaiUfYeTkpGeW;rhh?d%O;^xHZbJ!KA?q(D?g{=FM0)2xH)!6g@@oX-?f0>DS@?{D6 zIk%xrSyWkv=>;Ejwpp}@u1BQ#N|d2y@9Zz1Ugwb(Q1|A^51tJS7n$jGvl+z|LH6#? z;=6ersLCIBG=VURzO-qK(+^Bz6q$Z%HX{#G_}`z+D9W5SopJh!>5SU8gd#$(pg1=r Xxgfxsl?`M%2M{&_t?HTy^eY1ZJ9lcf From 073d330db4ff2c1bdd6b590c1cd8d9d55cbbcf56 Mon Sep 17 00:00:00 2001 From: Leigh Pointer Date: Wed, 23 Feb 2022 14:33:24 +0100 Subject: [PATCH 07/68] Fix for Module Settings Import and Export #2019 Added the module Settings so they are available for the Import and Export Interface. --- Oqtane.Server/Repository/ModuleRepository.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Oqtane.Server/Repository/ModuleRepository.cs b/Oqtane.Server/Repository/ModuleRepository.cs index 9637c4d1..bb974817 100644 --- a/Oqtane.Server/Repository/ModuleRepository.cs +++ b/Oqtane.Server/Repository/ModuleRepository.cs @@ -105,6 +105,7 @@ namespace Oqtane.Repository { try { + module.Settings = _settings.GetSettings(EntityNames.Module, moduleId).ToDictionary(x => x.SettingName, x => x.SettingValue); var moduleobject = ActivatorUtilities.CreateInstance(_serviceProvider, moduletype); modulecontent.Content = ((IPortable)moduleobject).ExportModule(module); } @@ -149,6 +150,7 @@ namespace Oqtane.Repository { try { + module.Settings = _settings.GetSettings(EntityNames.Module, moduleId).ToDictionary(x => x.SettingName, x => x.SettingValue); var moduleobject = ActivatorUtilities.CreateInstance(_serviceProvider, moduletype); ((IPortable)moduleobject).ImportModule(module, modulecontent.Content, modulecontent.Version); success = true; From ac45f67a2150bf2dc9da02f7eff3ce6eafb8e142 Mon Sep 17 00:00:00 2001 From: Shaun Walker Date: Wed, 23 Feb 2022 16:10:24 -0500 Subject: [PATCH 08/68] enhancement to send log notifications to host users --- Oqtane.Client/Modules/Admin/Login/Index.razor | 4 +-- .../Modules/Admin/SystemInfo/Index.razor | 18 ++++++++++ .../Modules/Admin/SystemInfo/Index.resx | 9 +++++ Oqtane.Server/Controllers/SystemController.cs | 4 +++ Oqtane.Server/Infrastructure/LogManager.cs | 33 +++++++++++++++++-- Oqtane.Shared/Enums/LogLevel.cs | 5 +-- 6 files changed, 66 insertions(+), 7 deletions(-) diff --git a/Oqtane.Client/Modules/Admin/Login/Index.razor b/Oqtane.Client/Modules/Admin/Login/Index.razor index 2bd43685..9a1eb15d 100644 --- a/Oqtane.Client/Modules/Admin/Login/Index.razor +++ b/Oqtane.Client/Modules/Admin/Login/Index.razor @@ -131,7 +131,7 @@ } else { - await logger.LogError(LogFunction.Security, "Login Failed For Username {Username}", _username); + await logger.LogInformation(LogFunction.Security, "Login Failed For Username {Username}", _username); AddModuleMessage(Localizer["Error.Login.Fail"], MessageType.Error); } } @@ -152,7 +152,7 @@ } else { - await logger.LogError(LogFunction.Security, "Login Failed For Username {Username}", _username); + await logger.LogInformation(LogFunction.Security, "Login Failed For Username {Username}", _username); AddModuleMessage(Localizer["Error.Login.Fail"], MessageType.Error); } } diff --git a/Oqtane.Client/Modules/Admin/SystemInfo/Index.razor b/Oqtane.Client/Modules/Admin/SystemInfo/Index.razor index ef09cd2a..0fe5c328 100644 --- a/Oqtane.Client/Modules/Admin/SystemInfo/Index.razor +++ b/Oqtane.Client/Modules/Admin/SystemInfo/Index.razor @@ -69,6 +69,21 @@ + + + + +

+ +
+
@@ -110,6 +125,7 @@ private string _detailederrors = string.Empty; private string _logginglevel = string.Empty; + private string _notificationlevel = string.Empty; private string _swagger = string.Empty; private string _packageservice = string.Empty; @@ -128,6 +144,7 @@ _detailederrors = systeminfo["detailederrors"]; _logginglevel = systeminfo["logginglevel"]; + _notificationlevel = systeminfo["notificationlevel"]; _swagger = systeminfo["swagger"]; _packageservice = systeminfo["packageservice"]; } @@ -140,6 +157,7 @@ var settings = new Dictionary(); settings.Add("detailederrors", _detailederrors); settings.Add("logginglevel", _logginglevel); + settings.Add("notificationlevel", _notificationlevel); settings.Add("swagger", _swagger); settings.Add("packageservice", _packageservice); await SystemService.UpdateSystemInfoAsync(settings); diff --git a/Oqtane.Client/Resources/Modules/Admin/SystemInfo/Index.resx b/Oqtane.Client/Resources/Modules/Admin/SystemInfo/Index.resx index e564fcf8..95a0fcc9 100644 --- a/Oqtane.Client/Resources/Modules/Admin/SystemInfo/Index.resx +++ b/Oqtane.Client/Resources/Modules/Admin/SystemInfo/Index.resx @@ -231,4 +231,13 @@ Restart Application + + None + + + The Minimum Logging Level For Which Notifications Should Be Sent To Host Users. + + + Notification Level: + \ No newline at end of file diff --git a/Oqtane.Server/Controllers/SystemController.cs b/Oqtane.Server/Controllers/SystemController.cs index 6ec0fe11..0991c60f 100644 --- a/Oqtane.Server/Controllers/SystemController.cs +++ b/Oqtane.Server/Controllers/SystemController.cs @@ -38,6 +38,7 @@ namespace Oqtane.Controllers systeminfo.Add("rendermode", _configManager.GetSetting("RenderMode", "ServerPrerendered")); systeminfo.Add("detailederrors", _configManager.GetSetting("DetailedErrors", "false")); systeminfo.Add("logginglevel", _configManager.GetSetting("Logging:LogLevel:Default", "Information")); + systeminfo.Add("notificationlevel", _configManager.GetSetting("Logging:LogLevel:Notify", "Error")); systeminfo.Add("swagger", _configManager.GetSetting("UseSwagger", "true")); systeminfo.Add("packageservice", _configManager.GetSetting("PackageService", "true")); @@ -64,6 +65,9 @@ namespace Oqtane.Controllers case "logginglevel": _configManager.AddOrUpdateSetting("Logging:LogLevel:Default", kvp.Value, false); break; + case "notificationlevel": + _configManager.AddOrUpdateSetting("Logging:LogLevel:Notify", kvp.Value, false); + break; case "swagger": _configManager.AddOrUpdateSetting("UseSwagger", kvp.Value, false); break; diff --git a/Oqtane.Server/Infrastructure/LogManager.cs b/Oqtane.Server/Infrastructure/LogManager.cs index e8f58854..5a1d9984 100644 --- a/Oqtane.Server/Infrastructure/LogManager.cs +++ b/Oqtane.Server/Infrastructure/LogManager.cs @@ -18,14 +18,18 @@ namespace Oqtane.Infrastructure private readonly IConfigManager _config; private readonly IUserPermissions _userPermissions; private readonly IHttpContextAccessor _accessor; + private readonly IUserRoleRepository _userRoles; + private readonly INotificationRepository _notifications; - public LogManager(ILogRepository logs, ITenantManager tenantManager, IConfigManager config, IUserPermissions userPermissions, IHttpContextAccessor accessor) + public LogManager(ILogRepository logs, ITenantManager tenantManager, IConfigManager config, IUserPermissions userPermissions, IHttpContextAccessor accessor, IUserRoleRepository userRoles, INotificationRepository notifications) { _logs = logs; _tenantManager = tenantManager; _config = config; _userPermissions = userPermissions; _accessor = accessor; + _userRoles = userRoles; + _notifications = notifications; } public void Log(LogLevel level, object @class, LogFunction function, string message, params object[] args) @@ -124,11 +128,11 @@ namespace Oqtane.Infrastructure try { _logs.AddLog(log); + SendNotification(log); } - catch (Exception ex) + catch { // an error occurred writing to the database - var x = ex.Message; } } } @@ -188,5 +192,28 @@ namespace Oqtane.Infrastructure } return log; } + + private void SendNotification(Log log) + { + LogLevel notifylevel = LogLevel.Error; + var section = _config.GetSection("Logging:LogLevel:Notify"); + if (section.Exists()) + { + notifylevel = Enum.Parse(section.Value); + } + if (Enum.Parse(log.Level) >= notifylevel) + { + foreach (var userrole in _userRoles.GetUserRoles(log.SiteId.Value)) + { + if (userrole.Role.Name == RoleNames.Host) + { + var url = _accessor.HttpContext.Request.Scheme + "://" + _tenantManager.GetAlias().Name + "/admin/log"; + var notification = new Notification(log.SiteId.Value, null, userrole.User, "Site " + log.Level + " Notification", "Please visit " + url + " for more information", null); + _notifications.AddNotification(notification); + } + } + + } + } } } diff --git a/Oqtane.Shared/Enums/LogLevel.cs b/Oqtane.Shared/Enums/LogLevel.cs index f772157e..79ba84c9 100644 --- a/Oqtane.Shared/Enums/LogLevel.cs +++ b/Oqtane.Shared/Enums/LogLevel.cs @@ -1,4 +1,4 @@ -namespace Oqtane.Shared +namespace Oqtane.Shared { public enum LogLevel { @@ -7,6 +7,7 @@ Information, Warning, Error, - Critical + Critical, + None } } From 938bcb2b620b11877d0bf1bd74844cc3b5072943 Mon Sep 17 00:00:00 2001 From: Shaun Walker Date: Thu, 24 Feb 2022 09:01:44 -0500 Subject: [PATCH 09/68] Added more constructors for convenience in creating Notification objects. Refactored to use the new constructors where applicable. Fixed localization key issue in Site Settings and added scroll to top when testing SMTP. --- Oqtane.Client/Modules/Admin/Site/Index.razor | 6 +- .../Modules/Admin/UserProfile/Add.razor | 2 +- Oqtane.Server/Controllers/UserController.cs | 4 +- Oqtane.Server/Infrastructure/LogManager.cs | 2 +- Oqtane.Shared/Models/Notification.cs | 76 ++++++++++++++----- 5 files changed, 65 insertions(+), 25 deletions(-) diff --git a/Oqtane.Client/Modules/Admin/Site/Index.razor b/Oqtane.Client/Modules/Admin/Site/Index.razor index 63f22032..13d51301 100644 --- a/Oqtane.Client/Modules/Admin/Site/Index.razor +++ b/Oqtane.Client/Modules/Admin/Site/Index.razor @@ -605,8 +605,10 @@ await SettingService.UpdateSiteSettingsAsync(settings, PageState.Site.SiteId); await logger.LogInformation("Site SMTP Settings Saved"); - await NotificationService.AddNotificationAsync(new Notification(PageState.Site.SiteId, PageState.User.DisplayName, PageState.User.Email, PageState.User.DisplayName, PageState.User.Email, PageState.Site.Name + " SMTP Configuration Test", "SMTP Server Is Configured Correctly.")); + await NotificationService.AddNotificationAsync(new Notification(PageState.Site.SiteId, PageState.User, PageState.Site.Name + " SMTP Configuration Test", "SMTP Server Is Configured Correctly.")); AddModuleMessage(Localizer["Info.Smtp.SaveSettings"], MessageType.Info); + var interop = new Interop(JSRuntime); + await interop.ScrollTo(0, 0, "smooth"); } catch (Exception ex) { @@ -616,7 +618,7 @@ } else { - AddModuleMessage(Localizer["Message.required.Smtp"], MessageType.Warning); + AddModuleMessage(Localizer["Message.Required.Smtp"], MessageType.Warning); } } diff --git a/Oqtane.Client/Modules/Admin/UserProfile/Add.razor b/Oqtane.Client/Modules/Admin/UserProfile/Add.razor index d04cc1b9..ceeb2713 100644 --- a/Oqtane.Client/Modules/Admin/UserProfile/Add.razor +++ b/Oqtane.Client/Modules/Admin/UserProfile/Add.razor @@ -49,7 +49,7 @@ var user = await UserService.GetUserAsync(username, PageState.Site.SiteId); if (user != null) { - var notification = new Notification(PageState.Site.SiteId, PageState.User, user, subject, body, null); + var notification = new Notification(PageState.Site.SiteId, PageState.User, user, subject, body); notification = await NotificationService.AddNotificationAsync(notification); await logger.LogInformation("Notification Created {NotificationId}", notification.NotificationId); NavigationManager.NavigateTo(NavigateUrl()); diff --git a/Oqtane.Server/Controllers/UserController.cs b/Oqtane.Server/Controllers/UserController.cs index 89efac3e..df6a804e 100644 --- a/Oqtane.Server/Controllers/UserController.cs +++ b/Oqtane.Server/Controllers/UserController.cs @@ -173,7 +173,7 @@ namespace Oqtane.Controllers string token = await _identityUserManager.GenerateEmailConfirmationTokenAsync(identityuser); string url = HttpContext.Request.Scheme + "://" + _alias.Name + "/login?name=" + user.Username + "&token=" + WebUtility.UrlEncode(token); string body = "Dear " + user.DisplayName + ",\n\nIn Order To Complete The Registration Of Your User Account Please Click The Link Displayed Below:\n\n" + url + "\n\nThank You!"; - var notification = new Notification(user.SiteId, null, newUser, "User Account Verification", body, null); + var notification = new Notification(user.SiteId, newUser, "User Account Verification", body); _notifications.AddNotification(notification); } @@ -428,7 +428,7 @@ namespace Oqtane.Controllers "\n\nIf you did not request to reset your password you can safely ignore this message." + "\n\nThank You!"; - var notification = new Notification(user.SiteId, null, user, "User Password Reset", body, null); + var notification = new Notification(user.SiteId, user, "User Password Reset", body); _notifications.AddNotification(notification); _logger.Log(LogLevel.Information, this, LogFunction.Security, "Password Reset Notification Sent For {Username}", user.Username); } diff --git a/Oqtane.Server/Infrastructure/LogManager.cs b/Oqtane.Server/Infrastructure/LogManager.cs index 5a1d9984..b75a69f0 100644 --- a/Oqtane.Server/Infrastructure/LogManager.cs +++ b/Oqtane.Server/Infrastructure/LogManager.cs @@ -208,7 +208,7 @@ namespace Oqtane.Infrastructure if (userrole.Role.Name == RoleNames.Host) { var url = _accessor.HttpContext.Request.Scheme + "://" + _tenantManager.GetAlias().Name + "/admin/log"; - var notification = new Notification(log.SiteId.Value, null, userrole.User, "Site " + log.Level + " Notification", "Please visit " + url + " for more information", null); + var notification = new Notification(log.SiteId.Value, userrole.User, "Site " + log.Level + " Notification", "Please visit " + url + " for more information"); _notifications.AddNotification(notification); } } diff --git a/Oqtane.Shared/Models/Notification.cs b/Oqtane.Shared/Models/Notification.cs index 64e0fbc7..975b28c8 100644 --- a/Oqtane.Shared/Models/Notification.cs +++ b/Oqtane.Shared/Models/Notification.cs @@ -94,9 +94,46 @@ namespace Oqtane.Models /// public DateTime? SendOn { get; set; } + + // constructors public Notification() {} + public Notification(int siteId, User to, string subject, string body) + { + ConstructNotification(siteId, null, "", "", to, "", "", subject, body, null, null); + } + + public Notification(int siteId, User to, string subject, string body, DateTime sendOn) + { + ConstructNotification(siteId, null, "", "", to, "", "", subject, body, null, sendOn); + } + + public Notification(int siteId, User from, User to, string subject, string body) + { + ConstructNotification(siteId, from, "", "", to, "", "", subject, body, null, null); + } + public Notification(int siteId, User from, User to, string subject, string body, int? parentId) + { + ConstructNotification(siteId, from, "", "", to, "", "", subject, body, parentId, null); + } + + public Notification(int siteId, string toDisplayName, string toEmail, string subject, string body) + { + ConstructNotification(siteId, null, "", "", null, toDisplayName, toEmail, subject, body, null, null); + } + + public Notification(int siteId, string toDisplayName, string toEmail, string subject, string body, DateTime sendOn) + { + ConstructNotification(siteId, null, "", "", null, toDisplayName, toEmail, subject, body, null, sendOn); + } + + public Notification(int siteId, string fromDisplayName, string fromEmail, string toDisplayName, string toEmail, string subject, string body) + { + ConstructNotification(siteId, null, fromDisplayName, fromEmail, null, toDisplayName, toEmail, subject, body, null, null); + } + + private void ConstructNotification(int siteId, User from, string fromDisplayName, string fromEmail, User to, string toDisplayName, string toEmail, string subject, string body, int? parentId, DateTime? sendOn) { SiteId = siteId; if (from != null) @@ -105,37 +142,38 @@ namespace Oqtane.Models FromDisplayName = from.DisplayName; FromEmail = from.Email; } + else + { + FromUserId = null; + FromDisplayName = fromDisplayName; + FromEmail = fromEmail; + } if (to != null) { ToUserId = to.UserId; ToDisplayName = to.DisplayName; ToEmail = to.Email; } + else + { + ToUserId = null; + ToDisplayName = toDisplayName; + ToEmail = toEmail; + } Subject = subject; Body = body; ParentId = parentId; CreatedOn = DateTime.UtcNow; + if (sendOn != null) + { + SendOn = sendOn; + } + else + { + SendOn = CreatedOn; + } IsDelivered = false; DeliveredOn = null; - SendOn = DateTime.UtcNow; - } - - public Notification(int siteId, string fromDisplayName, string fromEmail, string toDisplayName, string toEmail, string subject, string body) - { - SiteId = siteId; - FromUserId = null; - FromDisplayName = fromDisplayName; - FromEmail = fromEmail; - ToUserId = null; - ToDisplayName = toDisplayName; - ToEmail = toEmail; - Subject = subject; - Body = body; - ParentId = null; - CreatedOn = DateTime.UtcNow; - IsDelivered = false; - DeliveredOn = null; - SendOn = DateTime.UtcNow; } } From 15fdba060ce749c3a1d82c834f8566d204c03df7 Mon Sep 17 00:00:00 2001 From: Shaun Walker Date: Thu, 24 Feb 2022 09:55:34 -0500 Subject: [PATCH 10/68] Improvements to System Upgrade to preserve the processing details in a log file in the /Packages folder to improve troubleshooting abilities --- Oqtane.Updater/Program.cs | 58 +++++++++++++++++++++++++++------------ 1 file changed, 40 insertions(+), 18 deletions(-) diff --git a/Oqtane.Updater/Program.cs b/Oqtane.Updater/Program.cs index 59a19de0..c7c3746b 100644 --- a/Oqtane.Updater/Program.cs +++ b/Oqtane.Updater/Program.cs @@ -25,11 +25,16 @@ namespace Oqtane.Updater { string contentrootfolder = args[0]; string webrootfolder = args[1]; + string deployfolder = Path.Combine(contentrootfolder, "Packages"); string backupfolder = Path.Combine(contentrootfolder, "Backup"); if (Directory.Exists(deployfolder)) { + string log = "Upgrade Process Started: " + DateTime.UtcNow.ToString() + Environment.NewLine; + log += "ContentRootPath: " + contentrootfolder + Environment.NewLine; + log += "WebRootPath: " + webrootfolder + Environment.NewLine; + string packagename = ""; string[] packages = Directory.GetFiles(deployfolder, "Oqtane.Framework.*.Upgrade.zip"); if (packages.Length > 0) @@ -37,15 +42,15 @@ namespace Oqtane.Updater packagename = packages[packages.Length - 1]; // use highest version } - if (packagename != "") + if (packagename != "" && File.Exists(Path.Combine(webrootfolder, "app_offline.bak"))) { - // take the app offline - if (File.Exists(Path.Combine(webrootfolder, "app_offline.bak"))) - { - File.Copy(Path.Combine(webrootfolder, "app_offline.bak"), Path.Combine(contentrootfolder, "app_offline.htm"), true); - } + log += "Located Upgrade Package: " + packagename + Environment.NewLine; + + log += "Stopping Application Using: " + Path.Combine(contentrootfolder, "app_offline.htm") + Environment.NewLine; + File.Copy(Path.Combine(webrootfolder, "app_offline.bak"), Path.Combine(contentrootfolder, "app_offline.htm"), true); // get list of files in package with local paths + log += "Retrieving List Of Files From Upgrade Package..." + Environment.NewLine; List files = new List(); using (ZipArchive archive = ZipFile.OpenRead(packagename)) { @@ -61,6 +66,7 @@ namespace Oqtane.Updater // ensure files are not locked if (CanAccessFiles(files)) { + log += "Preparing Backup Folder: " + backupfolder + Environment.NewLine; bool success = true; try { @@ -73,13 +79,14 @@ namespace Oqtane.Updater } catch (Exception ex) { - Console.WriteLine(ex.Message); + log += "Error Creating Backup Folder: " + ex.Message + Environment.NewLine; success = false; } // backup files if (success) { + log += "Backing Up Files..." + Environment.NewLine; foreach (string file in files) { string filename = Path.Combine(backupfolder, file.Replace(contentrootfolder + Path.DirectorySeparatorChar, "")); @@ -96,7 +103,7 @@ namespace Oqtane.Updater } catch (Exception ex) { - Console.WriteLine(ex.Message); + log += "Error Backing Up Files: " + ex.Message + Environment.NewLine; success = false; } } @@ -105,6 +112,7 @@ namespace Oqtane.Updater // extract files if (success) { + log += "Extracting Files From Upgrade Package..." + Environment.NewLine; try { using (ZipArchive archive = ZipFile.OpenRead(packagename)) @@ -125,20 +133,28 @@ namespace Oqtane.Updater } catch (Exception ex) { - // an error occurred extracting a file success = false; - Console.WriteLine("Update Not Successful: Error Extracting Files From Package - " + ex.Message); + log += "Error Extracting Files From Upgrade Package: " + ex.Message + Environment.NewLine; } if (success) { - // clean up backup - Directory.Delete(backupfolder, true); - // delete package - File.Delete(packagename); + log += "Removing Backup Folder..." + Environment.NewLine; + try + { + // clean up backup + Directory.Delete(backupfolder, true); + // delete package + File.Delete(packagename); + } + catch (Exception ex) + { + log += "Error Removing Backup Folder: " + ex.Message + Environment.NewLine; + } } else { + log += "Restoring Files From Backup Folder..." + Environment.NewLine; try { // restore on failure @@ -156,30 +172,36 @@ namespace Oqtane.Updater } catch (Exception ex) { - Console.WriteLine("Update Not Successful: Error Restoring Files - " + ex.Message); + log += "Error Restoring Files From Backup Folder: " + ex.Message + Environment.NewLine; } } } else { - Console.WriteLine("Update Not Successful: Could Not Backup All Existing Files"); + log += "Upgrade Failed: Could Not Backup Files" + Environment.NewLine; } } else { - Console.WriteLine("Upgrade Not Successful: Some Files Are Locked"); + log += "Upgrade Failed: Some Files Are Locked By The Hosting Environment" + Environment.NewLine; } // bring the app back online if (File.Exists(Path.Combine(contentrootfolder, "app_offline.htm"))) { + log += "Restarting Application By Removing: " + Path.Combine(contentrootfolder, "app_offline.htm") + Environment.NewLine; File.Delete(Path.Combine(contentrootfolder, "app_offline.htm")); } } else { - Console.WriteLine("Framework Upgrade Package Not Found"); + log += "Framework Upgrade Package Not Found Or " + Path.Combine(webrootfolder, "app_offline.bak") + " Does Not Exist" + Environment.NewLine; } + + log += "Upgrade Process Ended: " + DateTime.UtcNow.ToString() + Environment.NewLine; + + // create upgrade log file + File.WriteAllText(Path.Combine(deployfolder, "Oqtane.Framework.Upgrade." + DateTime.UtcNow.ToString("yyyyMMddHHmm") + ".log"), log); } else { From 82fef82c4f29115a3c9f2ffefbae355ce24605e6 Mon Sep 17 00:00:00 2001 From: Shaun Walker Date: Thu, 24 Feb 2022 11:11:15 -0500 Subject: [PATCH 11/68] use consistent naming convention for System Update log file --- Oqtane.Updater/Program.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Oqtane.Updater/Program.cs b/Oqtane.Updater/Program.cs index c7c3746b..22c0b4d9 100644 --- a/Oqtane.Updater/Program.cs +++ b/Oqtane.Updater/Program.cs @@ -201,7 +201,12 @@ namespace Oqtane.Updater log += "Upgrade Process Ended: " + DateTime.UtcNow.ToString() + Environment.NewLine; // create upgrade log file - File.WriteAllText(Path.Combine(deployfolder, "Oqtane.Framework.Upgrade." + DateTime.UtcNow.ToString("yyyyMMddHHmm") + ".log"), log); + string logfile = Path.Combine(deployfolder, Path.GetFileNameWithoutExtension(packagename) + ".log"); + if (File.Exists(logfile)) + { + File.Delete(logfile); + } + File.WriteAllText(logfile, log); } else { From 0fba385b9e73bea4d534f95ecadbd95a55cf32dc Mon Sep 17 00:00:00 2001 From: Shaun Walker Date: Thu, 24 Feb 2022 12:37:06 -0500 Subject: [PATCH 12/68] Enhanced Purge Job to include retention policy for Notifications --- Oqtane.Client/Modules/Admin/Site/Index.razor | 21 +++++++++++++------ Oqtane.Server/Infrastructure/Jobs/PurgeJob.cs | 21 +++++++++++++++++-- .../Repository/Interfaces/ILogRepository.cs | 2 +- .../Interfaces/INotificationRepository.cs | 1 + .../Interfaces/IVisitorRepository.cs | 2 +- Oqtane.Server/Repository/LogRepository.cs | 6 +++--- .../Repository/NotificationRepository.cs | 20 ++++++++++++++++++ Oqtane.Server/Repository/VisitorRepository.cs | 6 +++--- 8 files changed, 63 insertions(+), 16 deletions(-) diff --git a/Oqtane.Client/Modules/Admin/Site/Index.razor b/Oqtane.Client/Modules/Admin/Site/Index.razor index 13d51301..a83a71d5 100644 --- a/Oqtane.Client/Modules/Admin/Site/Index.razor +++ b/Oqtane.Client/Modules/Admin/Site/Index.razor @@ -129,6 +129,12 @@ +
+ +
+ +
+


@@ -261,6 +267,7 @@ private string _smtpusername = string.Empty; private string _smtppassword = string.Empty; private string _smtpsender = string.Empty; + private string _retention = string.Empty; private string _pwaisenabled; private int _pwaappiconfileid = -1; private FileManager _pwaappiconfilemanager; @@ -330,6 +337,7 @@ _smtpusername = SettingService.GetSetting(settings, "SMTPUsername", string.Empty); _smtppassword = SettingService.GetSetting(settings, "SMTPPassword", string.Empty); _smtpsender = SettingService.GetSetting(settings, "SMTPSender", string.Empty); + _retention = SettingService.GetSetting(settings, "NotificationRetention", "30"); if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host)) { @@ -479,12 +487,13 @@ site = await SiteService.UpdateSiteAsync(site); var settings = await SettingService.GetSiteSettingsAsync(site.SiteId); - SettingService.SetSetting(settings, "SMTPHost", _smtphost, true); - SettingService.SetSetting(settings, "SMTPPort", _smtpport, true); - SettingService.SetSetting(settings, "SMTPSSL", _smtpssl, true); - SettingService.SetSetting(settings, "SMTPUsername", _smtpusername, true); - SettingService.SetSetting(settings, "SMTPPassword", _smtppassword, true); - SettingService.SetSetting(settings, "SMTPSender", _smtpsender, true); + settings = SettingService.SetSetting(settings, "SMTPHost", _smtphost, true); + settings = SettingService.SetSetting(settings, "SMTPPort", _smtpport, true); + settings = SettingService.SetSetting(settings, "SMTPSSL", _smtpssl, true); + settings = SettingService.SetSetting(settings, "SMTPUsername", _smtpusername, true); + settings = SettingService.SetSetting(settings, "SMTPPassword", _smtppassword, true); + settings = SettingService.SetSetting(settings, "SMTPSender", _smtpsender, true); + settings = SettingService.SetSetting(settings, "NotificationRetention", _retention, true); await SettingService.UpdateSiteSettingsAsync(settings, site.SiteId); if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host)) diff --git a/Oqtane.Server/Infrastructure/Jobs/PurgeJob.cs b/Oqtane.Server/Infrastructure/Jobs/PurgeJob.cs index a73bef98..8eb22a97 100644 --- a/Oqtane.Server/Infrastructure/Jobs/PurgeJob.cs +++ b/Oqtane.Server/Infrastructure/Jobs/PurgeJob.cs @@ -31,6 +31,7 @@ namespace Oqtane.Infrastructure var settingRepository = provider.GetRequiredService(); var logRepository = provider.GetRequiredService(); var visitorRepository = provider.GetRequiredService(); + var notificationRepository = provider.GetRequiredService(); // iterate through sites for current tenant List sites = siteRepository.GetSites().ToList(); @@ -51,7 +52,7 @@ namespace Oqtane.Infrastructure } try { - count = logRepository.DeleteLogs(retention); + count = logRepository.DeleteLogs(site.SiteId, retention); log += count.ToString() + " Events Purged
"; } catch (Exception ex) @@ -69,7 +70,7 @@ namespace Oqtane.Infrastructure } try { - count = visitorRepository.DeleteVisitors(retention); + count = visitorRepository.DeleteVisitors(site.SiteId, retention); log += count.ToString() + " Visitors Purged
"; } catch (Exception ex) @@ -77,6 +78,22 @@ namespace Oqtane.Infrastructure log += $"Error Purging Visitors - {ex.Message}
"; } } + + // purge notifications + retention = 30; // 30 days + if (settings.ContainsKey("NotificationRetention") && !string.IsNullOrEmpty(settings["NotificationRetention"])) + { + retention = int.Parse(settings["NotificationRetention"]); + } + try + { + count = notificationRepository.DeleteNotifications(site.SiteId, retention); + log += count.ToString() + " Notifications Purged
"; + } + catch (Exception ex) + { + log += $"Error Purging Notifications - {ex.Message}
"; + } } return log; diff --git a/Oqtane.Server/Repository/Interfaces/ILogRepository.cs b/Oqtane.Server/Repository/Interfaces/ILogRepository.cs index cc2c8aae..918785f2 100644 --- a/Oqtane.Server/Repository/Interfaces/ILogRepository.cs +++ b/Oqtane.Server/Repository/Interfaces/ILogRepository.cs @@ -8,6 +8,6 @@ namespace Oqtane.Repository IEnumerable GetLogs(int siteId, string level, string function, int rows); Log GetLog(int logId); void AddLog(Log log); - int DeleteLogs(int age); + int DeleteLogs(int siteId, int age); } } diff --git a/Oqtane.Server/Repository/Interfaces/INotificationRepository.cs b/Oqtane.Server/Repository/Interfaces/INotificationRepository.cs index 5dfb81b6..34fb58be 100644 --- a/Oqtane.Server/Repository/Interfaces/INotificationRepository.cs +++ b/Oqtane.Server/Repository/Interfaces/INotificationRepository.cs @@ -11,5 +11,6 @@ namespace Oqtane.Repository Notification GetNotification(int notificationId); Notification GetNotification(int notificationId, bool tracking); void DeleteNotification(int notificationId); + int DeleteNotifications(int siteId, int age); } } diff --git a/Oqtane.Server/Repository/Interfaces/IVisitorRepository.cs b/Oqtane.Server/Repository/Interfaces/IVisitorRepository.cs index d73a02ed..9100e0b8 100644 --- a/Oqtane.Server/Repository/Interfaces/IVisitorRepository.cs +++ b/Oqtane.Server/Repository/Interfaces/IVisitorRepository.cs @@ -12,6 +12,6 @@ namespace Oqtane.Repository Visitor GetVisitor(int visitorId); Visitor GetVisitor(int siteId, string IPAddress); void DeleteVisitor(int visitorId); - int DeleteVisitors(int age); + int DeleteVisitors(int siteId, int age); } } diff --git a/Oqtane.Server/Repository/LogRepository.cs b/Oqtane.Server/Repository/LogRepository.cs index 24920d42..8f8363ac 100644 --- a/Oqtane.Server/Repository/LogRepository.cs +++ b/Oqtane.Server/Repository/LogRepository.cs @@ -49,19 +49,19 @@ namespace Oqtane.Repository _db.SaveChanges(); } - public int DeleteLogs(int age) + public int DeleteLogs(int siteId, int age) { // delete logs in batches of 100 records int count = 0; var purgedate = DateTime.UtcNow.AddDays(-age); - var logs = _db.Log.Where(item => item.Level != "Error" && item.LogDate < purgedate) + var logs = _db.Log.Where(item => item.SiteId == siteId && item.Level != "Error" && item.LogDate < purgedate) .OrderBy(item => item.LogDate).Take(100).ToList(); while (logs.Count > 0) { count += logs.Count; _db.Log.RemoveRange(logs); _db.SaveChanges(); - logs = _db.Log.Where(item => item.Level != "Error" && item.LogDate < purgedate) + logs = _db.Log.Where(item => item.SiteId == siteId && item.Level != "Error" && item.LogDate < purgedate) .OrderBy(item => item.LogDate).Take(100).ToList(); } return count; diff --git a/Oqtane.Server/Repository/NotificationRepository.cs b/Oqtane.Server/Repository/NotificationRepository.cs index aa05bf17..7596ee94 100644 --- a/Oqtane.Server/Repository/NotificationRepository.cs +++ b/Oqtane.Server/Repository/NotificationRepository.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Linq; using Microsoft.EntityFrameworkCore; @@ -68,6 +69,25 @@ namespace Oqtane.Repository _db.Notification.Remove(notification); _db.SaveChanges(); } + + public int DeleteNotifications(int siteId, int age) + { + // delete notifications in batches of 100 records + int count = 0; + var purgedate = DateTime.UtcNow.AddDays(-age); + var notifications = _db.Notification.Where(item => item.SiteId == siteId && item.FromUserId == null && item.IsDelivered && item.DeliveredOn < purgedate) + .OrderBy(item => item.DeliveredOn).Take(100).ToList(); + while (notifications.Count > 0) + { + count += notifications.Count; + _db.Notification.RemoveRange(notifications); + _db.SaveChanges(); + notifications = _db.Notification.Where(item => item.SiteId == siteId && item.FromUserId == null && item.IsDelivered && item.DeliveredOn < purgedate) + .OrderBy(item => item.DeliveredOn).Take(100).ToList(); + } + return count; + } + } } diff --git a/Oqtane.Server/Repository/VisitorRepository.cs b/Oqtane.Server/Repository/VisitorRepository.cs index 2f7c10dc..866d61f9 100644 --- a/Oqtane.Server/Repository/VisitorRepository.cs +++ b/Oqtane.Server/Repository/VisitorRepository.cs @@ -53,19 +53,19 @@ namespace Oqtane.Repository _db.SaveChanges(); } - public int DeleteVisitors(int age) + public int DeleteVisitors(int siteId, int age) { // delete visitors in batches of 100 records int count = 0; var purgedate = DateTime.UtcNow.AddDays(-age); - var visitors = _db.Visitor.Where(item => item.Visits <= 1 && item.VisitedOn < purgedate) + var visitors = _db.Visitor.Where(item => item.SiteId == siteId && item.Visits < 2 && item.VisitedOn < purgedate) .OrderBy(item => item.VisitedOn).Take(100).ToList(); while (visitors.Count > 0) { count += visitors.Count; _db.Visitor.RemoveRange(visitors); _db.SaveChanges(); - visitors = _db.Visitor.Where(item => item.Visits < 2 && item.VisitedOn < purgedate) + visitors = _db.Visitor.Where(item => item.SiteId == siteId && item.Visits < 2 && item.VisitedOn < purgedate) .OrderBy(item => item.VisitedOn).Take(100).ToList(); } return count; From eb1ac3bc9b16d43364db21969d212e6a5042f87d Mon Sep 17 00:00:00 2001 From: Shaun Walker Date: Fri, 25 Feb 2022 16:17:54 -0500 Subject: [PATCH 13/68] Added support for User Account Lockout --- Oqtane.Server/Controllers/UserController.cs | 19 +++++++++++++++++-- Oqtane.Server/Pages/Login.cshtml.cs | 2 +- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/Oqtane.Server/Controllers/UserController.cs b/Oqtane.Server/Controllers/UserController.cs index df6a804e..2d10be0f 100644 --- a/Oqtane.Server/Controllers/UserController.cs +++ b/Oqtane.Server/Controllers/UserController.cs @@ -339,7 +339,7 @@ namespace Oqtane.Controllers IdentityUser identityuser = await _identityUserManager.FindByNameAsync(user.Username); if (identityuser != null) { - var result = await _identitySignInManager.CheckPasswordSignInAsync(identityuser, user.Password, false); + var result = await _identitySignInManager.CheckPasswordSignInAsync(identityuser, user.Password, true); if (result.Succeeded) { loginUser = _users.GetUser(identityuser.UserName); @@ -365,7 +365,22 @@ namespace Oqtane.Controllers } else { - _logger.Log(LogLevel.Error, this, LogFunction.Security, "User Login Failed {Username}", user.Username); + if (result.IsLockedOut) + { + user = _users.GetUser(user.Username); + string token = await _identityUserManager.GeneratePasswordResetTokenAsync(identityuser); + string url = HttpContext.Request.Scheme + "://" + _alias.Name + "/reset?name=" + user.Username + "&token=" + WebUtility.UrlEncode(token); + string body = "Dear " + user.DisplayName + ",\n\nYou attempted 3 times unsuccessfully to login to your account and it is now locked out. Please wait 10 minutes and then try again... or use the link below to reset your password:\n\n" + url + + "\n\nPlease note that the link is only valid for 24 hours so if you are unable to take action within that time period, you should initiate another password reset on the site." + + "\n\nThank You!"; + var notification = new Notification(user.SiteId, user, "User Password Lockout", body); + _notifications.AddNotification(notification); + _logger.Log(LogLevel.Information, this, LogFunction.Security, "Password Lockout Notification Sent For {Username}", user.Username); + } + else + { + _logger.Log(LogLevel.Error, this, LogFunction.Security, "User Login Failed {Username}", user.Username); + } } } } diff --git a/Oqtane.Server/Pages/Login.cshtml.cs b/Oqtane.Server/Pages/Login.cshtml.cs index b1a8a66f..345a4bd8 100644 --- a/Oqtane.Server/Pages/Login.cshtml.cs +++ b/Oqtane.Server/Pages/Login.cshtml.cs @@ -27,7 +27,7 @@ namespace Oqtane.Pages IdentityUser identityuser = await _identityUserManager.FindByNameAsync(username); if (identityuser != null) { - var result = await _identitySignInManager.CheckPasswordSignInAsync(identityuser, password, false); + var result = await _identitySignInManager.CheckPasswordSignInAsync(identityuser, password, true); if (result.Succeeded) { validuser = true; From 19f180331bf6a4adee0dfb73661525f1c21c23b8 Mon Sep 17 00:00:00 2001 From: Shaun Walker Date: Mon, 28 Feb 2022 15:58:49 -0500 Subject: [PATCH 14/68] Adding 2 factor authentication --- Oqtane.Shared/Models/User.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Oqtane.Shared/Models/User.cs b/Oqtane.Shared/Models/User.cs index 8e7a83af..b38d1db5 100644 --- a/Oqtane.Shared/Models/User.cs +++ b/Oqtane.Shared/Models/User.cs @@ -97,5 +97,11 @@ namespace Oqtane.Models { get => "Users\\" + UserId.ToString() + "\\"; } + + /// + /// Indicates if the user requires 2 factor authentication to sign in + /// + [NotMapped] + public bool TwoFactorEnabled { get; set; } } } From 28629aa836ea8f42fa0b47f0e6c9d148a77325b6 Mon Sep 17 00:00:00 2001 From: Shaun Walker Date: Mon, 28 Feb 2022 16:00:52 -0500 Subject: [PATCH 15/68] Adding 2 factor authentication --- Oqtane.Server/Controllers/UserController.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/Oqtane.Server/Controllers/UserController.cs b/Oqtane.Server/Controllers/UserController.cs index 2d10be0f..94e58881 100644 --- a/Oqtane.Server/Controllers/UserController.cs +++ b/Oqtane.Server/Controllers/UserController.cs @@ -247,14 +247,15 @@ namespace Oqtane.Controllers { if (ModelState.IsValid && user.SiteId == _alias.SiteId && _users.GetUser(user.UserId, false) != null && (User.IsInRole(RoleNames.Admin) || User.Identity.Name == user.Username)) { - if (user.Password != "") + IdentityUser identityuser = await _identityUserManager.FindByNameAsync(user.Username); + if (identityuser != null) { - IdentityUser identityuser = await _identityUserManager.FindByNameAsync(user.Username); - if (identityuser != null) + identityuser.TwoFactorEnabled = user.TwoFactorEnabled; + if (user.Password != "") { identityuser.PasswordHash = _identityUserManager.PasswordHasher.HashPassword(identityuser, user.Password); - await _identityUserManager.UpdateAsync(identityuser); } + await _identityUserManager.UpdateAsync(identityuser); } user = _users.UpdateUser(user); _syncManager.AddSyncEvent(_alias.TenantId, EntityNames.User, user.UserId); From 1cdc80e09b37299c7356c4dea959d0c88f4909a1 Mon Sep 17 00:00:00 2001 From: Shaun Walker Date: Thu, 3 Mar 2022 09:12:37 -0500 Subject: [PATCH 16/68] 2 factor authentication and user account lockout completed --- Oqtane.Client/Modules/Admin/Login/Index.razor | 341 ++++++++++-------- .../Modules/Admin/UserProfile/Index.razor | 194 +++++----- .../Resources/Modules/Admin/Login/Index.resx | 46 ++- .../Modules/Admin/UserProfile/Index.resx | 6 + .../Services/Interfaces/IUserService.cs | 8 + Oqtane.Client/Services/UserService.cs | 5 + Oqtane.Server/Controllers/UserController.cs | 123 +++++-- .../OqtaneServiceCollectionExtensions.cs | 6 +- .../EntityBuilders/BaseEntityBuilder.cs | 74 +++- .../Tenant/03010002_AddUserTwoFactor.cs | 33 ++ .../Oqtane.Modules.Admin.Login/Module.css | 6 +- Oqtane.Shared/Models/User.cs | 21 +- 12 files changed, 561 insertions(+), 302 deletions(-) create mode 100644 Oqtane.Server/Migrations/Tenant/03010002_AddUserTwoFactor.cs diff --git a/Oqtane.Client/Modules/Admin/Login/Index.razor b/Oqtane.Client/Modules/Admin/Login/Index.razor index 9a1eb15d..ec418fd4 100644 --- a/Oqtane.Client/Modules/Admin/Login/Index.razor +++ b/Oqtane.Client/Modules/Admin/Login/Index.razor @@ -7,10 +7,6 @@ @inject IStringLocalizer Localizer @inject IStringLocalizer SharedLocalizer -@if (_message != string.Empty) -{ - -} ... @@ -19,179 +15,208 @@
@Localizer["Info.SignedIn"]
-
- -
-
+ @if (!twofactor) + { +
+ +
+ } + else + { +
+ +
+ } +
@code { - private string _returnUrl = string.Empty; - private string _message = string.Empty; - private MessageType _type = MessageType.Info; - private string _username = string.Empty; - private string _password = string.Empty; - private bool _remember = false; - private bool validated = false; + private ElementReference login; + private bool validated = false; + private bool twofactor = false; + private string _username = string.Empty; + private ElementReference username; + private string _password = string.Empty; + private bool _remember = false; + private string _code = string.Empty; - private ElementReference login; - private ElementReference username; + private string _returnUrl = string.Empty; - public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Anonymous; + public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Anonymous; - public override List Resources => new List() + public override List Resources => new List() { new Resource { ResourceType = ResourceType.Stylesheet, Url = ModulePath() + "Module.css" } }; - protected override async Task OnInitializedAsync() - { - if (PageState.QueryString.ContainsKey("returnurl")) - { - _returnUrl = PageState.QueryString["returnurl"]; - } + protected override async Task OnInitializedAsync() + { + if (PageState.QueryString.ContainsKey("returnurl")) + { + _returnUrl = PageState.QueryString["returnurl"]; + } - if (PageState.QueryString.ContainsKey("name")) - { - _username = PageState.QueryString["name"]; - } + if (PageState.QueryString.ContainsKey("name")) + { + _username = PageState.QueryString["name"]; + } - if (PageState.QueryString.ContainsKey("token")) - { - var user = new User(); - user.SiteId = PageState.Site.SiteId; - user.Username = _username; - user = await UserService.VerifyEmailAsync(user, PageState.QueryString["token"]); + if (PageState.QueryString.ContainsKey("token")) + { + var user = new User(); + user.SiteId = PageState.Site.SiteId; + user.Username = _username; + user = await UserService.VerifyEmailAsync(user, PageState.QueryString["token"]); - if (user != null) - { - await logger.LogInformation(LogFunction.Security, "Email Verified For For Username {Username}", _username); - _message = Localizer["Success.Account.Verified"]; - } - else - { - await logger.LogError(LogFunction.Security, "Email Verification Failed For Username {Username}", _username); - _message = Localizer["Message.Account.NotVerfied"]; - _type = MessageType.Warning; - } - } - } + if (user != null) + { + await logger.LogInformation(LogFunction.Security, "Email Verified For For Username {Username}", _username); + AddModuleMessage(Localizer["Success.Account.Verified"], MessageType.Info); + } + else + { + await logger.LogError(LogFunction.Security, "Email Verification Failed For Username {Username}", _username); + AddModuleMessage(Localizer["Message.Account.NotVerfied"], MessageType.Warning); + } + } + } - protected override async Task OnAfterRenderAsync(bool firstRender) - { - if (firstRender) - { - if(PageState.User == null) - { - await username.FocusAsync(); - } - } - } + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender && PageState.User == null) + { + await username.FocusAsync(); + } + } - private async Task Login() - { - validated = true; - var interop = new Interop(JSRuntime); - if (await interop.FormValid(login)) - { - if (PageState.Runtime == Oqtane.Shared.Runtime.Server) - { - var user = new User(); - user.SiteId = PageState.Site.SiteId; - user.Username = _username; - user.Password = _password; - user = await UserService.LoginUserAsync(user, false, false); + private async Task Login() + { + validated = true; + var interop = new Interop(JSRuntime); + if (await interop.FormValid(login)) + { + var user = new User { SiteId = PageState.Site.SiteId, Username = _username, Password = _password}; - if (user.IsAuthenticated) - { - await logger.LogInformation(LogFunction.Security, "Login Successful For Username {Username}", _username); - // server-side Blazor needs to post to the Login page so that the cookies are set correctly - var fields = new { __RequestVerificationToken = SiteState.AntiForgeryToken, username = _username, password = _password, remember = _remember, returnurl = _returnUrl }; - string url = Utilities.TenantUrl(PageState.Alias, "/pages/login/"); - await interop.SubmitForm(url, fields); - } - else - { - await logger.LogInformation(LogFunction.Security, "Login Failed For Username {Username}", _username); - AddModuleMessage(Localizer["Error.Login.Fail"], MessageType.Error); - } - } - else - { - // client-side Blazor - var user = new User(); - user.SiteId = PageState.Site.SiteId; - user.Username = _username; - user.Password = _password; - user = await UserService.LoginUserAsync(user, true, _remember); - if (user.IsAuthenticated) - { - await logger.LogInformation(LogFunction.Security, "Login Successful For Username {Username}", _username); - var authstateprovider = (IdentityAuthenticationStateProvider)ServiceProvider.GetService(typeof(IdentityAuthenticationStateProvider)); - authstateprovider.NotifyAuthenticationChanged(); - NavigationManager.NavigateTo(NavigateUrl(_returnUrl, true)); - } - else - { - await logger.LogInformation(LogFunction.Security, "Login Failed For Username {Username}", _username); - AddModuleMessage(Localizer["Error.Login.Fail"], MessageType.Error); - } - } - } - else - { - AddModuleMessage(Localizer["Message.Required.UserInfo"], MessageType.Warning); - } - } + if (!twofactor) + { + user = await UserService.LoginUserAsync(user, false, false); + } + else + { + user = await UserService.VerifyTwoFactorAsync(user, _code); + } - private void Cancel() - { - NavigationManager.NavigateTo(_returnUrl); - } + if (user.IsAuthenticated) + { + await logger.LogInformation(LogFunction.Security, "Login Successful For Username {Username}", _username); - private async Task Forgot() - { - if (_username != string.Empty) - { - var user = await UserService.GetUserAsync(_username, PageState.Site.SiteId); - if (user != null) - { - await UserService.ForgotPasswordAsync(user); - await logger.LogInformation(LogFunction.Security, "Password Reset Notification Sent For Username {Username}", _username); - _message = Localizer["Message.ForgotUser"]; - } - else - { - _message = Localizer["Message.UserDoesNotExist"]; - _type = MessageType.Warning; - } - } - else - { - _message = Localizer["Message.ForgotPassword"]; - } + if (PageState.Runtime == Oqtane.Shared.Runtime.Server) + { + // server-side Blazor needs to post to the Login page so that the cookies are set correctly + var fields = new { __RequestVerificationToken = SiteState.AntiForgeryToken, username = _username, password = _password, remember = _remember, returnurl = _returnUrl }; + string url = Utilities.TenantUrl(PageState.Alias, "/pages/login/"); + await interop.SubmitForm(url, fields); + } + else + { + var authstateprovider = (IdentityAuthenticationStateProvider)ServiceProvider.GetService(typeof(IdentityAuthenticationStateProvider)); + authstateprovider.NotifyAuthenticationChanged(); + NavigationManager.NavigateTo(NavigateUrl(_returnUrl, true)); + } + } + else + { + if (user.TwoFactorRequired) + { + twofactor = true; + validated = false; + AddModuleMessage(Localizer["Message.TwoFactor"], MessageType.Info); + } + else + { + if (!twofactor) + { + await logger.LogInformation(LogFunction.Security, "Login Failed For Username {Username}", _username); + AddModuleMessage(Localizer["Error.Login.Fail"], MessageType.Error); + } + else + { + await logger.LogInformation(LogFunction.Security, "Two Factor Verification Failed For Username {Username}", _username); + AddModuleMessage(Localizer["Error.TwoFactor.Fail"], MessageType.Error); + } + } + } + } + else + { + AddModuleMessage(Localizer["Message.Required.UserInfo"], MessageType.Warning); + } + } - StateHasChanged(); - } + private void Cancel() + { + NavigationManager.NavigateTo(_returnUrl); + } + + private async Task Forgot() + { + if (_username != string.Empty) + { + var user = await UserService.GetUserAsync(_username, PageState.Site.SiteId); + if (user != null) + { + await UserService.ForgotPasswordAsync(user); + await logger.LogInformation(LogFunction.Security, "Password Reset Notification Sent For Username {Username}", _username); + AddModuleMessage(Localizer["Message.ForgotUser"], MessageType.Info); + } + else + { + AddModuleMessage(Localizer["Message.UserDoesNotExist"], MessageType.Warning); + } + } + else + { + AddModuleMessage(Localizer["Message.ForgotPassword"], MessageType.Info); + } + + StateHasChanged(); + } + + private void Reset() + { + twofactor = false; + _username = ""; + _password = ""; + ClearModuleMessage(); + StateHasChanged(); + } private async Task KeyPressed(KeyboardEventArgs e) { diff --git a/Oqtane.Client/Modules/Admin/UserProfile/Index.razor b/Oqtane.Client/Modules/Admin/UserProfile/Index.razor index 9f3d19ae..fc6d7891 100644 --- a/Oqtane.Client/Modules/Admin/UserProfile/Index.razor +++ b/Oqtane.Client/Modules/Admin/UserProfile/Index.razor @@ -41,6 +41,15 @@ else +
+ +
+ +
+
@@ -201,104 +210,119 @@ else } +

@code { - private string username = string.Empty; - private string password = string.Empty; - private string confirm = string.Empty; - private string email = string.Empty; - private string displayname = string.Empty; - private FileManager filemanager; - private int folderid = -1; - private int photofileid = -1; - private File photo = null; - private List profiles; - private Dictionary settings; - private string category = string.Empty; - private string filter = "to"; - private List notifications; + private string username = string.Empty; + private string password = string.Empty; + private string confirm = string.Empty; + private string twofactor = "False"; + private string email = string.Empty; + private string displayname = string.Empty; + private FileManager filemanager; + private int folderid = -1; + private int photofileid = -1; + private File photo = null; + private List profiles; + private Dictionary settings; + private string category = string.Empty; + private string filter = "to"; + private List notifications; - public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.View; + public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.View; - protected override async Task OnParametersSetAsync() - { - try - { - if (PageState.User != null) - { - username = PageState.User.Username; - email = PageState.User.Email; - displayname = PageState.User.DisplayName; + protected override async Task OnParametersSetAsync() + { + try + { + if (PageState.User != null) + { + username = PageState.User.Username; + twofactor = PageState.User.TwoFactorRequired.ToString(); + email = PageState.User.Email; + displayname = PageState.User.DisplayName; - // get user folder - var folder = await FolderService.GetFolderAsync(ModuleState.SiteId, PageState.User.FolderPath); - if (folder != null) - { - folderid = folder.FolderId; - } + // get user folder + var folder = await FolderService.GetFolderAsync(ModuleState.SiteId, PageState.User.FolderPath); + if (folder != null) + { + folderid = folder.FolderId; + } - if (PageState.User.PhotoFileId != null) - { - photofileid = PageState.User.PhotoFileId.Value; - photo = await FileService.GetFileAsync(photofileid); - } - else - { - photofileid = -1; - photo = null; - } + if (PageState.User.PhotoFileId != null) + { + photofileid = PageState.User.PhotoFileId.Value; + photo = await FileService.GetFileAsync(photofileid); + } + else + { + photofileid = -1; + photo = null; + } - profiles = await ProfileService.GetProfilesAsync(ModuleState.SiteId); - settings = await SettingService.GetUserSettingsAsync(PageState.User.UserId); + profiles = await ProfileService.GetProfilesAsync(ModuleState.SiteId); + settings = await SettingService.GetUserSettingsAsync(PageState.User.UserId); - await LoadNotificationsAsync(); - } - else - { - AddModuleMessage(Localizer["Message.User.NoLogIn"], MessageType.Warning); - } - } - catch (Exception ex) - { - await logger.LogError(ex, "Error Loading User Profile {Error}", ex.Message); - AddModuleMessage(Localizer["Error.Profile.Load"], MessageType.Error); - } - } + await LoadNotificationsAsync(); + } + else + { + AddModuleMessage(Localizer["Message.User.NoLogIn"], MessageType.Warning); + } + } + catch (Exception ex) + { + await logger.LogError(ex, "Error Loading User Profile {Error}", ex.Message); + AddModuleMessage(Localizer["Error.Profile.Load"], MessageType.Error); + } + } - private async Task LoadNotificationsAsync() - { - notifications = await NotificationService.GetNotificationsAsync(PageState.Site.SiteId, filter, PageState.User.UserId); - notifications = notifications.Where(item => item.DeletedBy != PageState.User.Username).ToList(); - } + private async Task LoadNotificationsAsync() + { + notifications = await NotificationService.GetNotificationsAsync(PageState.Site.SiteId, filter, PageState.User.UserId); + notifications = notifications.Where(item => item.DeletedBy != PageState.User.Username).ToList(); + } - private string GetProfileValue(string SettingName, string DefaultValue) - => SettingService.GetSetting(settings, SettingName, DefaultValue); + private string GetProfileValue(string SettingName, string DefaultValue) + => SettingService.GetSetting(settings, SettingName, DefaultValue); - private async Task Save() - { - try - { - if (username != string.Empty && email != string.Empty && ValidateProfiles()) - { - if (password == confirm) - { - var user = PageState.User; - user.Username = username; - user.Password = password; - user.Email = email; - user.DisplayName = (displayname == string.Empty ? username : displayname); - user.PhotoFileId = filemanager.GetFileId(); - if (user.PhotoFileId == -1) - { - user.PhotoFileId = null; - } + private async Task Save() + { + try + { + if (username != string.Empty && email != string.Empty && ValidateProfiles()) + { + if (password == confirm) + { + var user = PageState.User; + user.Username = username; + user.Password = password; + user.TwoFactorRequired = bool.Parse(twofactor); + user.Email = email; + user.DisplayName = (displayname == string.Empty ? username : displayname); + user.PhotoFileId = filemanager.GetFileId(); + if (user.PhotoFileId == -1) + { + user.PhotoFileId = null; + } + if (user.PhotoFileId != null) + { + photofileid = user.PhotoFileId.Value; + photo = await FileService.GetFileAsync(photofileid); + } + else + { + photofileid = -1; + photo = null; + } - await UserService.UpdateUserAsync(user); - await SettingService.UpdateUserSettingsAsync(settings, PageState.User.UserId); - await logger.LogInformation("User Profile Saved"); + await UserService.UpdateUserAsync(user); + await SettingService.UpdateUserSettingsAsync(settings, PageState.User.UserId); + await logger.LogInformation("User Profile Saved"); - NavigationManager.NavigateTo(NavigateUrl()); - } + AddModuleMessage(Localizer["Success.Profile.Update"], MessageType.Success); + StateHasChanged(); + } else { AddModuleMessage(Localizer["Message.Password.Invalid"], MessageType.Warning); diff --git a/Oqtane.Client/Resources/Modules/Admin/Login/Index.resx b/Oqtane.Client/Resources/Modules/Admin/Login/Index.resx index 36ae93c3..7e1ad433 100644 --- a/Oqtane.Client/Resources/Modules/Admin/Login/Index.resx +++ b/Oqtane.Client/Resources/Modules/Admin/Login/Index.resx @@ -117,9 +117,6 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - Remember Me? - Forgot Password @@ -130,10 +127,10 @@ User Account Could Not Be Verified. Please Contact Your Administrator For Further Instructions. - Login Failed. Please Remember That Passwords Are Case Sensitive And User Accounts Require Verification When They Are Initially Created So You May Wish To Check Your Email. + Login Failed. Please Remember That Passwords Are Case Sensitive. If You Have Attempted To Sign In 3 Times Unsuccessfully, Your Account Will Be Locked Out For 10 Minutes. Note That User Accounts Require Verification When They Are Initially Created So You May Wish To Check Your Email If You Are A New User. - Please Provide Your Username And Password + Please Provide All Required Fields You Are Already Signed In @@ -147,4 +144,43 @@ User Does Not Exist + + Please Enter The Secure Verification Code Which Was Sent To You By Email. + + + Verification Code + + + Verification Code: + + + Verification Failed. Please Ensure You Entered The Code Exactly In The Form Provided In Your Email. If You Wish To Request A New Verification Code Please Select The Cancel Option And Sign In Again. + + + A Secure Verification Code Has Been Sent To Your Email Address. Please Enter The Code That You Received. If You Do Not Receive The Code Within 10 Minutes Or You Have Lost Access To Your Email, Please Contact Your Administrator. + + + Please Enter The Password Related To Your Account. Remember That Passwords Are Case Sensitive. If Your Sign In Attempts Fail 3 Times In Succession, Your Account Will Be Locked Out For 10 Minutes. + + + Password + + + Password: + + + Specify If You Would Like To Be Signed Back In Automatically The Next Time You Visit This Site + + + Remember Me? + + + Please Enter The Username Related To Your Account + + + Username + + + Username: + \ No newline at end of file diff --git a/Oqtane.Client/Resources/Modules/Admin/UserProfile/Index.resx b/Oqtane.Client/Resources/Modules/Admin/UserProfile/Index.resx index 294cc895..22a36db3 100644 --- a/Oqtane.Client/Resources/Modules/Admin/UserProfile/Index.resx +++ b/Oqtane.Client/Resources/Modules/Admin/UserProfile/Index.resx @@ -204,4 +204,10 @@ Username: + + Indicates if you are using two factor authentication + + + Two Factor Authentication? + \ No newline at end of file diff --git a/Oqtane.Client/Services/Interfaces/IUserService.cs b/Oqtane.Client/Services/Interfaces/IUserService.cs index 08289bea..28258449 100644 --- a/Oqtane.Client/Services/Interfaces/IUserService.cs +++ b/Oqtane.Client/Services/Interfaces/IUserService.cs @@ -88,5 +88,13 @@ namespace Oqtane.Services /// /// Task ResetPasswordAsync(User user, string token); + + /// + /// Verify the two factor verification code + /// + /// + /// + /// + Task VerifyTwoFactorAsync(User user, string token); } } diff --git a/Oqtane.Client/Services/UserService.cs b/Oqtane.Client/Services/UserService.cs index 6a981e54..77a83005 100644 --- a/Oqtane.Client/Services/UserService.cs +++ b/Oqtane.Client/Services/UserService.cs @@ -68,5 +68,10 @@ namespace Oqtane.Services { return await PostJsonAsync($"{Apiurl}/reset?token={token}", user); } + + public async Task VerifyTwoFactorAsync(User user, string token) + { + return await PostJsonAsync($"{Apiurl}/twofactor?token={token}", user); + } } } diff --git a/Oqtane.Server/Controllers/UserController.cs b/Oqtane.Server/Controllers/UserController.cs index 94e58881..d9920953 100644 --- a/Oqtane.Server/Controllers/UserController.cs +++ b/Oqtane.Server/Controllers/UserController.cs @@ -97,23 +97,30 @@ namespace Oqtane.Controllers private User Filter(User user) { - if (user != null && !User.IsInRole(RoleNames.Admin) && User.Identity.Name?.ToLower() != user.Username.ToLower()) + if (user != null) { - user.DisplayName = ""; - user.Email = ""; - user.PhotoFileId = null; - user.LastLoginOn = DateTime.MinValue; - user.LastIPAddress = ""; - user.Roles = ""; - user.CreatedBy = ""; - user.CreatedOn = DateTime.MinValue; - user.ModifiedBy = ""; - user.ModifiedOn = DateTime.MinValue; - user.DeletedBy = ""; - user.DeletedOn = DateTime.MinValue; - user.IsDeleted = false; user.Password = ""; user.IsAuthenticated = false; + user.TwoFactorCode = ""; + user.TwoFactorExpiry = null; + + if (!User.IsInRole(RoleNames.Admin) && User.Identity.Name?.ToLower() != user.Username.ToLower()) + { + user.DisplayName = ""; + user.Email = ""; + user.PhotoFileId = null; + user.LastLoginOn = DateTime.MinValue; + user.LastIPAddress = ""; + user.Roles = ""; + user.CreatedBy = ""; + user.CreatedOn = DateTime.MinValue; + user.ModifiedBy = ""; + user.ModifiedOn = DateTime.MinValue; + user.DeletedBy = ""; + user.DeletedOn = DateTime.MinValue; + user.IsDeleted = false; + user.TwoFactorRequired = false; + } } return user; } @@ -247,15 +254,14 @@ namespace Oqtane.Controllers { if (ModelState.IsValid && user.SiteId == _alias.SiteId && _users.GetUser(user.UserId, false) != null && (User.IsInRole(RoleNames.Admin) || User.Identity.Name == user.Username)) { - IdentityUser identityuser = await _identityUserManager.FindByNameAsync(user.Username); - if (identityuser != null) + if (user.Password != "") { - identityuser.TwoFactorEnabled = user.TwoFactorEnabled; - if (user.Password != "") + IdentityUser identityuser = await _identityUserManager.FindByNameAsync(user.Username); + if (identityuser != null) { identityuser.PasswordHash = _identityUserManager.PasswordHasher.HashPassword(identityuser, user.Password); + await _identityUserManager.UpdateAsync(identityuser); } - await _identityUserManager.UpdateAsync(identityuser); } user = _users.UpdateUser(user); _syncManager.AddSyncEvent(_alias.TenantId, EntityNames.User, user.UserId); @@ -333,7 +339,7 @@ namespace Oqtane.Controllers [HttpPost("login")] public async Task Login([FromBody] User user, bool setCookie, bool isPersistent) { - User loginUser = new User { Username = user.Username, IsAuthenticated = false }; + User loginUser = new User { SiteId = user.SiteId, Username = user.Username, IsAuthenticated = false }; if (ModelState.IsValid) { @@ -343,24 +349,44 @@ namespace Oqtane.Controllers var result = await _identitySignInManager.CheckPasswordSignInAsync(identityuser, user.Password, true); if (result.Succeeded) { - loginUser = _users.GetUser(identityuser.UserName); - if (loginUser != null) + user = _users.GetUser(user.Username); + if (user.TwoFactorRequired) { - if (identityuser.EmailConfirmed) + var token = await _identityUserManager.GenerateTwoFactorTokenAsync(identityuser, "Email"); + user.TwoFactorCode = token; + user.TwoFactorExpiry = DateTime.UtcNow.AddMinutes(10); + _users.UpdateUser(user); + + string body = "Dear " + user.DisplayName + ",\n\nYou requested a secure verification code to log in to your account. Please enter the secure verification code on the site:\n\n" + token + + "\n\nPlease note that the code is only valid for 10 minutes so if you are unable to take action within that time period, you should initiate a new login on the site." + + "\n\nThank You!"; + var notification = new Notification(loginUser.SiteId, user, "User Verification Code", body); + _notifications.AddNotification(notification); + + _logger.Log(LogLevel.Information, this, LogFunction.Security, "User Verification Notification Sent For {Username}", user.Username); + loginUser.TwoFactorRequired = true; + } + else + { + loginUser = _users.GetUser(identityuser.UserName); + if (loginUser != null) { - loginUser.IsAuthenticated = true; - loginUser.LastLoginOn = DateTime.UtcNow; - loginUser.LastIPAddress = HttpContext.Connection.RemoteIpAddress.ToString(); - _users.UpdateUser(loginUser); - _logger.Log(LogLevel.Information, this, LogFunction.Security, "User Login Successful {Username}", user.Username); - if (setCookie) + if (identityuser.EmailConfirmed) { - await _identitySignInManager.SignInAsync(identityuser, isPersistent); + loginUser.IsAuthenticated = true; + loginUser.LastLoginOn = DateTime.UtcNow; + loginUser.LastIPAddress = HttpContext.Connection.RemoteIpAddress.ToString(); + _users.UpdateUser(loginUser); + _logger.Log(LogLevel.Information, this, LogFunction.Security, "User Login Successful {Username}", user.Username); + if (setCookie) + { + await _identitySignInManager.SignInAsync(identityuser, isPersistent); + } + } + else + { + _logger.Log(LogLevel.Information, this, LogFunction.Security, "User Not Verified {Username}", user.Username); } - } - else - { - _logger.Log(LogLevel.Information, this, LogFunction.Security, "User Not Verified {Username}", user.Username); } } } @@ -371,16 +397,16 @@ namespace Oqtane.Controllers user = _users.GetUser(user.Username); string token = await _identityUserManager.GeneratePasswordResetTokenAsync(identityuser); string url = HttpContext.Request.Scheme + "://" + _alias.Name + "/reset?name=" + user.Username + "&token=" + WebUtility.UrlEncode(token); - string body = "Dear " + user.DisplayName + ",\n\nYou attempted 3 times unsuccessfully to login to your account and it is now locked out. Please wait 10 minutes and then try again... or use the link below to reset your password:\n\n" + url + + string body = "Dear " + user.DisplayName + ",\n\nYou attempted 3 times unsuccessfully to log in to your account and it is now locked out. Please wait 10 minutes and then try again... or use the link below to reset your password:\n\n" + url + "\n\nPlease note that the link is only valid for 24 hours so if you are unable to take action within that time period, you should initiate another password reset on the site." + "\n\nThank You!"; - var notification = new Notification(user.SiteId, user, "User Password Lockout", body); + var notification = new Notification(loginUser.SiteId, user, "User Lockout", body); _notifications.AddNotification(notification); - _logger.Log(LogLevel.Information, this, LogFunction.Security, "Password Lockout Notification Sent For {Username}", user.Username); + _logger.Log(LogLevel.Information, this, LogFunction.Security, "User Lockout Notification Sent For {Username}", user.Username); } else { - _logger.Log(LogLevel.Error, this, LogFunction.Security, "User Login Failed {Username}", user.Username); + _logger.Log(LogLevel.Information, this, LogFunction.Security, "User Login Failed {Username}", user.Username); } } } @@ -485,6 +511,27 @@ namespace Oqtane.Controllers return user; } + // POST api//twofactor + [HttpPost("twofactor")] + public User TwoFactor([FromBody] User user, string token) + { + User loginUser = new User { SiteId = user.SiteId, Username = user.Username, IsAuthenticated = false }; + + if (ModelState.IsValid && !string.IsNullOrEmpty(token)) + { + user = _users.GetUser(user.Username); + if (user != null) + { + if (user.TwoFactorRequired && user.TwoFactorCode == token && DateTime.UtcNow < user.TwoFactorExpiry) + { + loginUser.IsAuthenticated = true; + } + } + } + + return loginUser; + } + // GET api//authenticate [HttpGet("authenticate")] public User Authenticate() diff --git a/Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs b/Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs index 89f9895a..2d1c36a8 100644 --- a/Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs +++ b/Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs @@ -141,9 +141,9 @@ namespace Microsoft.Extensions.DependencyInjection options.Password.RequireLowercase = false; // Lockout settings - options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(30); - options.Lockout.MaxFailedAccessAttempts = 10; - options.Lockout.AllowedForNewUsers = true; + options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(10); + options.Lockout.MaxFailedAccessAttempts = 3; + options.Lockout.AllowedForNewUsers = false; // User settings options.User.RequireUniqueEmail = false; diff --git a/Oqtane.Server/Migrations/EntityBuilders/BaseEntityBuilder.cs b/Oqtane.Server/Migrations/EntityBuilders/BaseEntityBuilder.cs index 24ded671..5c1d0e28 100644 --- a/Oqtane.Server/Migrations/EntityBuilders/BaseEntityBuilder.cs +++ b/Oqtane.Server/Migrations/EntityBuilders/BaseEntityBuilder.cs @@ -52,64 +52,119 @@ namespace Oqtane.Migrations.EntityBuilders _migrationBuilder.AddColumn(RewriteName(name), RewriteName(EntityTableName), nullable: nullable); } + public void AddBooleanColumn(string name, bool nullable, bool defaultValue) + { + _migrationBuilder.AddColumn(RewriteName(name), RewriteName(EntityTableName), nullable: nullable, defaultValue: defaultValue); + } + protected OperationBuilder AddBooleanColumn(ColumnsBuilder table, string name, bool nullable = false) { return table.Column(name: RewriteName(name), nullable: nullable); } + protected OperationBuilder AddBooleanColumn(ColumnsBuilder table, string name, bool nullable, bool defaultValue) + { + return table.Column(name: RewriteName(name), nullable: nullable, defaultValue: defaultValue); + } + public void AddDateTimeColumn(string name, bool nullable = false) { _migrationBuilder.AddColumn(RewriteName(name), RewriteName(EntityTableName), nullable: nullable); } + public void AddDateTimeColumn(string name, bool nullable, DateTime defaultValue) + { + _migrationBuilder.AddColumn(RewriteName(name), RewriteName(EntityTableName), nullable: nullable, defaultValue: defaultValue); + } + protected OperationBuilder AddDateTimeColumn(ColumnsBuilder table, string name, bool nullable = false) { return table.Column(name: RewriteName(name), nullable: nullable); } + protected OperationBuilder AddDateTimeColumn(ColumnsBuilder table, string name, bool nullable, DateTime defaultValue) + { + return table.Column(name: RewriteName(name), nullable: nullable, defaultValue: defaultValue); + } + public void AddDateTimeOffsetColumn(string name, bool nullable = false) { _migrationBuilder.AddColumn(RewriteName(name), RewriteName(EntityTableName), nullable: nullable); } + public void AddDateTimeOffsetColumn(string name, bool nullable, DateTimeOffset defaultValue) + { + _migrationBuilder.AddColumn(RewriteName(name), RewriteName(EntityTableName), nullable: nullable, defaultValue: defaultValue); + } + protected OperationBuilder AddDateTimeOffsetColumn(ColumnsBuilder table, string name, bool nullable = false) { return table.Column(name: RewriteName(name), nullable: nullable); } + protected OperationBuilder AddDateTimeOffsetColumn(ColumnsBuilder table, string name, bool nullable, DateTimeOffset defaultValue) + { + return table.Column(name: RewriteName(name), nullable: nullable, defaultValue: defaultValue); + } + public void AddIntegerColumn(string name, bool nullable = false) { _migrationBuilder.AddColumn(RewriteName(name), RewriteName(EntityTableName), nullable: nullable); } + public void AddIntegerColumn(string name, bool nullable, int defaultValue) + { + _migrationBuilder.AddColumn(RewriteName(name), RewriteName(EntityTableName), nullable: nullable, defaultValue: defaultValue); + } + protected OperationBuilder AddIntegerColumn(ColumnsBuilder table, string name, bool nullable = false) { return table.Column(name: RewriteName(name), nullable: nullable); } + protected OperationBuilder AddIntegerColumn(ColumnsBuilder table, string name, bool nullable, int defaultValue) + { + return table.Column(name: RewriteName(name), nullable: nullable, defaultValue: defaultValue); + } + public void AddMaxStringColumn(string name, bool nullable = false, bool unicode = true) { _migrationBuilder.AddColumn(RewriteName(name), RewriteName(EntityTableName), nullable: nullable, unicode: unicode); } + public void AddMaxStringColumn(string name, bool nullable, bool unicode, string defaultValue) + { + _migrationBuilder.AddColumn(RewriteName(name), RewriteName(EntityTableName), nullable: nullable, unicode: unicode, defaultValue: defaultValue); + } + protected OperationBuilder AddMaxStringColumn(ColumnsBuilder table, string name, bool nullable = false, bool unicode = true) { return table.Column(name: RewriteName(name), nullable: nullable, unicode: unicode); } + protected OperationBuilder AddMaxStringColumn(ColumnsBuilder table, string name, bool nullable, bool unicode, string defaultValue) + { + return table.Column(name: RewriteName(name), nullable: nullable, unicode: unicode, defaultValue: defaultValue); + } + public void AddStringColumn(string name, int length, bool nullable = false, bool unicode = true) { _migrationBuilder.AddColumn(RewriteName(name), RewriteName(EntityTableName), maxLength: length, nullable: nullable, unicode: unicode); } + public void AddStringColumn(string name, int length, bool nullable, bool unicode, string defaultValue) + { + _migrationBuilder.AddColumn(RewriteName(name), RewriteName(EntityTableName), maxLength: length, nullable: nullable, unicode: unicode, defaultValue: defaultValue); + } + protected OperationBuilder AddStringColumn(ColumnsBuilder table, string name, int length, bool nullable = false, bool unicode = true) { return table.Column(name: RewriteName(name), maxLength: length, nullable: nullable, unicode: unicode); } - public void AlterStringColumn(string name, int length, bool nullable = false, bool unicode = true) + protected OperationBuilder AddStringColumn(ColumnsBuilder table, string name, int length, bool nullable, bool unicode, string defaultValue) { - ActiveDatabase.AlterStringColumn(_migrationBuilder, RewriteName(name), RewriteName(EntityTableName), length, nullable, unicode); + return table.Column(name: RewriteName(name), maxLength: length, nullable: nullable, unicode: unicode, defaultValue: defaultValue); } public void AddDecimalColumn(string name, int precision, int scale, bool nullable = false) @@ -117,11 +172,26 @@ namespace Oqtane.Migrations.EntityBuilders _migrationBuilder.AddColumn(RewriteName(name), RewriteName(EntityTableName), nullable: nullable, precision: precision, scale: scale); } + public void AddDecimalColumn(string name, int precision, int scale, bool nullable, decimal defaultValue) + { + _migrationBuilder.AddColumn(RewriteName(name), RewriteName(EntityTableName), nullable: nullable, precision: precision, scale: scale, defaultValue: defaultValue); + } + protected OperationBuilder AddDecimalColumn(ColumnsBuilder table, string name, int precision, int scale, bool nullable = false) { return table.Column(name: RewriteName(name), nullable: nullable, precision: precision, scale: scale); } + protected OperationBuilder AddDecimalColumn(ColumnsBuilder table, string name, int precision, int scale, bool nullable, decimal defaultValue) + { + return table.Column(name: RewriteName(name), nullable: nullable, precision: precision, scale: scale, defaultValue: defaultValue); + } + + public void AlterStringColumn(string name, int length, bool nullable = false, bool unicode = true) + { + ActiveDatabase.AlterStringColumn(_migrationBuilder, RewriteName(name), RewriteName(EntityTableName), length, nullable, unicode); + } + public void DropColumn(string name) { ActiveDatabase.DropColumn(_migrationBuilder, RewriteName(name), RewriteName(EntityTableName)); diff --git a/Oqtane.Server/Migrations/Tenant/03010002_AddUserTwoFactor.cs b/Oqtane.Server/Migrations/Tenant/03010002_AddUserTwoFactor.cs new file mode 100644 index 00000000..e38d84c4 --- /dev/null +++ b/Oqtane.Server/Migrations/Tenant/03010002_AddUserTwoFactor.cs @@ -0,0 +1,33 @@ +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Oqtane.Databases.Interfaces; +using Oqtane.Migrations.EntityBuilders; +using Oqtane.Repository; + +namespace Oqtane.Migrations.Tenant +{ + [DbContext(typeof(TenantDBContext))] + [Migration("Tenant.03.01.00.02")] + public class AddUserTwoFactor : MultiDatabaseMigration + { + public AddUserTwoFactor(IDatabase database) : base(database) + { + } + + protected override void Up(MigrationBuilder migrationBuilder) + { + var userEntityBuilder = new UserEntityBuilder(migrationBuilder, ActiveDatabase); + userEntityBuilder.AddBooleanColumn("TwoFactorRequired", false, false); + userEntityBuilder.AddStringColumn("TwoFactorCode", 6, true); + userEntityBuilder.AddDateTimeColumn("TwoFactorExpiry", true); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + var userEntityBuilder = new UserEntityBuilder(migrationBuilder, ActiveDatabase); + userEntityBuilder.DropColumn("TwoFactorRequired"); + userEntityBuilder.DropColumn("TwoFactorCode"); + userEntityBuilder.DropColumn("TwoFactorExpiry"); + } + } +} diff --git a/Oqtane.Server/wwwroot/Modules/Oqtane.Modules.Admin.Login/Module.css b/Oqtane.Server/wwwroot/Modules/Oqtane.Modules.Admin.Login/Module.css index abfbcc47..e25ff012 100644 --- a/Oqtane.Server/wwwroot/Modules/Oqtane.Modules.Admin.Login/Module.css +++ b/Oqtane.Server/wwwroot/Modules/Oqtane.Modules.Admin.Login/Module.css @@ -1,9 +1,5 @@ /* Login Module Custom Styles */ -.Oqtane-Modules-Admin-Login .username { - width: 200px; -} - -.Oqtane-Modules-Admin-Login .password { +.Oqtane-Modules-Admin-Login .input { width: 200px; } diff --git a/Oqtane.Shared/Models/User.cs b/Oqtane.Shared/Models/User.cs index b38d1db5..b227375b 100644 --- a/Oqtane.Shared/Models/User.cs +++ b/Oqtane.Shared/Models/User.cs @@ -43,6 +43,21 @@ namespace Oqtane.Models /// public string LastIPAddress { get; set; } + /// + /// Indicates if the user requires 2 factor authentication to sign in + /// + public bool TwoFactorRequired { get; set; } + + /// + /// Stores the 2 factor verification code + /// + public string TwoFactorCode { get; set; } + + /// + /// The expiry date/time for the 2 factor verification code + /// + public DateTime? TwoFactorExpiry { get; set; } + /// /// Reference to the this user belongs to. /// @@ -97,11 +112,5 @@ namespace Oqtane.Models { get => "Users\\" + UserId.ToString() + "\\"; } - - /// - /// Indicates if the user requires 2 factor authentication to sign in - /// - [NotMapped] - public bool TwoFactorEnabled { get; set; } } } From 1481c76a0d1bf8135b611ec5c6fbcafba5f9d10d Mon Sep 17 00:00:00 2001 From: Shaun Walker Date: Thu, 3 Mar 2022 09:19:57 -0500 Subject: [PATCH 17/68] Update README.md --- README.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 2e60105c..b12ae31b 100644 --- a/README.md +++ b/README.md @@ -61,11 +61,12 @@ Backlog (Not Yet Assigned) - [ ] Allow language specification in Url (#1731) V.3.1.0 ( Q1 2022 ) +- [x] User account lockout support +- [x] 2 factor authentication support +- [ ] Allow configuration of local authentication parameters (ie. password complexity) +- [ ] Authentication extensibility via OIDC Identity Providers - [ ] Token based authentication / authorization via OAuth2 / OpenID Connect - [ ] Provide Single Sign On (SSO) for other applications -- [ ] Authentication extensibility via Social logins / Azure B2C -- [ ] Allow configuration of password complexity for local authentication -- [ ] User account lockout support V.3.0.3 ( Feb 15, 2022 ) - [x] Url fragment and anchor navigation support From 12d1b5e8493cd05f35b7663e161b8e28f379a5c6 Mon Sep 17 00:00:00 2001 From: Shaun Walker Date: Fri, 4 Mar 2022 10:35:07 -0500 Subject: [PATCH 18/68] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b12ae31b..a9fe28f7 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,7 @@ Backlog (Not Yet Assigned) V.3.1.0 ( Q1 2022 ) - [x] User account lockout support - [x] 2 factor authentication support -- [ ] Allow configuration of local authentication parameters (ie. password complexity) +- [x] Allow configuration of local authentication parameters (ie. password complexity) - [ ] Authentication extensibility via OIDC Identity Providers - [ ] Token based authentication / authorization via OAuth2 / OpenID Connect - [ ] Provide Single Sign On (SSO) for other applications From 5adecc307fbd0c1b1e9dcfa36c2ab704d330907f Mon Sep 17 00:00:00 2001 From: Shaun Walker Date: Fri, 4 Mar 2022 10:41:45 -0500 Subject: [PATCH 19/68] Allow user identity password and lockout configuration to be customized. Included additional environment information in System Info. --- .../Modules/Admin/SystemInfo/Index.razor | 88 +++++-- Oqtane.Client/Modules/Admin/Users/Index.razor | 238 +++++++++++++----- .../Resources/Modules/Admin/Login/Index.resx | 4 +- .../Modules/Admin/SystemInfo/Index.resx | 38 ++- .../Resources/Modules/Admin/Users/Index.resx | 51 ++++ .../Services/Interfaces/ISystemService.cs | 24 +- Oqtane.Client/Services/SystemService.cs | 20 +- Oqtane.Server/Controllers/SystemController.cs | 101 ++++---- Oqtane.Server/Controllers/UserController.cs | 2 +- .../OqtaneServiceCollectionExtensions.cs | 25 +- Oqtane.Server/Infrastructure/ConfigManager.cs | 2 +- Oqtane.Server/Startup.cs | 2 +- 12 files changed, 445 insertions(+), 150 deletions(-) diff --git a/Oqtane.Client/Modules/Admin/SystemInfo/Index.razor b/Oqtane.Client/Modules/Admin/SystemInfo/Index.razor index 0fe5c328..77878a1b 100644 --- a/Oqtane.Client/Modules/Admin/SystemInfo/Index.razor +++ b/Oqtane.Client/Modules/Admin/SystemInfo/Index.razor @@ -27,9 +27,27 @@
- +
- + +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+
@@ -38,6 +56,18 @@
+
+ +
+ +
+
+
+ +
+ +
+
@@ -119,8 +149,13 @@ private string _version = string.Empty; private string _clrversion = string.Empty; private string _osversion = string.Empty; - private string _serverpath = string.Empty; + private string _machinename = string.Empty; + private string _ipaddress = string.Empty; + private string _contentrootpath = string.Empty; + private string _webrootpath = string.Empty; private string _servertime = string.Empty; + private string _tickcount = string.Empty; + private string _workingset = string.Empty; private string _installationid = string.Empty; private string _detailederrors = string.Empty; @@ -133,33 +168,42 @@ { _version = Constants.Version; - Dictionary systeminfo = await SystemService.GetSystemInfoAsync(); + Dictionary systeminfo = await SystemService.GetSystemInfoAsync("environment"); if (systeminfo != null) { - _clrversion = systeminfo["clrversion"]; - _osversion = systeminfo["osversion"]; - _serverpath = systeminfo["serverpath"]; - _servertime = systeminfo["servertime"] + " UTC"; - _installationid = systeminfo["installationid"]; + _clrversion = systeminfo["CLRVersion"].ToString(); + _osversion = systeminfo["OSVersion"].ToString(); + _machinename = systeminfo["MachineName"].ToString(); + _ipaddress = systeminfo["IPAddress"].ToString(); + _contentrootpath = systeminfo["ContentRootPath"].ToString(); + _webrootpath = systeminfo["WebRootPath"].ToString(); + _servertime = systeminfo["ServerTime"].ToString() + " UTC"; + _tickcount = TimeSpan.FromMilliseconds(Convert.ToInt64(systeminfo["TickCount"].ToString())).ToString(); + _workingset = (Convert.ToInt64(systeminfo["WorkingSet"].ToString()) / 1000000).ToString() + " MB"; + } - _detailederrors = systeminfo["detailederrors"]; - _logginglevel = systeminfo["logginglevel"]; - _notificationlevel = systeminfo["notificationlevel"]; - _swagger = systeminfo["swagger"]; - _packageservice = systeminfo["packageservice"]; - } - } + systeminfo = await SystemService.GetSystemInfoAsync(); + if (systeminfo != null) + { + _installationid = systeminfo["InstallationId"].ToString(); + _detailederrors = systeminfo["DetailedErrors"].ToString(); + _logginglevel = systeminfo["Logging:LogLevel:Default"].ToString(); + _notificationlevel = systeminfo["Logging:LogLevel:Notify"].ToString(); + _swagger = systeminfo["UseSwagger"].ToString(); + _packageservice = systeminfo["PackageService"].ToString(); + } + } private async Task SaveConfig() { try { - var settings = new Dictionary(); - settings.Add("detailederrors", _detailederrors); - settings.Add("logginglevel", _logginglevel); - settings.Add("notificationlevel", _notificationlevel); - settings.Add("swagger", _swagger); - settings.Add("packageservice", _packageservice); + var settings = new Dictionary(); + settings.Add("DetailedErrors", _detailederrors); + settings.Add("Logging:LogLevel:Default", _logginglevel); + settings.Add("Logging:LogLevel:Notify", _notificationlevel); + settings.Add("UseSwagger", _swagger); + settings.Add("PackageService", _packageservice); await SystemService.UpdateSystemInfoAsync(settings); AddModuleMessage(Localizer["Success.UpdateConfig.Restart"], MessageType.Success); } diff --git a/Oqtane.Client/Modules/Admin/Users/Index.razor b/Oqtane.Client/Modules/Admin/Users/Index.razor index 4f017b85..a0b1a54d 100644 --- a/Oqtane.Client/Modules/Admin/Users/Index.razor +++ b/Oqtane.Client/Modules/Admin/Users/Index.razor @@ -4,6 +4,7 @@ @inject IUserService UserService @inject ISettingService SettingService @inject ISiteService SiteService +@inject ISystemService SystemService @inject IStringLocalizer Localizer @inject IStringLocalizer SharedLocalizer @@ -64,6 +65,74 @@ else
+ @if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host)) + { +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ }
@@ -72,79 +141,104 @@ else } @code { - private List allroles; - private List userroles; - private string _search; + private List allroles; + private List userroles; + private string _search; + private string _allowregistration; + private string _minimumlength = "6"; + private string _uniquecharacters = "1"; + private string _requiredigit = "true"; + private string _requireupper = "true"; + private string _requirelower = "true"; + private string _requirepunctuation = "true"; + private string _maximumfailures = "5"; + private string _lockoutduration = "5"; - public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin; + public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin; + + protected override async Task OnInitializedAsync() + { + allroles = await UserRoleService.GetUserRolesAsync(PageState.Site.SiteId); + await LoadSettingsAsync(); + userroles = Search(_search); - protected override async Task OnInitializedAsync() - { - allroles = await UserRoleService.GetUserRolesAsync(PageState.Site.SiteId); - await LoadSettingsAsync(); - userroles = Search(_search); _allowregistration = PageState.Site.AllowRegistration.ToString(); - } + if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host)) + { + Dictionary systeminfo = await SystemService.GetSystemInfoAsync(); + if (systeminfo != null) + { + _minimumlength = systeminfo["Password:RequiredLength"].ToString(); + _uniquecharacters = systeminfo["Password:RequiredUniqueChars"].ToString(); + _requiredigit = systeminfo["Password:RequireDigit"].ToString(); + _requireupper = systeminfo["Password:RequireUppercase"].ToString(); + _requirelower = systeminfo["Password:RequireLowercase"].ToString(); + _requirepunctuation = systeminfo["Password:RequireNonAlphanumeric"].ToString(); + _maximumfailures = systeminfo["Lockout:MaxFailedAccessAttempts"].ToString(); + _lockoutduration = TimeSpan.Parse(systeminfo["Lockout:DefaultLockoutTimeSpan"].ToString()).TotalMinutes.ToString(); + } + } + } - private List Search(string search) - { - var results = allroles.Where(item => item.Role.Name == RoleNames.Registered || (item.Role.Name == RoleNames.Host && UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))); + private List Search(string search) + { + var results = allroles.Where(item => item.Role.Name == RoleNames.Registered || (item.Role.Name == RoleNames.Host && UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))); - if (!string.IsNullOrEmpty(_search)) - { - results = results.Where(item => - ( - item.User.Username.Contains(search, StringComparison.OrdinalIgnoreCase) || - item.User.Email.Contains(search, StringComparison.OrdinalIgnoreCase) || - item.User.DisplayName.Contains(search, StringComparison.OrdinalIgnoreCase) - ) - ); - } - return results.ToList(); - } + if (!string.IsNullOrEmpty(_search)) + { + results = results.Where(item => + ( + item.User.Username.Contains(search, StringComparison.OrdinalIgnoreCase) || + item.User.Email.Contains(search, StringComparison.OrdinalIgnoreCase) || + item.User.DisplayName.Contains(search, StringComparison.OrdinalIgnoreCase) + ) + ); + } + return results.ToList(); + } - private async Task OnSearch() - { - userroles = Search(_search); - await UpdateSettingsAsync(); - } + private async Task OnSearch() + { + userroles = Search(_search); + await UpdateSettingsAsync(); + } - private async Task DeleteUser(UserRole UserRole) - { - try - { - var user = await UserService.GetUserAsync(UserRole.UserId, PageState.Site.SiteId); - if (user != null) - { - await UserService.DeleteUserAsync(user.UserId, PageState.Site.SiteId); - await logger.LogInformation("User Deleted {User}", UserRole.User); - allroles = await UserRoleService.GetUserRolesAsync(PageState.Site.SiteId); - userroles = Search(_search); - StateHasChanged(); - } - } - catch (Exception ex) - { - await logger.LogError(ex, "Error Deleting User {User} {Error}", UserRole.User, ex.Message); - AddModuleMessage(ex.Message, MessageType.Error); - } - } + private async Task DeleteUser(UserRole UserRole) + { + try + { + var user = await UserService.GetUserAsync(UserRole.UserId, PageState.Site.SiteId); + if (user != null) + { + await UserService.DeleteUserAsync(user.UserId, PageState.Site.SiteId); + await logger.LogInformation("User Deleted {User}", UserRole.User); + allroles = await UserRoleService.GetUserRolesAsync(PageState.Site.SiteId); + userroles = Search(_search); + StateHasChanged(); + } + } + catch (Exception ex) + { + await logger.LogError(ex, "Error Deleting User {User} {Error}", UserRole.User, ex.Message); + AddModuleMessage(ex.Message, MessageType.Error); + } + } - private string settingSearch = "AU-search"; + private string settingSearch = "AU-search"; - private async Task LoadSettingsAsync() - { - Dictionary settings = await SettingService.GetUserSettingsAsync(PageState.User.UserId); - _search = SettingService.GetSetting(settings, settingSearch, ""); - } + private async Task LoadSettingsAsync() + { + Dictionary settings = await SettingService.GetUserSettingsAsync(PageState.User.UserId); + _search = SettingService.GetSetting(settings, settingSearch, ""); + } - private async Task UpdateSettingsAsync() - { - Dictionary settings = await SettingService.GetUserSettingsAsync(PageState.User.UserId); - SettingService.SetSetting(settings, settingSearch, _search); - await SettingService.UpdateUserSettingsAsync(settings, PageState.User.UserId); - } + private async Task UpdateSettingsAsync() + { + Dictionary settings = await SettingService.GetUserSettingsAsync(PageState.User.UserId); + SettingService.SetSetting(settings, settingSearch, _search); + await SettingService.UpdateUserSettingsAsync(settings, PageState.User.UserId); + } private async Task SaveSiteSettings() { @@ -153,7 +247,25 @@ else var site = PageState.Site; site.AllowRegistration = bool.Parse(_allowregistration); await SiteService.UpdateSiteAsync(site); - AddModuleMessage(Localizer["Success.SaveSiteSettings"], MessageType.Success); + + if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host)) + { + var settings = new Dictionary(); + settings.Add("Password:RequiredLength", _minimumlength); + settings.Add("Password:RequiredUniqueChars", _uniquecharacters); + settings.Add("Password:RequireDigit", _requiredigit); + settings.Add("Password:RequireUppercase", _requireupper); + settings.Add("Password:RequireLowercase", _requirelower); + settings.Add("Password:RequireNonAlphanumeric", _requirepunctuation); + settings.Add("Lockout:MaxFailedAccessAttempts", _maximumfailures); + settings.Add("Lockout:DefaultLockoutTimeSpan", TimeSpan.FromMinutes(Convert.ToInt64(_lockoutduration)).ToString()); + await SystemService.UpdateSystemInfoAsync(settings); + AddModuleMessage(Localizer["Success.UpdateConfig.Restart"], MessageType.Success); + } + else + { + AddModuleMessage(Localizer["Success.SaveSiteSettings"], MessageType.Success); + } } catch (Exception ex) { diff --git a/Oqtane.Client/Resources/Modules/Admin/Login/Index.resx b/Oqtane.Client/Resources/Modules/Admin/Login/Index.resx index 7e1ad433..6582bb79 100644 --- a/Oqtane.Client/Resources/Modules/Admin/Login/Index.resx +++ b/Oqtane.Client/Resources/Modules/Admin/Login/Index.resx @@ -157,10 +157,10 @@ Verification Failed. Please Ensure You Entered The Code Exactly In The Form Provided In Your Email. If You Wish To Request A New Verification Code Please Select The Cancel Option And Sign In Again. - A Secure Verification Code Has Been Sent To Your Email Address. Please Enter The Code That You Received. If You Do Not Receive The Code Within 10 Minutes Or You Have Lost Access To Your Email, Please Contact Your Administrator. + A Secure Verification Code Has Been Sent To Your Email Address. Please Enter The Code That You Received. If You Do Not Receive The Code Or You Have Lost Access To Your Email, Please Contact Your Administrator. - Please Enter The Password Related To Your Account. Remember That Passwords Are Case Sensitive. If Your Sign In Attempts Fail 3 Times In Succession, Your Account Will Be Locked Out For 10 Minutes. + Please Enter The Password Related To Your Account. Remember That Passwords Are Case Sensitive. If You Attempt Unsuccessfully To Log In To Your Account Multiple Times, You Will Be Locked Out For A Period Of Time. Password diff --git a/Oqtane.Client/Resources/Modules/Admin/SystemInfo/Index.resx b/Oqtane.Client/Resources/Modules/Admin/SystemInfo/Index.resx index 95a0fcc9..ed9f15b6 100644 --- a/Oqtane.Client/Resources/Modules/Admin/SystemInfo/Index.resx +++ b/Oqtane.Client/Resources/Modules/Admin/SystemInfo/Index.resx @@ -129,8 +129,8 @@ Operating System Version - - Server Path + + Server Root Path Server Date/Time (in UTC) @@ -144,8 +144,8 @@ OS Version: - - Server Path: + + Root Path: Server Date/Time: @@ -240,4 +240,34 @@ Notification Level: + + Server IP Address + + + IP Address: + + + Server Machine Name + + + Machine Name: + + + Amount Of Time The Service Has Been Available And Operational + + + Service Uptime: + + + Server Web Root Path + + + Web Path: + + + Memory Allocation Of Service (in MB) + + + Memory Allocation: + \ No newline at end of file diff --git a/Oqtane.Client/Resources/Modules/Admin/Users/Index.resx b/Oqtane.Client/Resources/Modules/Admin/Users/Index.resx index 70155229..a2b74514 100644 --- a/Oqtane.Client/Resources/Modules/Admin/Users/Index.resx +++ b/Oqtane.Client/Resources/Modules/Admin/Users/Index.resx @@ -153,4 +153,55 @@ Roles + + The Number Of Minutes A User Should Be Locked Out + + + Lockout Duration: + + + The Maximum Number Of Sign In Attempts Before A User Is Locked Out + + + Maximum Failures: + + + Indicate If Passwords Must Contain A Digit + + + Require Digit? + + + The Minimum Length For A Password + + + Minimum Length: + + + Indicate If Passwords Must Contain A Lower Case Character + + + Require Lowercase? + + + Indicate if Passwords Must Contain A Non-alphanumeric Character (ie. Punctuation) + + + Require Punctuation? + + + Indicate If Passwords Must Contain An Upper Case Character + + + Require Uppercase? + + + Configuration Updated. Please Select Restart Application For These Changes To Be Activated. + + + The Minimum Number Of Unique Characters Which A Password Must Contain + + + Unique Characters: + \ No newline at end of file diff --git a/Oqtane.Client/Services/Interfaces/ISystemService.cs b/Oqtane.Client/Services/Interfaces/ISystemService.cs index cc7bf00a..8648cc4a 100644 --- a/Oqtane.Client/Services/Interfaces/ISystemService.cs +++ b/Oqtane.Client/Services/Interfaces/ISystemService.cs @@ -9,16 +9,34 @@ namespace Oqtane.Services public interface ISystemService { /// - /// returns a key-value directory with the current system information (os-version, clr-version, etc.) + /// returns a key-value directory with the current system configuration information /// /// - Task> GetSystemInfoAsync(); + Task> GetSystemInfoAsync(); + + /// + /// returns a key-value directory with the current system information - "environment" or "configuration" + /// + /// + Task> GetSystemInfoAsync(string type); + + /// + /// returns a config value + /// + /// + Task GetSystemInfoAsync(string settingKey, object defaultValue); /// /// Updates system information /// /// /// - Task UpdateSystemInfoAsync(Dictionary settings); + Task UpdateSystemInfoAsync(Dictionary settings); + + /// + /// updates a config value + /// + /// + Task UpdateSystemInfoAsync(string settingKey, object settingValue); } } diff --git a/Oqtane.Client/Services/SystemService.cs b/Oqtane.Client/Services/SystemService.cs index ba0f54d0..6bc5a880 100644 --- a/Oqtane.Client/Services/SystemService.cs +++ b/Oqtane.Client/Services/SystemService.cs @@ -18,14 +18,28 @@ namespace Oqtane.Services private string Apiurl => CreateApiUrl("System", _siteState.Alias); - public async Task> GetSystemInfoAsync() + public async Task> GetSystemInfoAsync() { - return await GetJsonAsync>(Apiurl); + return await GetSystemInfoAsync("configuration"); } - public async Task UpdateSystemInfoAsync(Dictionary settings) + public async Task> GetSystemInfoAsync(string type) + { + return await GetJsonAsync>($"{Apiurl}?type={type}"); + } + + public async Task GetSystemInfoAsync(string settingKey, object defaultValue) + { + return await GetJsonAsync($"{Apiurl}/{settingKey}/{defaultValue}"); + } + + public async Task UpdateSystemInfoAsync(Dictionary settings) { await PostJsonAsync(Apiurl, settings); } + public async Task UpdateSystemInfoAsync(string settingKey, object settingValue) + { + await PutJsonAsync($"{Apiurl}/{settingKey}/{settingValue}", ""); + } } } diff --git a/Oqtane.Server/Controllers/SystemController.cs b/Oqtane.Server/Controllers/SystemController.cs index 0991c60f..31035d37 100644 --- a/Oqtane.Server/Controllers/SystemController.cs +++ b/Oqtane.Server/Controllers/SystemController.cs @@ -5,6 +5,7 @@ using Oqtane.Shared; using System; using Microsoft.AspNetCore.Hosting; using Oqtane.Infrastructure; +using Microsoft.AspNetCore.Http.Features; namespace Oqtane.Controllers { @@ -20,62 +21,76 @@ namespace Oqtane.Controllers _configManager = configManager; } - // GET: api/ + // GET: api/?type=x [HttpGet] [Authorize(Roles = RoleNames.Host)] - public Dictionary Get() + public Dictionary Get(string type) { - Dictionary systeminfo = new Dictionary(); + Dictionary systeminfo = new Dictionary(); - systeminfo.Add("clrversion", Environment.Version.ToString()); - systeminfo.Add("osversion", Environment.OSVersion.ToString()); - systeminfo.Add("machinename", Environment.MachineName); - systeminfo.Add("serverpath", _environment.ContentRootPath); - systeminfo.Add("servertime", DateTime.UtcNow.ToString()); - systeminfo.Add("installationid", _configManager.GetInstallationId()); - - systeminfo.Add("runtime", _configManager.GetSetting("Runtime", "Server")); - systeminfo.Add("rendermode", _configManager.GetSetting("RenderMode", "ServerPrerendered")); - systeminfo.Add("detailederrors", _configManager.GetSetting("DetailedErrors", "false")); - systeminfo.Add("logginglevel", _configManager.GetSetting("Logging:LogLevel:Default", "Information")); - systeminfo.Add("notificationlevel", _configManager.GetSetting("Logging:LogLevel:Notify", "Error")); - systeminfo.Add("swagger", _configManager.GetSetting("UseSwagger", "true")); - systeminfo.Add("packageservice", _configManager.GetSetting("PackageService", "true")); + switch (type.ToLower()) + { + case "environment": + systeminfo.Add("CLRVersion", Environment.Version.ToString()); + systeminfo.Add("OSVersion", Environment.OSVersion.ToString()); + systeminfo.Add("MachineName", Environment.MachineName); + systeminfo.Add("WorkingSet", Environment.WorkingSet.ToString()); + systeminfo.Add("TickCount", Environment.TickCount64.ToString()); + systeminfo.Add("ContentRootPath", _environment.ContentRootPath); + systeminfo.Add("WebRootPath", _environment.WebRootPath); + systeminfo.Add("ServerTime", DateTime.UtcNow.ToString()); + var feature = HttpContext.Features.Get(); + systeminfo.Add("IPAddress", feature?.LocalIpAddress?.ToString()); + break; + case "configuration": + systeminfo.Add("InstallationId", _configManager.GetInstallationId()); + systeminfo.Add("Runtime", _configManager.GetSetting("Runtime", "Server")); + systeminfo.Add("RenderMode", _configManager.GetSetting("RenderMode", "ServerPrerendered")); + systeminfo.Add("DetailedErrors", _configManager.GetSetting("DetailedErrors", "false")); + systeminfo.Add("Logging:LogLevel:Default", _configManager.GetSetting("Logging:LogLevel:Default", "Information")); + systeminfo.Add("Logging:LogLevel:Notify", _configManager.GetSetting("Logging:LogLevel:Notify", "Error")); + systeminfo.Add("UseSwagger", _configManager.GetSetting("UseSwagger", "true")); + systeminfo.Add("PackageService", _configManager.GetSetting("PackageService", "true")); + systeminfo.Add("Password:RequiredLength", _configManager.GetSetting("Password:RequiredLength", "6")); + systeminfo.Add("Password:RequiredUniqueChars", _configManager.GetSetting("Password:RequiredUniqueChars", "1")); + systeminfo.Add("Password:RequireDigit", _configManager.GetSetting("Password:RequireDigit", "true")); + systeminfo.Add("Password:RequireUppercase", _configManager.GetSetting("Password:RequireUppercase", "true")); + systeminfo.Add("Password:RequireLowercase", _configManager.GetSetting("Password:RequireLowercase", "true")); + systeminfo.Add("Password:RequireNonAlphanumeric", _configManager.GetSetting("Password:RequireNonAlphanumeric", "true")); + systeminfo.Add("Lockout:MaxFailedAccessAttempts", _configManager.GetSetting("Lockout:MaxFailedAccessAttempts", "5")); + systeminfo.Add("Lockout:DefaultLockoutTimeSpan", _configManager.GetSetting("Lockout:DefaultLockoutTimeSpan", "00:05:00")); + break; + } return systeminfo; } + + // GET: api/ + [HttpGet("{key}/{value}")] + [Authorize(Roles = RoleNames.Host)] + public object Get(string key, object value) + { + return _configManager.GetSetting(key, value); + } + + // POST: api/ [HttpPost] [Authorize(Roles = RoleNames.Host)] - public void Post([FromBody] Dictionary settings) + public void Post([FromBody] Dictionary settings) { - foreach(KeyValuePair kvp in settings) + foreach(KeyValuePair kvp in settings) { - switch (kvp.Key) - { - case "runtime": - _configManager.AddOrUpdateSetting("Runtime", kvp.Value, false); - break; - case "rendermode": - _configManager.AddOrUpdateSetting("RenderMode", kvp.Value, false); - break; - case "detailederrors": - _configManager.AddOrUpdateSetting("DetailedErrors", kvp.Value, false); - break; - case "logginglevel": - _configManager.AddOrUpdateSetting("Logging:LogLevel:Default", kvp.Value, false); - break; - case "notificationlevel": - _configManager.AddOrUpdateSetting("Logging:LogLevel:Notify", kvp.Value, false); - break; - case "swagger": - _configManager.AddOrUpdateSetting("UseSwagger", kvp.Value, false); - break; - case "packageservice": - _configManager.AddOrUpdateSetting("PackageService", kvp.Value, false); - break; - } + _configManager.AddOrUpdateSetting(kvp.Key, kvp.Value, false); } } + + // PUT: api/ + [HttpPut("{key}/{value}")] + [Authorize(Roles = RoleNames.Host)] + public void Put(string key, object value) + { + _configManager.AddOrUpdateSetting(key, value, false); + } } } diff --git a/Oqtane.Server/Controllers/UserController.cs b/Oqtane.Server/Controllers/UserController.cs index d9920953..9128c8aa 100644 --- a/Oqtane.Server/Controllers/UserController.cs +++ b/Oqtane.Server/Controllers/UserController.cs @@ -397,7 +397,7 @@ namespace Oqtane.Controllers user = _users.GetUser(user.Username); string token = await _identityUserManager.GeneratePasswordResetTokenAsync(identityuser); string url = HttpContext.Request.Scheme + "://" + _alias.Name + "/reset?name=" + user.Username + "&token=" + WebUtility.UrlEncode(token); - string body = "Dear " + user.DisplayName + ",\n\nYou attempted 3 times unsuccessfully to log in to your account and it is now locked out. Please wait 10 minutes and then try again... or use the link below to reset your password:\n\n" + url + + string body = "Dear " + user.DisplayName + ",\n\nYou attempted multiple times unsuccessfully to log in to your account and it is now locked out. Please wait a few minutes and then try again... or use the link below to reset your password:\n\n" + url + "\n\nPlease note that the link is only valid for 24 hours so if you are unable to take action within that time period, you should initiate another password reset on the site." + "\n\nThank You!"; var notification = new Notification(loginUser.SiteId, user, "User Lockout", body); diff --git a/Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs b/Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs index 2d1c36a8..15f70c2e 100644 --- a/Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs +++ b/Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs @@ -10,6 +10,7 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.OpenApi.Models; @@ -129,26 +130,36 @@ namespace Microsoft.Extensions.DependencyInjection return services; } - public static IServiceCollection ConfigureOqtaneIdentityOptions(this IServiceCollection services) + public static IServiceCollection ConfigureOqtaneIdentityOptions(this IServiceCollection services, IConfigurationRoot Configuration) { + // default settings services.Configure(options => { // Password settings - options.Password.RequireDigit = false; + options.Password.RequireDigit = true; options.Password.RequiredLength = 6; - options.Password.RequireNonAlphanumeric = false; - options.Password.RequireUppercase = false; - options.Password.RequireLowercase = false; + options.Password.RequireNonAlphanumeric = true; + options.Password.RequireUppercase = true; + options.Password.RequireLowercase = true; + options.Password.RequiredUniqueChars = 1; // Lockout settings - options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(10); - options.Lockout.MaxFailedAccessAttempts = 3; + options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(5); + options.Lockout.MaxFailedAccessAttempts = 5; options.Lockout.AllowedForNewUsers = false; + // SignIn settings + options.SignIn.RequireConfirmedEmail = true; + options.SignIn.RequireConfirmedPhoneNumber = false; + // User settings options.User.RequireUniqueEmail = false; + options.User.AllowedUserNameCharacters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._@+"; }); + // overrides defined in appsettings + services.Configure(Configuration); + return services; } diff --git a/Oqtane.Server/Infrastructure/ConfigManager.cs b/Oqtane.Server/Infrastructure/ConfigManager.cs index 6b1d2349..a9390912 100644 --- a/Oqtane.Server/Infrastructure/ConfigManager.cs +++ b/Oqtane.Server/Infrastructure/ConfigManager.cs @@ -100,7 +100,7 @@ namespace Oqtane.Infrastructure switch (action) { case "set": - jsonObj[currentSection] = value; + jsonObj[currentSection] = JToken.FromObject(value); break; case "remove": if (jsonObj.Property(currentSection) != null) diff --git a/Oqtane.Server/Startup.cs b/Oqtane.Server/Startup.cs index 83cb5a6a..34ca42d1 100644 --- a/Oqtane.Server/Startup.cs +++ b/Oqtane.Server/Startup.cs @@ -86,7 +86,7 @@ namespace Oqtane .AddDefaultTokenProviders() .AddClaimsPrincipalFactory>(); // role claims - services.ConfigureOqtaneIdentityOptions(); + services.ConfigureOqtaneIdentityOptions(Configuration); services.AddAuthentication(Constants.AuthenticationScheme) .AddCookie(Constants.AuthenticationScheme); From b80fe428ac1ed135d16865cb246afc0ead623312 Mon Sep 17 00:00:00 2001 From: Shaun Walker Date: Fri, 4 Mar 2022 11:43:54 -0500 Subject: [PATCH 20/68] add show/hide password toggle on Login form --- Oqtane.Client/Modules/Admin/Login/Index.razor | 43 ++++++++++++++----- .../Resources/Modules/Admin/Login/Index.resx | 6 +++ .../Oqtane.Modules.Admin.Login/Module.css | 4 ++ 3 files changed, 43 insertions(+), 10 deletions(-) diff --git a/Oqtane.Client/Modules/Admin/Login/Index.razor b/Oqtane.Client/Modules/Admin/Login/Index.razor index ec418fd4..3aea28f5 100644 --- a/Oqtane.Client/Modules/Admin/Login/Index.razor +++ b/Oqtane.Client/Modules/Admin/Login/Index.razor @@ -23,11 +23,14 @@ -
+
- +
+ + +
-
+
@@ -64,6 +67,8 @@ private string _username = string.Empty; private ElementReference username; private string _password = string.Empty; + private string _passwordtype = "password"; + private string _togglepassword = string.Empty; private bool _remember = false; private string _code = string.Empty; @@ -78,6 +83,8 @@ protected override async Task OnInitializedAsync() { + _togglepassword = Localizer["ShowPassword"]; + if (PageState.QueryString.ContainsKey("returnurl")) { _returnUrl = PageState.QueryString["returnurl"]; @@ -218,11 +225,27 @@ StateHasChanged(); } - private async Task KeyPressed(KeyboardEventArgs e) - { - if (e.Code == "Enter" || e.Code == "NumpadEnter") - { - await Login(); - } - } + private async Task KeyPressed(KeyboardEventArgs e) + { + if (e.Code == "Enter" || e.Code == "NumpadEnter") + { + await Login(); + } + } + + private void TogglePassword() + { + if (_passwordtype == "password") + { + _passwordtype = "text"; + _togglepassword = Localizer["HidePassword"]; + } + else + { + _passwordtype = "password"; + _togglepassword = Localizer["ShowPassword"]; + } + //StateHasChanged(); + } + } diff --git a/Oqtane.Client/Resources/Modules/Admin/Login/Index.resx b/Oqtane.Client/Resources/Modules/Admin/Login/Index.resx index 6582bb79..61f855d1 100644 --- a/Oqtane.Client/Resources/Modules/Admin/Login/Index.resx +++ b/Oqtane.Client/Resources/Modules/Admin/Login/Index.resx @@ -183,4 +183,10 @@ Username: + + Hide + + + Show + \ No newline at end of file diff --git a/Oqtane.Server/wwwroot/Modules/Oqtane.Modules.Admin.Login/Module.css b/Oqtane.Server/wwwroot/Modules/Oqtane.Modules.Admin.Login/Module.css index e25ff012..a3d63e55 100644 --- a/Oqtane.Server/wwwroot/Modules/Oqtane.Modules.Admin.Login/Module.css +++ b/Oqtane.Server/wwwroot/Modules/Oqtane.Modules.Admin.Login/Module.css @@ -3,3 +3,7 @@ .Oqtane-Modules-Admin-Login .input { width: 200px; } + +.Oqtane-Modules-Admin-Login .password { + width: 270px; +} \ No newline at end of file From fd89254d5a9898dbe3c8ad50077d0389ccbaaa86 Mon Sep 17 00:00:00 2001 From: Shaun Walker Date: Mon, 7 Mar 2022 12:19:00 -0500 Subject: [PATCH 21/68] fix #2041 - Server restart post module install fails with null exception --- .../Infrastructure/Jobs/HostedServiceBase.cs | 20 ++++++++++++++----- Oqtane.Server/Startup.cs | 1 + 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/Oqtane.Server/Infrastructure/Jobs/HostedServiceBase.cs b/Oqtane.Server/Infrastructure/Jobs/HostedServiceBase.cs index 16140a09..f3f70ebe 100644 --- a/Oqtane.Server/Infrastructure/Jobs/HostedServiceBase.cs +++ b/Oqtane.Server/Infrastructure/Jobs/HostedServiceBase.cs @@ -266,17 +266,27 @@ namespace Oqtane.Infrastructure jobs.UpdateJob(job); } } + } + catch + { + // error updating the job + } - if (_executingTask == null) - { - return; - } + // stop called without start + if (_executingTask == null) + { + return; + } + try + { + // force cancellation of the executing method _cancellationTokenSource.Cancel(); } finally { - await Task.WhenAny(_executingTask, Task.Delay(Timeout.Infinite, cancellationToken)); + // wait until the task completes or the stop token triggers + await Task.WhenAny(_executingTask, Task.Delay(Timeout.Infinite, cancellationToken)).ConfigureAwait(false); } } diff --git a/Oqtane.Server/Startup.cs b/Oqtane.Server/Startup.cs index 34ca42d1..c3ec9217 100644 --- a/Oqtane.Server/Startup.cs +++ b/Oqtane.Server/Startup.cs @@ -58,6 +58,7 @@ namespace Oqtane services.AddLocalization(options => options.ResourcesPath = "Resources"); services.AddOptions>().Bind(Configuration.GetSection(SettingKeys.AvailableDatabasesSection)); + services.Configure(opts => opts.ShutdownTimeout = TimeSpan.FromSeconds(10)); // increase from default of 5 seconds services.AddServerSideBlazor() .AddCircuitOptions(options => From 668da6251933e554b9dca2cfc372952d7b67545c Mon Sep 17 00:00:00 2001 From: Shaun Walker Date: Mon, 7 Mar 2022 12:23:35 -0500 Subject: [PATCH 22/68] Fix #2032 - Fresh install with Postgres failed with "42703: column "settingname" does not exist POSITION: 43 --- .../Migrations/Tenant/03000201_UpdateSettingIsPublic.cs | 4 ++-- .../Migrations/Tenant/03000202_UpdateSettingIsPrivate.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Oqtane.Server/Migrations/Tenant/03000201_UpdateSettingIsPublic.cs b/Oqtane.Server/Migrations/Tenant/03000201_UpdateSettingIsPublic.cs index 7599ffd1..2f99e341 100644 --- a/Oqtane.Server/Migrations/Tenant/03000201_UpdateSettingIsPublic.cs +++ b/Oqtane.Server/Migrations/Tenant/03000201_UpdateSettingIsPublic.cs @@ -18,13 +18,13 @@ namespace Oqtane.Migrations.Tenant protected override void Up(MigrationBuilder migrationBuilder) { var settingEntityBuilder = new SettingEntityBuilder(migrationBuilder, ActiveDatabase); - settingEntityBuilder.UpdateColumn("IsPublic", "1", "bool", "SettingName NOT LIKE 'SMTP%'"); + settingEntityBuilder.UpdateColumn("IsPublic", "1", "bool", $"{RewriteName("SettingName")} NOT LIKE 'SMTP%'"); } protected override void Down(MigrationBuilder migrationBuilder) { var settingEntityBuilder = new SettingEntityBuilder(migrationBuilder, ActiveDatabase); - settingEntityBuilder.UpdateColumn("IsPublic", "0", "bool", "SettingName NOT LIKE 'SMTP%'"); + settingEntityBuilder.UpdateColumn("IsPublic", "0", "bool", $"{RewriteName("SettingName")} NOT LIKE 'SMTP%'"); } } } diff --git a/Oqtane.Server/Migrations/Tenant/03000202_UpdateSettingIsPrivate.cs b/Oqtane.Server/Migrations/Tenant/03000202_UpdateSettingIsPrivate.cs index 1d4f25c0..f7e73854 100644 --- a/Oqtane.Server/Migrations/Tenant/03000202_UpdateSettingIsPrivate.cs +++ b/Oqtane.Server/Migrations/Tenant/03000202_UpdateSettingIsPrivate.cs @@ -20,7 +20,7 @@ namespace Oqtane.Migrations.Tenant var settingEntityBuilder = new SettingEntityBuilder(migrationBuilder, ActiveDatabase); settingEntityBuilder.AddBooleanColumn("IsPrivate", true); settingEntityBuilder.UpdateColumn("IsPrivate", "0", "bool", ""); - settingEntityBuilder.UpdateColumn("IsPrivate", "1", "bool", "EntityName = 'Site' AND SettingName LIKE 'SMTP%'"); + settingEntityBuilder.UpdateColumn("IsPrivate", "1", "bool", $"{RewriteName("EntityName")} = 'Site' AND { RewriteName("SettingName")} LIKE 'SMTP%'"); settingEntityBuilder.DropColumn("IsPublic"); } From 003f14003eb80dab0e9ed136ff5e13a6eee852d0 Mon Sep 17 00:00:00 2001 From: Shaun Walker Date: Mon, 7 Mar 2022 16:52:40 -0500 Subject: [PATCH 23/68] Fixed issue with IHostResources not being registered properly --- Oqtane.Client/Modules/Controls/PermissionGrid.razor | 2 +- Oqtane.Client/Services/ServiceBase.cs | 2 +- Oqtane.Server/Pages/_Host.cshtml.cs | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Oqtane.Client/Modules/Controls/PermissionGrid.razor b/Oqtane.Client/Modules/Controls/PermissionGrid.razor index a8d103ef..d1777a68 100644 --- a/Oqtane.Client/Modules/Controls/PermissionGrid.razor +++ b/Oqtane.Client/Modules/Controls/PermissionGrid.razor @@ -252,7 +252,7 @@ for (int i = 0; i < _permissions.Count; i++) { permission = _permissions[i]; - List ids = permission.Permissions.Split(';').ToList(); + List ids = permission.Permissions.Split(';', StringSplitOptions.RemoveEmptyEntries).ToList(); ids.Remove("!" + RoleNames.Everyone); // remove deny all users ids.Remove("!" + RoleNames.Registered); // remove deny registered users if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host)) diff --git a/Oqtane.Client/Services/ServiceBase.cs b/Oqtane.Client/Services/ServiceBase.cs index ecd6c37b..2b2c1df5 100644 --- a/Oqtane.Client/Services/ServiceBase.cs +++ b/Oqtane.Client/Services/ServiceBase.cs @@ -240,7 +240,7 @@ namespace Oqtane.Services [Obsolete("This property of ServiceBase is deprecated. Cross tenant service calls are not supported.", false)] public Alias Alias { get; set; } - [Obsolete("This method is obsolete. Use CreateApiUrl(string entityName, int entityId) instead.", false)] + [Obsolete("This method is obsolete. Use CreateAuthorizationPolicyUrl(string url, string entityName, int entityId) where entityName = EntityNames.Module instead.", false)] public string CreateAuthorizationPolicyUrl(string url, int entityId) { return url + ((url.Contains("?")) ? "&" : "?") + "entityid=" + entityId.ToString(); diff --git a/Oqtane.Server/Pages/_Host.cshtml.cs b/Oqtane.Server/Pages/_Host.cshtml.cs index bfb73379..254cc96d 100644 --- a/Oqtane.Server/Pages/_Host.cshtml.cs +++ b/Oqtane.Server/Pages/_Host.cshtml.cs @@ -395,6 +395,7 @@ namespace Oqtane.Pages var obj = Activator.CreateInstance(type) as IHostResources; foreach (var resource in obj.Resources) { + resource.Declaration = ResourceDeclaration.Global; ProcessResource(resource, 0); } } From f250aff99b43666cba0086eaa22433433e8c6276 Mon Sep 17 00:00:00 2001 From: Shaun Walker Date: Tue, 8 Mar 2022 08:31:18 -0500 Subject: [PATCH 24/68] Added password policy validation in install wizard --- Oqtane.Client/Installer/Installer.razor | 155 +++++++++--------- .../Resources/Installer/Installer.resx | 9 +- .../Services/Interfaces/IUserService.cs | 8 + Oqtane.Client/Services/UserService.cs | 6 + Oqtane.Server/Controllers/UserController.cs | 31 ++-- 5 files changed, 121 insertions(+), 88 deletions(-) diff --git a/Oqtane.Client/Installer/Installer.razor b/Oqtane.Client/Installer/Installer.razor index 75343a49..2dca6de9 100644 --- a/Oqtane.Client/Installer/Installer.razor +++ b/Oqtane.Client/Installer/Installer.razor @@ -121,90 +121,97 @@ { _databaseName = "LocalDB"; } - LoadDatabaseConfigComponent(); - } + LoadDatabaseConfigComponent(); + } - private void DatabaseChanged(ChangeEventArgs eventArgs) - { - try - { - _databaseName = (string)eventArgs.Value; + private void DatabaseChanged(ChangeEventArgs eventArgs) + { + try + { + _databaseName = (string)eventArgs.Value; - LoadDatabaseConfigComponent(); - } - catch - { - _message = Localizer["Error.DbConfig.Load"]; - } - } + LoadDatabaseConfigComponent(); + } + catch + { + _message = Localizer["Error.DbConfig.Load"]; + } + } - private void LoadDatabaseConfigComponent() - { - var database = _databases.SingleOrDefault(d => d.Name == _databaseName); - if (database != null) - { - _databaseConfigType = Type.GetType(database.ControlType); - DatabaseConfigComponent = builder => - { - builder.OpenComponent(0, _databaseConfigType); - builder.AddComponentReferenceCapture(1, inst => { _databaseConfig = Convert.ChangeType(inst, _databaseConfigType); }); - builder.CloseComponent(); - }; - } - } + private void LoadDatabaseConfigComponent() + { + var database = _databases.SingleOrDefault(d => d.Name == _databaseName); + if (database != null) + { + _databaseConfigType = Type.GetType(database.ControlType); + DatabaseConfigComponent = builder => + { + builder.OpenComponent(0, _databaseConfigType); + builder.AddComponentReferenceCapture(1, inst => { _databaseConfig = Convert.ChangeType(inst, _databaseConfigType); }); + builder.CloseComponent(); + }; + } + } - protected override async Task OnAfterRenderAsync(bool firstRender) - { - if (firstRender) - { - var interop = new Interop(JSRuntime); - await interop.IncludeLink("", "stylesheet", "https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.1.3/css/bootstrap.min.css", "text/css", "sha512-GQGU0fMMi238uA+a/bdWJfpUGKUkBdgfFdgBm72SUQ6BeyWjoY/ton0tEjH+OSH9iP4Dfh+7HM0I9f5eR0L/4w==", "anonymous", ""); - await interop.IncludeScript("", "https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.1.3/js/bootstrap.bundle.min.js", "sha512-pax4MlgXjHEPfCwcJLQhigY7+N8rt6bVvWLFyUMuxShv170X53TRzGPmPkZmGBhk+jikR8WBM4yl7A9WMHHqvg==", "anonymous", "", "head", ""); - } - } + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + { + var interop = new Interop(JSRuntime); + await interop.IncludeLink("", "stylesheet", "https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.1.3/css/bootstrap.min.css", "text/css", "sha512-GQGU0fMMi238uA+a/bdWJfpUGKUkBdgfFdgBm72SUQ6BeyWjoY/ton0tEjH+OSH9iP4Dfh+7HM0I9f5eR0L/4w==", "anonymous", ""); + await interop.IncludeScript("", "https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.1.3/js/bootstrap.bundle.min.js", "sha512-pax4MlgXjHEPfCwcJLQhigY7+N8rt6bVvWLFyUMuxShv170X53TRzGPmPkZmGBhk+jikR8WBM4yl7A9WMHHqvg==", "anonymous", "", "head", ""); + } + } - private async Task Install() - { - var connectionString = String.Empty; - if (_databaseConfig is IDatabaseConfigControl databaseConfigControl) - { - connectionString = databaseConfigControl.GetConnectionString(); - } + private async Task Install() + { + var connectionString = String.Empty; + if (_databaseConfig is IDatabaseConfigControl databaseConfigControl) + { + connectionString = databaseConfigControl.GetConnectionString(); + } - if (connectionString != "" && !string.IsNullOrEmpty(_hostUsername) && _hostPassword.Length >= 6 && _hostPassword == _confirmPassword && !string.IsNullOrEmpty(_hostEmail) && _hostEmail.Contains("@")) - { - _loadingDisplay = ""; - StateHasChanged(); + if (connectionString != "" && !string.IsNullOrEmpty(_hostUsername) && !string.IsNullOrEmpty(_hostPassword) && _hostPassword == _confirmPassword && !string.IsNullOrEmpty(_hostEmail) && _hostEmail.Contains("@")) + { + if (await UserService.ValidatePasswordAsync(_hostPassword)) + { + _loadingDisplay = ""; + StateHasChanged(); - Uri uri = new Uri(NavigationManager.Uri); + Uri uri = new Uri(NavigationManager.Uri); - var database = _databases.SingleOrDefault(d => d.Name == _databaseName); + var database = _databases.SingleOrDefault(d => d.Name == _databaseName); - var config = new InstallConfig - { - DatabaseType = database.DBType, - ConnectionString = connectionString, - Aliases = uri.Authority, - HostUsername = _hostUsername, - HostPassword = _hostPassword, - HostEmail = _hostEmail, - HostName = _hostUsername, - TenantName = TenantNames.Master, - IsNewTenant = true, - SiteName = Constants.DefaultSite, - Register = _register - }; + var config = new InstallConfig + { + DatabaseType = database.DBType, + ConnectionString = connectionString, + Aliases = uri.Authority, + HostUsername = _hostUsername, + HostPassword = _hostPassword, + HostEmail = _hostEmail, + HostName = _hostUsername, + TenantName = TenantNames.Master, + IsNewTenant = true, + SiteName = Constants.DefaultSite, + Register = _register + }; - var installation = await InstallationService.Install(config); - if (installation.Success) - { - NavigationManager.NavigateTo(uri.Scheme + "://" + uri.Authority, true); - } - else - { - _message = installation.Message; - _loadingDisplay = "display: none;"; - } + var installation = await InstallationService.Install(config); + if (installation.Success) + { + NavigationManager.NavigateTo(uri.Scheme + "://" + uri.Authority, true); + } + else + { + _message = installation.Message; + _loadingDisplay = "display: none;"; + } + } + else + { + _message = Localizer["Message.Password.Invalid"]; + } } else { diff --git a/Oqtane.Client/Resources/Installer/Installer.resx b/Oqtane.Client/Resources/Installer/Installer.resx index ecbce69e..740ba227 100644 --- a/Oqtane.Client/Resources/Installer/Installer.resx +++ b/Oqtane.Client/Resources/Installer/Installer.resx @@ -130,12 +130,15 @@ Install Now - Error loading Database Configuration Control + Error Loading Database Configuration Control - Please Enter All Required Fields. Ensure Passwords Match And Are Greater Than 5 Characters In Length. Ensure Email Address Provided Is Valid. + Please Enter All Required Fields. Ensure Passwords Match And Email Address Provided Is Valid. - + + The Password Provided Does Not Meet The Password Policy. Please Verify The Minimum Password Length And Complexity Requirements. + + Please Register Me For Major Product Updates And Security Bulletins diff --git a/Oqtane.Client/Services/Interfaces/IUserService.cs b/Oqtane.Client/Services/Interfaces/IUserService.cs index 28258449..483e5284 100644 --- a/Oqtane.Client/Services/Interfaces/IUserService.cs +++ b/Oqtane.Client/Services/Interfaces/IUserService.cs @@ -96,5 +96,13 @@ namespace Oqtane.Services /// /// Task VerifyTwoFactorAsync(User user, string token); + + /// + /// Validate a users password against the password policy + /// + /// + /// + Task ValidatePasswordAsync(string password); + } } diff --git a/Oqtane.Client/Services/UserService.cs b/Oqtane.Client/Services/UserService.cs index 77a83005..f8eeeef7 100644 --- a/Oqtane.Client/Services/UserService.cs +++ b/Oqtane.Client/Services/UserService.cs @@ -3,6 +3,7 @@ using Oqtane.Models; using System.Net.Http; using System.Threading.Tasks; using Oqtane.Documentation; +using System.Net; namespace Oqtane.Services { @@ -73,5 +74,10 @@ namespace Oqtane.Services { return await PostJsonAsync($"{Apiurl}/twofactor?token={token}", user); } + + public async Task ValidatePasswordAsync(string password) + { + return await GetJsonAsync($"{Apiurl}/validate/{WebUtility.UrlEncode(password)}"); + } } } diff --git a/Oqtane.Server/Controllers/UserController.cs b/Oqtane.Server/Controllers/UserController.cs index 9128c8aa..4f938c69 100644 --- a/Oqtane.Server/Controllers/UserController.cs +++ b/Oqtane.Server/Controllers/UserController.cs @@ -26,12 +26,12 @@ namespace Oqtane.Controllers private readonly IUserRoleRepository _userRoles; private readonly UserManager _identityUserManager; private readonly SignInManager _identitySignInManager; + private readonly ITenantManager _tenantManager; private readonly INotificationRepository _notifications; private readonly IFolderRepository _folders; private readonly ISyncManager _syncManager; private readonly ISiteRepository _sites; private readonly ILogManager _logger; - private readonly Alias _alias; public UserController(IUserRepository users, IRoleRepository roles, IUserRoleRepository userRoles, UserManager identityUserManager, SignInManager identitySignInManager, ITenantManager tenantManager, INotificationRepository notifications, IFolderRepository folders, ISyncManager syncManager, ISiteRepository sites, ILogManager logger) { @@ -40,12 +40,12 @@ namespace Oqtane.Controllers _userRoles = userRoles; _identityUserManager = identityUserManager; _identitySignInManager = identitySignInManager; + _tenantManager = tenantManager; _folders = folders; _notifications = notifications; _syncManager = syncManager; _sites = sites; _logger = logger; - _alias = tenantManager.GetAlias(); } // GET api//5?siteid=x @@ -54,7 +54,7 @@ namespace Oqtane.Controllers public User Get(int id, string siteid) { int SiteId; - if (int.TryParse(siteid, out SiteId) && SiteId == _alias.SiteId) + if (int.TryParse(siteid, out SiteId) && SiteId == _tenantManager.GetAlias().SiteId) { User user = _users.GetUser(id); if (user != null) @@ -77,7 +77,7 @@ namespace Oqtane.Controllers public User Get(string name, string siteid) { int SiteId; - if (int.TryParse(siteid, out SiteId) && SiteId == _alias.SiteId) + if (int.TryParse(siteid, out SiteId) && SiteId == _tenantManager.GetAlias().SiteId) { User user = _users.GetUser(name); if (user != null) @@ -129,7 +129,7 @@ namespace Oqtane.Controllers [HttpPost] public async Task Post([FromBody] User user) { - if (ModelState.IsValid && user.SiteId == _alias.SiteId) + if (ModelState.IsValid && user.SiteId == _tenantManager.GetAlias().SiteId) { var User = await CreateUser(user); return User; @@ -178,7 +178,7 @@ namespace Oqtane.Controllers if (!verified) { string token = await _identityUserManager.GenerateEmailConfirmationTokenAsync(identityuser); - string url = HttpContext.Request.Scheme + "://" + _alias.Name + "/login?name=" + user.Username + "&token=" + WebUtility.UrlEncode(token); + string url = HttpContext.Request.Scheme + "://" + _tenantManager.GetAlias().Name + "/login?name=" + user.Username + "&token=" + WebUtility.UrlEncode(token); string body = "Dear " + user.DisplayName + ",\n\nIn Order To Complete The Registration Of Your User Account Please Click The Link Displayed Below:\n\n" + url + "\n\nThank You!"; var notification = new Notification(user.SiteId, newUser, "User Account Verification", body); _notifications.AddNotification(notification); @@ -252,7 +252,7 @@ namespace Oqtane.Controllers [Authorize] public async Task Put(int id, [FromBody] User user) { - if (ModelState.IsValid && user.SiteId == _alias.SiteId && _users.GetUser(user.UserId, false) != null && (User.IsInRole(RoleNames.Admin) || User.Identity.Name == user.Username)) + if (ModelState.IsValid && user.SiteId == _tenantManager.GetAlias().SiteId && _users.GetUser(user.UserId, false) != null && (User.IsInRole(RoleNames.Admin) || User.Identity.Name == user.Username)) { if (user.Password != "") { @@ -264,7 +264,7 @@ namespace Oqtane.Controllers } } user = _users.UpdateUser(user); - _syncManager.AddSyncEvent(_alias.TenantId, EntityNames.User, user.UserId); + _syncManager.AddSyncEvent(_tenantManager.GetAlias().TenantId, EntityNames.User, user.UserId); user.Password = ""; // remove sensitive information _logger.Log(LogLevel.Information, this, LogFunction.Update, "User Updated {User}", user); } @@ -285,7 +285,7 @@ namespace Oqtane.Controllers { int SiteId; User user = _users.GetUser(id); - if (user != null && int.TryParse(siteid, out SiteId) && SiteId == _alias.SiteId) + if (user != null && int.TryParse(siteid, out SiteId) && SiteId == _tenantManager.GetAlias().SiteId) { // remove user roles for site foreach (UserRole userrole in _userRoles.GetUserRoles(user.UserId, SiteId).ToList()) @@ -396,7 +396,7 @@ namespace Oqtane.Controllers { user = _users.GetUser(user.Username); string token = await _identityUserManager.GeneratePasswordResetTokenAsync(identityuser); - string url = HttpContext.Request.Scheme + "://" + _alias.Name + "/reset?name=" + user.Username + "&token=" + WebUtility.UrlEncode(token); + string url = HttpContext.Request.Scheme + "://" + _tenantManager.GetAlias().Name + "/reset?name=" + user.Username + "&token=" + WebUtility.UrlEncode(token); string body = "Dear " + user.DisplayName + ",\n\nYou attempted multiple times unsuccessfully to log in to your account and it is now locked out. Please wait a few minutes and then try again... or use the link below to reset your password:\n\n" + url + "\n\nPlease note that the link is only valid for 24 hours so if you are unable to take action within that time period, you should initiate another password reset on the site." + "\n\nThank You!"; @@ -464,7 +464,7 @@ namespace Oqtane.Controllers { user = _users.GetUser(user.Username); string token = await _identityUserManager.GeneratePasswordResetTokenAsync(identityuser); - string url = HttpContext.Request.Scheme + "://" + _alias.Name + "/reset?name=" + user.Username + "&token=" + WebUtility.UrlEncode(token); + string url = HttpContext.Request.Scheme + "://" + _tenantManager.GetAlias().Name + "/reset?name=" + user.Username + "&token=" + WebUtility.UrlEncode(token); string body = "Dear " + user.DisplayName + ",\n\nYou recently requested to reset your password. Please use the link below to complete the process:\n\n" + url + "\n\nPlease note that the link is only valid for 24 hours so if you are unable to take action within that time period, you should initiate another password reset on the site." + "\n\nIf you did not request to reset your password you can safely ignore this message." + @@ -532,6 +532,15 @@ namespace Oqtane.Controllers return loginUser; } + // GET api//validate/x + [HttpGet("validate/{password}")] + public async Task Validate(string password) + { + var validator = new PasswordValidator(); + var result = await validator.ValidateAsync(_identityUserManager, null, password); + return result.Succeeded; + } + // GET api//authenticate [HttpGet("authenticate")] public User Authenticate() From 432429026b8663983c5ea30a65946548ec47e9d3 Mon Sep 17 00:00:00 2001 From: Leigh Pointer Date: Wed, 9 Mar 2022 14:27:14 +0100 Subject: [PATCH 25/68] Fix for File Upload Failed {Error} .json file #2052 Udate to FileUpload Constant added extensions json, xml, xslt, rss, html, htm, css This is an interim fix with plans to make the upload extensions a soft implementation. --- Oqtane.Shared/Shared/Constants.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Oqtane.Shared/Shared/Constants.cs b/Oqtane.Shared/Shared/Constants.cs index 50848d85..9f91737c 100644 --- a/Oqtane.Shared/Shared/Constants.cs +++ b/Oqtane.Shared/Shared/Constants.cs @@ -63,7 +63,7 @@ namespace Oqtane.Shared { public const string RegisteredRole = RoleNames.Registered; public const string ImageFiles = "jpg,jpeg,jpe,gif,bmp,png,ico,webp"; - public const string UploadableFiles = ImageFiles + ",mov,wmv,avi,mp4,mp3,doc,docx,xls,xlsx,ppt,pptx,pdf,txt,zip,nupkg,csv"; + public const string UploadableFiles = ImageFiles + ",mov,wmv,avi,mp4,mp3,doc,docx,xls,xlsx,ppt,pptx,pdf,txt,zip,nupkg,csv,json,xml,xslt,rss,html,htm,css"; public const string ReservedDevices = "CON,NUL,PRN,COM0,COM1,COM2,COM3,COM4,COM5,COM6,COM7,COM8,COM9,LPT0,LPT1,LPT2,LPT3,LPT4,LPT5,LPT6,LPT7,LPT8,LPT9,CONIN$,CONOUT$"; public static readonly char[] InvalidFileNameChars = From 9bbbff31f835730f5a424e08d75c1f664837c465 Mon Sep 17 00:00:00 2001 From: Shaun Walker Date: Sun, 13 Mar 2022 22:55:52 -0400 Subject: [PATCH 26/68] Added support for per site options and OpenID Connect --- Oqtane.Client/Modules/Admin/Login/Index.razor | 19 +- Oqtane.Client/Modules/Admin/Users/Index.razor | 242 ++++++++++-------- .../Themes/Controls/Theme/LoginBase.cs | 2 +- Oqtane.Server/Controllers/SystemController.cs | 8 - .../Extensions/HttpContextExtensions.cs | 18 ++ .../OqtaneServiceCollectionExtensions.cs | 20 +- ...taneSiteAuthenticationBuilderExtensions.cs | 233 +++++++++++++++++ .../OqtaneSiteIdentityBuilderExtensions.cs | 57 +++++ .../Extensions/OqtaneSiteOptionsBuilder.cs | 39 +++ Oqtane.Server/Extensions/StringExtensions.cs | 4 +- Oqtane.Server/Infrastructure/AliasAccessor.cs | 18 ++ .../Interfaces/IAliasAccessor.cs | 9 + .../SiteAuthenticationSchemeProvider.cs | 112 ++++++++ .../Internal/SiteAuthenticationService.cs | 62 +++++ .../Middleware/TenantMiddleware.cs | 26 +- .../Infrastructure/Options/ISiteOptions.cs | 12 + .../Infrastructure/Options/SiteOptions.cs | 22 ++ .../Options/SiteOptionsCache.cs | 70 +++++ .../Options/SiteOptionsFactory.cs | 77 ++++++ .../Options/SiteOptionsManager.cs | 35 +++ Oqtane.Server/Infrastructure/TenantManager.cs | 2 +- Oqtane.Server/Oqtane.Server.csproj | 1 + Oqtane.Server/Pages/OIDC.cshtml | 3 + Oqtane.Server/Pages/OIDC.cshtml.cs | 15 ++ Oqtane.Server/Security/PrincipalValidator.cs | 23 +- Oqtane.Server/Startup.cs | 63 +++-- .../Oqtane.Modules.Admin.Login/Module.css | 6 +- Oqtane.Shared/Interfaces/IAlias.cs | 21 ++ Oqtane.Shared/Models/Alias.cs | 20 +- Oqtane.Shared/Shared/Constants.cs | 3 + Oqtane.Shared/Shared/SiteState.cs | 2 +- 31 files changed, 1064 insertions(+), 180 deletions(-) create mode 100644 Oqtane.Server/Extensions/HttpContextExtensions.cs create mode 100644 Oqtane.Server/Extensions/OqtaneSiteAuthenticationBuilderExtensions.cs create mode 100644 Oqtane.Server/Extensions/OqtaneSiteIdentityBuilderExtensions.cs create mode 100644 Oqtane.Server/Extensions/OqtaneSiteOptionsBuilder.cs create mode 100644 Oqtane.Server/Infrastructure/AliasAccessor.cs create mode 100644 Oqtane.Server/Infrastructure/Interfaces/IAliasAccessor.cs create mode 100644 Oqtane.Server/Infrastructure/Internal/SiteAuthenticationSchemeProvider.cs create mode 100644 Oqtane.Server/Infrastructure/Internal/SiteAuthenticationService.cs create mode 100644 Oqtane.Server/Infrastructure/Options/ISiteOptions.cs create mode 100644 Oqtane.Server/Infrastructure/Options/SiteOptions.cs create mode 100644 Oqtane.Server/Infrastructure/Options/SiteOptionsCache.cs create mode 100644 Oqtane.Server/Infrastructure/Options/SiteOptionsFactory.cs create mode 100644 Oqtane.Server/Infrastructure/Options/SiteOptionsManager.cs create mode 100644 Oqtane.Server/Pages/OIDC.cshtml create mode 100644 Oqtane.Server/Pages/OIDC.cshtml.cs create mode 100644 Oqtane.Shared/Interfaces/IAlias.cs diff --git a/Oqtane.Client/Modules/Admin/Login/Index.razor b/Oqtane.Client/Modules/Admin/Login/Index.razor index 3aea28f5..f5b1cf46 100644 --- a/Oqtane.Client/Modules/Admin/Login/Index.razor +++ b/Oqtane.Client/Modules/Admin/Login/Index.razor @@ -18,14 +18,19 @@ @if (!twofactor) {
-
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+

@@ -180,6 +201,9 @@ else private string _authority; private string _clientid; private string _clientsecret; + private string _metadata; + private string _logouturl; + private string _allowsitelogin; public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin; @@ -203,6 +227,9 @@ else _authority = SettingService.GetSetting(settings, "OpenIdConnectOptions:Authority", ""); _clientid = SettingService.GetSetting(settings, "OpenIdConnectOptions:ClientId", ""); _clientsecret = SettingService.GetSetting(settings, "OpenIdConnectOptions:ClientSecret", ""); + _metadata = SettingService.GetSetting(settings, "OpenIdConnectOptions:MetadataAddress", ""); + _logouturl = SettingService.GetSetting(settings, "OpenIdConnectOptions:LogoutUrl", ""); + _allowsitelogin = SettingService.GetSetting(settings, "AllowSiteLogin", "True"); } private List Search(string search) @@ -285,7 +312,11 @@ else settings = SettingService.SetSetting(settings, "OpenIdConnectOptions:Authority", _authority, true); settings = SettingService.SetSetting(settings, "OpenIdConnectOptions:ClientId", _clientid, true); settings = SettingService.SetSetting(settings, "OpenIdConnectOptions:ClientSecret", _clientsecret, true); + settings = SettingService.SetSetting(settings, "OpenIdConnectOptions:MetadataAddress", _metadata, true); + settings = SettingService.SetSetting(settings, "OpenIdConnectOptions:LogoutUrl", _logouturl, true); + settings = SettingService.SetSetting(settings, "AllowSiteLogin", _allowsitelogin, false); await SettingService.UpdateSiteSettingsAsync(settings, site.SiteId); + await SettingService.ClearSiteSettingsCacheAsync(site.SiteId); AddModuleMessage(Localizer["Success.SaveSiteSettings"], MessageType.Success); } @@ -295,5 +326,4 @@ else AddModuleMessage(Localizer["Error.SaveSiteSettings"], MessageType.Error); } } - } diff --git a/Oqtane.Client/Services/Interfaces/ISettingService.cs b/Oqtane.Client/Services/Interfaces/ISettingService.cs index 60099fd5..98f733d1 100644 --- a/Oqtane.Client/Services/Interfaces/ISettingService.cs +++ b/Oqtane.Client/Services/Interfaces/ISettingService.cs @@ -38,6 +38,12 @@ namespace Oqtane.Services /// Task UpdateSiteSettingsAsync(Dictionary siteSettings, int siteId); + /// + /// Clears site option cache + /// + /// + Task ClearSiteSettingsCacheAsync(int siteId); + /// /// Returns a key-value dictionary of all page settings for the given page /// @@ -149,7 +155,6 @@ namespace Oqtane.Services /// Task> GetSettingsAsync(string entityName, int entityId); - /// /// Updates settings for a given entityName and Id /// @@ -166,7 +171,6 @@ namespace Oqtane.Services /// Task GetSettingAsync(string entityName, int settingId); - /// /// Creates a new setting /// diff --git a/Oqtane.Client/Services/SettingService.cs b/Oqtane.Client/Services/SettingService.cs index 1c62a41c..343920d2 100644 --- a/Oqtane.Client/Services/SettingService.cs +++ b/Oqtane.Client/Services/SettingService.cs @@ -42,6 +42,11 @@ namespace Oqtane.Services await UpdateSettingsAsync(siteSettings, EntityNames.Site, siteId); } + public async Task ClearSiteSettingsCacheAsync(int siteId) + { + await DeleteAsync($"{Apiurl}/clear/{siteId}"); + } + public async Task> GetPageSettingsAsync(int pageId) { return await GetSettingsAsync(EntityNames.Page, pageId); diff --git a/Oqtane.Server/Controllers/SettingController.cs b/Oqtane.Server/Controllers/SettingController.cs index 6165a07e..d968e2e9 100644 --- a/Oqtane.Server/Controllers/SettingController.cs +++ b/Oqtane.Server/Controllers/SettingController.cs @@ -8,6 +8,9 @@ using Oqtane.Enums; using Oqtane.Infrastructure; using Oqtane.Repository; using System.Net; +using Microsoft.Extensions.Options; +using Microsoft.AspNetCore.Authentication.OpenIdConnect; +using Microsoft.AspNetCore.Authorization; namespace Oqtane.Controllers { @@ -20,14 +23,16 @@ namespace Oqtane.Controllers private readonly ISyncManager _syncManager; private readonly ILogManager _logger; private readonly Alias _alias; + private readonly IOptionsMonitorCache _optionsMonitorCache; private readonly string _visitorCookie; - public SettingController(ISettingRepository settings, IPageModuleRepository pageModules, IUserPermissions userPermissions, ITenantManager tenantManager, ISyncManager syncManager, ILogManager logger) + public SettingController(ISettingRepository settings, IPageModuleRepository pageModules, IUserPermissions userPermissions, ITenantManager tenantManager, ISyncManager syncManager, IOptionsMonitorCache optionsMonitorCache, ILogManager logger) { _settings = settings; _pageModules = pageModules; _userPermissions = userPermissions; _syncManager = syncManager; + _optionsMonitorCache = optionsMonitorCache; _logger = logger; _alias = tenantManager.GetAlias(); _visitorCookie = "APP_VISITOR_" + _alias.SiteId.ToString(); @@ -131,6 +136,15 @@ namespace Oqtane.Controllers } } + // DELETE api//clear + [HttpDelete("clear/{id}")] + [Authorize(Roles = RoleNames.Admin)] + public void Clear(int id) + { + _optionsMonitorCache.Clear(); + _logger.Log(LogLevel.Information, this, LogFunction.Other, "Site Options Cache Cleared"); + } + private bool IsAuthorized(string entityName, int entityId, string permissionName) { bool authorized = false; diff --git a/Oqtane.Server/Extensions/DictionaryExtensions.cs b/Oqtane.Server/Extensions/DictionaryExtensions.cs new file mode 100644 index 00000000..cdb86173 --- /dev/null +++ b/Oqtane.Server/Extensions/DictionaryExtensions.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; + +namespace Oqtane.Extensions +{ + public static class DictionaryExtensions + { + public static TValue GetValue(this Dictionary dictionary, TKey key, TValue defaultValue, bool nullOrEmptyValueIsValid = false) + { + if (dictionary != null && key != null && dictionary.ContainsKey(key)) + { + if (nullOrEmptyValueIsValid || (dictionary[key] != null && !string.IsNullOrEmpty(dictionary[key].ToString()))) + { + return dictionary[key]; + } + } + return defaultValue; + } + } +} diff --git a/Oqtane.Server/Extensions/OqtaneSiteAuthenticationBuilderExtensions.cs b/Oqtane.Server/Extensions/OqtaneSiteAuthenticationBuilderExtensions.cs index e71a7142..5e9f7d05 100644 --- a/Oqtane.Server/Extensions/OqtaneSiteAuthenticationBuilderExtensions.cs +++ b/Oqtane.Server/Extensions/OqtaneSiteAuthenticationBuilderExtensions.cs @@ -15,6 +15,8 @@ using Oqtane.Repository; using System.IO; using System.Collections.Generic; using Oqtane.Security; +using System.Net; +using Microsoft.AspNetCore.Http; namespace Oqtane.Extensions { @@ -47,38 +49,41 @@ namespace Oqtane.Extensions // site OpenIdConnect options builder.AddSiteOptions((options, alias) => { - if (alias.SiteSettings.ContainsKey("OpenIdConnectOptions:Authority")) - { - options.Authority = alias.SiteSettings["OpenIdConnectOptions:Authority"]; - } - if (alias.SiteSettings.ContainsKey("OpenIdConnectOptions:ClientId")) - { - options.ClientId = alias.SiteSettings["OpenIdConnectOptions:ClientId"]; - } - if (alias.SiteSettings.ContainsKey("OpenIdConnectOptions:ClientSecret")) - { - options.ClientSecret = alias.SiteSettings["OpenIdConnectOptions:ClientSecret"]; - } - // default options options.SignInScheme = Constants.AuthenticationScheme; // identity cookie options.RequireHttpsMetadata = true; + options.SaveTokens = true; + options.GetClaimsFromUserInfoEndpoint = true; + options.CallbackPath = string.IsNullOrEmpty(alias.Path) ? "/signin-oidc" : "/" + alias.Path + "/signin-oidc"; + options.ResponseType = OpenIdConnectResponseType.Code; // authorization code flow + options.ResponseMode = OpenIdConnectResponseMode.FormPost; // recommended as most secure options.UsePkce = true; options.Scope.Add("openid"); // core claims options.Scope.Add("profile"); // name claims options.Scope.Add("email"); // email claim - //options.Scope.Add("offline_access"); // get refresh token - options.SaveTokens = true; - options.GetClaimsFromUserInfoEndpoint = true; - options.CallbackPath = string.IsNullOrEmpty(alias.Path) ? "/signin-oidc" : "/" + alias.Path + "/signin-oidc"; - options.ResponseType = OpenIdConnectResponseType.Code; + //options.Scope.Add("offline_access"); // refresh token + + // cookie config is required to avoid Correlation Failed errors + options.NonceCookie.SameSite = SameSiteMode.Unspecified; + options.CorrelationCookie.SameSite = SameSiteMode.Unspecified; + + // site options + options.Authority = alias.SiteSettings.GetValue("OpenIdConnectOptions:Authority", options.Authority); + options.ClientId = alias.SiteSettings.GetValue("OpenIdConnectOptions:ClientId", options.ClientId); + options.ClientSecret = alias.SiteSettings.GetValue("OpenIdConnectOptions:ClientSecret", options.ClientSecret); + options.MetadataAddress = alias.SiteSettings.GetValue("OpenIdConnectOptions:MetadataAddress", options.MetadataAddress); + + // openid connect events options.Events.OnTokenValidated = OnTokenValidated; + options.Events.OnRedirectToIdentityProvider = OnRedirectToIdentityProvider; + options.Events.OnRedirectToIdentityProviderForSignOut = OnRedirectToIdentityProviderForSignOut; + options.Events.OnRemoteFailure = OnRemoteFailure; }); // site ChallengeScheme options builder.AddSiteOptions((options, alias) => { - if (alias.SiteSettings.ContainsKey("OpenIdConnectOptions:Authority") && !string.IsNullOrEmpty(alias.SiteSettings["OpenIdConnectOptions:Authority"])) + if (alias.SiteSettings.GetValue("OpenIdConnectOptions:Authority", "") != "") { options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme; } @@ -175,7 +180,7 @@ namespace Oqtane.Extensions else { // provider keys do not match - _logger.Log(LogLevel.Error, nameof(OqtaneSiteAuthenticationBuilderExtensions), Enums.LogFunction.Security, "OpenId Connect Server Provider Key Does Not Match For User {Email}", email); + _logger.Log(LogLevel.Error, nameof(OqtaneSiteAuthenticationBuilderExtensions), Enums.LogFunction.Security, "OpenId Connect Provider Key Does Not Match For User {Email}", email); } } else @@ -208,14 +213,53 @@ namespace Oqtane.Extensions List userroles = _userRoles.GetUserRoles(user.UserId, context.HttpContext.GetAlias().SiteId).ToList(); var identity = UserSecurity.CreateClaimsIdentity(context.HttpContext.GetAlias(), user, userroles); principal.AddClaims(identity.Claims); + + // add provider + principal.AddClaim(new Claim("Provider", context.HttpContext.GetAlias().SiteSettings["OpenIdConnectOptions:Authority"])); } } else { - _logger.Log(LogLevel.Information, nameof(OqtaneSiteAuthenticationBuilderExtensions), Enums.LogFunction.Security, "OpenId Connect Server Did Not Return An Email Claim"); + _logger.Log(LogLevel.Information, nameof(OqtaneSiteAuthenticationBuilderExtensions), Enums.LogFunction.Security, "OpenId Connect Provider Did Not Return An Email Claim"); } } + private static Task OnRedirectToIdentityProvider(RedirectContext context) + { + //context.ProtocolMessage.SetParameter("key", "value"); + return Task.CompletedTask; + } + + private static Task OnRedirectToIdentityProviderForSignOut(RedirectContext context) + { + var logoutUrl = context.HttpContext.GetAlias().SiteSettings.GetValue("OpenIdConnectOptions:LogoutUrl", ""); + if (logoutUrl != "") + { + var postLogoutUri = context.Properties.RedirectUri; + if (!string.IsNullOrEmpty(postLogoutUri)) + { + if (postLogoutUri.StartsWith("/")) + { + var request = context.Request; + postLogoutUri = request.Scheme + "://" + request.Host + request.PathBase + postLogoutUri; + } + logoutUrl += $"&returnTo={Uri.EscapeDataString(postLogoutUri)}"; + } + context.Response.Redirect(logoutUrl); + context.HandleResponse(); + } + return Task.CompletedTask; + } + + private static Task OnRemoteFailure(RemoteFailureContext context) + { + var _logger = context.HttpContext.RequestServices.GetRequiredService(); + _logger.Log(LogLevel.Error, nameof(OqtaneSiteAuthenticationBuilderExtensions), Enums.LogFunction.Security, "OpenId Connect Remote Failure {Error}", context.Failure.Message); + context.Response.Redirect(context.Properties.RedirectUri); + context.HandleResponse(); + return Task.CompletedTask; + } + public static bool DecorateService(this IServiceCollection services, params object[] parameters) { var existingService = services.SingleOrDefault(s => s.ServiceType == typeof(TService)); diff --git a/Oqtane.Server/Extensions/OqtaneSiteIdentityBuilderExtensions.cs b/Oqtane.Server/Extensions/OqtaneSiteIdentityBuilderExtensions.cs index 14a766e3..8b9ec5d6 100644 --- a/Oqtane.Server/Extensions/OqtaneSiteIdentityBuilderExtensions.cs +++ b/Oqtane.Server/Extensions/OqtaneSiteIdentityBuilderExtensions.cs @@ -15,40 +15,16 @@ namespace Oqtane.Extensions builder.AddSiteOptions((options, alias) => { // password options - if (alias.SiteSettings.ContainsKey("IdentityOptions:Password:RequiredLength")) - { - options.Password.RequiredLength = int.Parse(alias.SiteSettings["IdentityOptions:Password:RequiredLength"]); - } - if (alias.SiteSettings.ContainsKey("IdentityOptions:Password:RequiredUniqueChars")) - { - options.Password.RequiredUniqueChars = int.Parse(alias.SiteSettings["IdentityOptions:Password:RequiredUniqueChars"]); - } - if (alias.SiteSettings.ContainsKey("IdentityOptions:Password:RequireDigit")) - { - options.Password.RequireDigit = bool.Parse(alias.SiteSettings["IdentityOptions:Password:RequireDigit"]); - } - if (alias.SiteSettings.ContainsKey("IdentityOptions:Password:RequireUppercase")) - { - options.Password.RequireUppercase = bool.Parse(alias.SiteSettings["IdentityOptions:Password:RequireUppercase"]); - } - if (alias.SiteSettings.ContainsKey("IdentityOptions:Password:RequireLowercase")) - { - options.Password.RequireLowercase = bool.Parse(alias.SiteSettings["IdentityOptions:Password:RequireLowercase"]); - } - if (alias.SiteSettings.ContainsKey("IdentityOptions:Password:RequireNonAlphanumeric")) - { - options.Password.RequireNonAlphanumeric = bool.Parse(alias.SiteSettings["IdentityOptions:Password:RequireNonAlphanumeric"]); - } + options.Password.RequiredLength = int.Parse(alias.SiteSettings.GetValue("IdentityOptions:Password:RequiredLength", options.Password.RequiredLength.ToString())); + options.Password.RequiredUniqueChars = int.Parse(alias.SiteSettings.GetValue("IdentityOptions:Password:RequiredUniqueChars", options.Password.RequiredUniqueChars.ToString())); + options.Password.RequireDigit = bool.Parse(alias.SiteSettings.GetValue("IdentityOptions:Password:RequireDigit", options.Password.RequireDigit.ToString())); + options.Password.RequireUppercase = bool.Parse(alias.SiteSettings.GetValue("IdentityOptions:Password:RequireUppercase", options.Password.RequireUppercase.ToString())); + options.Password.RequireLowercase = bool.Parse(alias.SiteSettings.GetValue("IdentityOptions:Password:RequireLowercase", options.Password.RequireLowercase.ToString())); + options.Password.RequireNonAlphanumeric = bool.Parse(alias.SiteSettings.GetValue("IdentityOptions:Password:RequireNonAlphanumeric", options.Password.RequireNonAlphanumeric.ToString())); // lockout options - if (alias.SiteSettings.ContainsKey("IdentityOptions:Password:MaxFailedAccessAttempts")) - { - options.Lockout.MaxFailedAccessAttempts = int.Parse(alias.SiteSettings["IdentityOptions:Password:MaxFailedAccessAttempts"]); - } - if (alias.SiteSettings.ContainsKey("IdentityOptions:Password:DefaultLockoutTimeSpan")) - { - options.Lockout.DefaultLockoutTimeSpan = TimeSpan.Parse(alias.SiteSettings["IdentityOptions:Password:DefaultLockoutTimeSpan"]); - } + options.Lockout.MaxFailedAccessAttempts = int.Parse(alias.SiteSettings.GetValue("IdentityOptions:Password:MaxFailedAccessAttempts", options.Lockout.MaxFailedAccessAttempts.ToString())); + options.Lockout.DefaultLockoutTimeSpan = TimeSpan.Parse(alias.SiteSettings.GetValue("IdentityOptions:Password:DefaultLockoutTimeSpan", options.Lockout.DefaultLockoutTimeSpan.ToString())); }); return builder; diff --git a/Oqtane.Server/Infrastructure/Middleware/TenantMiddleware.cs b/Oqtane.Server/Infrastructure/Middleware/TenantMiddleware.cs index e4238a62..ff16773e 100644 --- a/Oqtane.Server/Infrastructure/Middleware/TenantMiddleware.cs +++ b/Oqtane.Server/Infrastructure/Middleware/TenantMiddleware.cs @@ -1,6 +1,7 @@ using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Caching.Memory; using Oqtane.Repository; using Oqtane.Shared; @@ -27,10 +28,15 @@ namespace Oqtane.Infrastructure if (alias != null) { - // 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); + // get site settings + var cache = context.RequestServices.GetService(typeof(IMemoryCache)) as IMemoryCache; + alias.SiteSettings = cache.GetOrCreate("sitesettings:" + alias.SiteKey, entry => + { + var settingRepository = context.RequestServices.GetService(typeof(ISettingRepository)) as ISettingRepository; + return settingRepository.GetSettings(EntityNames.Site) + .ToDictionary(setting => setting.SettingName, setting => setting.SettingValue); + }); + // save alias in HttpContext context.Items.Add(Constants.HttpContextAliasKey, alias); // rewrite path by removing alias path prefix from api and pages requests (for consistent routing) @@ -42,9 +48,7 @@ namespace Oqtane.Infrastructure context.Request.Path = path.Replace("/" + alias.Path, ""); } } - } - } // continue processing diff --git a/Oqtane.Server/Pages/Logout.cshtml.cs b/Oqtane.Server/Pages/Logout.cshtml.cs index eef4dfb7..ab9ed7ad 100644 --- a/Oqtane.Server/Pages/Logout.cshtml.cs +++ b/Oqtane.Server/Pages/Logout.cshtml.cs @@ -1,32 +1,37 @@ +using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.OpenIdConnect; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; +using Oqtane.Extensions; using Oqtane.Shared; namespace Oqtane.Pages { - [AllowAnonymous] + [Authorize] public class LogoutModel : PageModel { public async Task OnGetAsync(string returnurl) { - if (HttpContext.User.Identity.IsAuthenticated) - { - await HttpContext.SignOutAsync(Constants.AuthenticationScheme); - } + await HttpContext.SignOutAsync(Constants.AuthenticationScheme); - if (returnurl == null) - { - returnurl = ""; - } - if (!returnurl.StartsWith("/")) - { - returnurl = "/" + returnurl; - } + returnurl = (returnurl == null) ? "/" : returnurl; + returnurl = (!returnurl.StartsWith("/")) ? "/" + returnurl : returnurl; - return LocalRedirect(Url.Content("~" + returnurl)); + var provider = HttpContext.User.Claims.FirstOrDefault(item => item.Type == "Provider"); + var authority = HttpContext.GetAlias().SiteSettings.GetValue("OpenIdConnectOptions:Authority", ""); + var logoutUrl = HttpContext.GetAlias().SiteSettings.GetValue("OpenIdConnectOptions:LogoutUrl", ""); + if (provider != null && provider.Value == authority && logoutUrl != "") + { + return new SignOutResult(OpenIdConnectDefaults.AuthenticationScheme, + new AuthenticationProperties { RedirectUri = returnurl }); + } + else + { + return LocalRedirect(Url.Content("~" + returnurl)); + } } } } diff --git a/Oqtane.Server/Pages/OIDC.cshtml.cs b/Oqtane.Server/Pages/OIDC.cshtml.cs index 433d1bea..d70f3931 100644 --- a/Oqtane.Server/Pages/OIDC.cshtml.cs +++ b/Oqtane.Server/Pages/OIDC.cshtml.cs @@ -9,7 +9,11 @@ namespace Oqtane.Pages { public IActionResult OnGetAsync(string returnurl) { - return new ChallengeResult(OpenIdConnectDefaults.AuthenticationScheme, new AuthenticationProperties { RedirectUri = !string.IsNullOrEmpty(returnurl) ? returnurl : "/" }); + returnurl = (returnurl == null) ? "/" : returnurl; + returnurl = (!returnurl.StartsWith("/")) ? "/" + returnurl : returnurl; + + return new ChallengeResult(OpenIdConnectDefaults.AuthenticationScheme, + new AuthenticationProperties { RedirectUri = returnurl }); } } } diff --git a/Oqtane.Server/Repository/SettingRepository.cs b/Oqtane.Server/Repository/SettingRepository.cs index 95b55de6..031acf4d 100644 --- a/Oqtane.Server/Repository/SettingRepository.cs +++ b/Oqtane.Server/Repository/SettingRepository.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Linq; using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Caching.Memory; using Oqtane.Models; using Oqtane.Shared; @@ -10,11 +11,15 @@ namespace Oqtane.Repository { private TenantDBContext _tenant; private MasterDBContext _master; + private readonly SiteState _siteState; + private readonly IMemoryCache _cache; - public SettingRepository(TenantDBContext tenant, MasterDBContext master) + public SettingRepository(TenantDBContext tenant, MasterDBContext master, SiteState siteState, IMemoryCache cache) { _tenant = tenant; _master = master; + _siteState = siteState; + _cache = cache; } public IEnumerable GetSettings(string entityName) @@ -47,6 +52,7 @@ namespace Oqtane.Repository _tenant.Setting.Add(setting); _tenant.SaveChanges(); } + ManageCache(setting.EntityName); return setting; } @@ -62,6 +68,7 @@ namespace Oqtane.Repository _tenant.Entry(setting).State = EntityState.Modified; _tenant.SaveChanges(); } + ManageCache(setting.EntityName); return setting; } @@ -103,6 +110,7 @@ namespace Oqtane.Repository _tenant.Setting.Remove(setting); _tenant.SaveChanges(); } + ManageCache(entityName); } public void DeleteSettings(string entityName, int entityId) @@ -129,11 +137,20 @@ namespace Oqtane.Repository } _tenant.SaveChanges(); } + ManageCache(entityName); } private bool IsMaster(string EntityName) { return (EntityName == EntityNames.ModuleDefinition || EntityName == EntityNames.Host); } + + private void ManageCache(string EntityName) + { + if (EntityName == EntityNames.Site) + { + _cache.Remove("sitesettings:" + _siteState.Alias.SiteKey); + } + } } } From 4b19059df136bf0b9952cbff02f0e656275a954d Mon Sep 17 00:00:00 2001 From: Shaun Walker Date: Mon, 21 Mar 2022 09:12:18 -0400 Subject: [PATCH 31/68] OIDC improvements --- Oqtane.Client/Modules/Admin/Users/Index.razor | 6 +- ...taneSiteAuthenticationBuilderExtensions.cs | 98 ++++----------- .../SiteAuthenticationSchemeProvider.cs | 112 ------------------ .../Internal/SiteAuthenticationService.cs | 62 ---------- .../Middleware/TenantMiddleware.cs | 2 +- Oqtane.Server/Security/PrincipalValidator.cs | 2 +- Oqtane.Shared/Security/UserSecurity.cs | 2 +- 7 files changed, 29 insertions(+), 255 deletions(-) delete mode 100644 Oqtane.Server/Infrastructure/Internal/SiteAuthenticationSchemeProvider.cs delete mode 100644 Oqtane.Server/Infrastructure/Internal/SiteAuthenticationService.cs diff --git a/Oqtane.Client/Modules/Admin/Users/Index.razor b/Oqtane.Client/Modules/Admin/Users/Index.razor index d09866d6..f36ed1dc 100644 --- a/Oqtane.Client/Modules/Admin/Users/Index.razor +++ b/Oqtane.Client/Modules/Admin/Users/Index.razor @@ -170,8 +170,8 @@ else
@@ -229,7 +229,7 @@ else _clientsecret = SettingService.GetSetting(settings, "OpenIdConnectOptions:ClientSecret", ""); _metadata = SettingService.GetSetting(settings, "OpenIdConnectOptions:MetadataAddress", ""); _logouturl = SettingService.GetSetting(settings, "OpenIdConnectOptions:LogoutUrl", ""); - _allowsitelogin = SettingService.GetSetting(settings, "AllowSiteLogin", "True"); + _allowsitelogin = SettingService.GetSetting(settings, "AllowSiteLogin", "true"); } private List Search(string search) diff --git a/Oqtane.Server/Extensions/OqtaneSiteAuthenticationBuilderExtensions.cs b/Oqtane.Server/Extensions/OqtaneSiteAuthenticationBuilderExtensions.cs index 5e9f7d05..09953d86 100644 --- a/Oqtane.Server/Extensions/OqtaneSiteAuthenticationBuilderExtensions.cs +++ b/Oqtane.Server/Extensions/OqtaneSiteAuthenticationBuilderExtensions.cs @@ -2,7 +2,6 @@ using System; using System.Linq; using Microsoft.AspNetCore.Authentication; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.AspNetCore.Authentication.OpenIdConnect; using Oqtane.Infrastructure; using Oqtane.Models; @@ -15,7 +14,6 @@ using Oqtane.Repository; using System.IO; using System.Collections.Generic; using Oqtane.Security; -using System.Net; using Microsoft.AspNetCore.Http; namespace Oqtane.Extensions @@ -26,22 +24,11 @@ namespace Oqtane.Extensions this OqtaneSiteOptionsBuilder builder) where TAlias : class, IAlias, new() { - builder.WithSiteAuthenticationCore(); builder.WithSiteAuthenticationOptions(); return builder; } - public static OqtaneSiteOptionsBuilder WithSiteAuthenticationCore( - this OqtaneSiteOptionsBuilder builder) - where TAlias : class, IAlias, new() - { - builder.Services.DecorateService>(); - builder.Services.Replace(ServiceDescriptor.Singleton()); - - return builder; - } - public static OqtaneSiteOptionsBuilder WithSiteAuthenticationOptions( this OqtaneSiteOptionsBuilder builder) where TAlias : class, IAlias, new() @@ -75,8 +62,8 @@ namespace Oqtane.Extensions // openid connect events options.Events.OnTokenValidated = OnTokenValidated; - options.Events.OnRedirectToIdentityProvider = OnRedirectToIdentityProvider; options.Events.OnRedirectToIdentityProviderForSignOut = OnRedirectToIdentityProviderForSignOut; + options.Events.OnAccessDenied = OnAccessDenied; options.Events.OnRemoteFailure = OnRemoteFailure; }); @@ -97,6 +84,7 @@ namespace Oqtane.Extensions var email = context.Principal.FindFirstValue(ClaimTypes.Email); var providerKey = context.Principal.FindFirstValue(ClaimTypes.NameIdentifier); var loginProvider = context.HttpContext.GetAlias().SiteSettings["OpenIdConnectOptions:Authority"]; + var alias = context.HttpContext.GetAlias(); var _logger = context.HttpContext.RequestServices.GetRequiredService(); if (email != null) @@ -117,10 +105,10 @@ namespace Oqtane.Extensions if (result.Succeeded) { // add user login - await _identityUserManager.AddLoginAsync(identityuser, new UserLoginInfo(loginProvider, providerKey, "")); + await _identityUserManager.AddLoginAsync(identityuser, new UserLoginInfo(loginProvider, providerKey, email)); user = new User(); - user.SiteId = context.HttpContext.GetAlias().SiteId; + user.SiteId = alias.SiteId; user.Username = email; user.DisplayName = email; user.Email = email; @@ -145,11 +133,11 @@ namespace Oqtane.Extensions Capacity = Constants.UserFolderCapacity, IsSystem = true, Permissions = new List - { - new Permission(PermissionNames.Browse, user.UserId, true), - new Permission(PermissionNames.View, RoleNames.Everyone, true), - new Permission(PermissionNames.Edit, user.UserId, true) - }.EncodePermissions() + { + new Permission(PermissionNames.Browse, user.UserId, true), + new Permission(PermissionNames.View, RoleNames.Everyone, true), + new Permission(PermissionNames.Edit, user.UserId, true) + }.EncodePermissions() }); } @@ -210,8 +198,8 @@ namespace Oqtane.Extensions } // add Oqtane claims - List userroles = _userRoles.GetUserRoles(user.UserId, context.HttpContext.GetAlias().SiteId).ToList(); - var identity = UserSecurity.CreateClaimsIdentity(context.HttpContext.GetAlias(), user, userroles); + List userroles = _userRoles.GetUserRoles(user.UserId, user.SiteId).ToList(); + var identity = UserSecurity.CreateClaimsIdentity(alias, user, userroles); principal.AddClaims(identity.Claims); // add provider @@ -224,12 +212,6 @@ namespace Oqtane.Extensions } } - private static Task OnRedirectToIdentityProvider(RedirectContext context) - { - //context.ProtocolMessage.SetParameter("key", "value"); - return Task.CompletedTask; - } - private static Task OnRedirectToIdentityProviderForSignOut(RedirectContext context) { var logoutUrl = context.HttpContext.GetAlias().SiteSettings.GetValue("OpenIdConnectOptions:LogoutUrl", ""); @@ -251,59 +233,25 @@ namespace Oqtane.Extensions return Task.CompletedTask; } - private static Task OnRemoteFailure(RemoteFailureContext context) + private static Task OnAccessDenied(AccessDeniedContext context) { var _logger = context.HttpContext.RequestServices.GetRequiredService(); - _logger.Log(LogLevel.Error, nameof(OqtaneSiteAuthenticationBuilderExtensions), Enums.LogFunction.Security, "OpenId Connect Remote Failure {Error}", context.Failure.Message); - context.Response.Redirect(context.Properties.RedirectUri); + _logger.Log(LogLevel.Information, nameof(OqtaneSiteAuthenticationBuilderExtensions), Enums.LogFunction.Security, "OpenId Connect Access Denied - User May Have Cancelled Their External Login Attempt"); + // redirect to login page + var alias = context.HttpContext.GetAlias(); + context.Response.Redirect(alias.Path + "/login?returnurl=" + context.Properties.RedirectUri); context.HandleResponse(); return Task.CompletedTask; } - public static bool DecorateService(this IServiceCollection services, params object[] parameters) + private static Task OnRemoteFailure(RemoteFailureContext context) { - var existingService = services.SingleOrDefault(s => s.ServiceType == typeof(TService)); - if (existingService == null) - return false; - - var newService = new ServiceDescriptor(existingService.ServiceType, - sp => - { - TService inner = (TService)ActivatorUtilities.CreateInstance(sp, existingService.ImplementationType!); - - var parameters2 = new object[parameters.Length + 1]; - Array.Copy(parameters, 0, parameters2, 1, parameters.Length); - parameters2[0] = inner; - - return ActivatorUtilities.CreateInstance(sp, parameters2)!; - }, - existingService.Lifetime); - - if (existingService.ImplementationInstance != null) - { - newService = new ServiceDescriptor(existingService.ServiceType, - sp => - { - TService inner = (TService)existingService.ImplementationInstance; - return ActivatorUtilities.CreateInstance(sp, inner, parameters)!; - }, - existingService.Lifetime); - } - else if (existingService.ImplementationFactory != null) - { - newService = new ServiceDescriptor(existingService.ServiceType, - sp => - { - TService inner = (TService)existingService.ImplementationFactory(sp); - return ActivatorUtilities.CreateInstance(sp, inner, parameters)!; - }, - existingService.Lifetime); - } - - services.Remove(existingService); - services.Add(newService); - - return true; + var _logger = context.HttpContext.RequestServices.GetRequiredService(); + _logger.Log(LogLevel.Error, nameof(OqtaneSiteAuthenticationBuilderExtensions), Enums.LogFunction.Security, "OpenId Connect Remote Failure - {Error}", context.Failure.Message); + // redirect to original page + context.Response.Redirect(context.Properties.RedirectUri); + context.HandleResponse(); + return Task.CompletedTask; } } } diff --git a/Oqtane.Server/Infrastructure/Internal/SiteAuthenticationSchemeProvider.cs b/Oqtane.Server/Infrastructure/Internal/SiteAuthenticationSchemeProvider.cs deleted file mode 100644 index 359e8183..00000000 --- a/Oqtane.Server/Infrastructure/Internal/SiteAuthenticationSchemeProvider.cs +++ /dev/null @@ -1,112 +0,0 @@ -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 options) - : this(options, new Dictionary(StringComparer.Ordinal)) - { - } - - public SiteAuthenticationSchemeProvider(IOptions options, IDictionary schemes) - { - _optionsProvider = options; - - _schemes = schemes ?? throw new ArgumentNullException(nameof(schemes)); - _requestHandlers = new List(); - - foreach (var builder in _optionsProvider.Value.Schemes) - { - var scheme = builder.Build(); - AddScheme(scheme); - } - } - - private readonly IOptions _optionsProvider; - private readonly object _lock = new object(); - - private readonly IDictionary _schemes; - private readonly List _requestHandlers; - - private Task GetDefaultSchemeAsync() - => _optionsProvider.Value.DefaultScheme != null - ? GetSchemeAsync(_optionsProvider.Value.DefaultScheme) - : Task.FromResult(null); - - public virtual Task GetDefaultAuthenticateSchemeAsync() - => _optionsProvider.Value.DefaultAuthenticateScheme != null - ? GetSchemeAsync(_optionsProvider.Value.DefaultAuthenticateScheme) - : GetDefaultSchemeAsync(); - - public virtual Task GetDefaultChallengeSchemeAsync() - => _optionsProvider.Value.DefaultChallengeScheme != null - ? GetSchemeAsync(_optionsProvider.Value.DefaultChallengeScheme) - : GetDefaultSchemeAsync(); - - public virtual Task GetDefaultForbidSchemeAsync() - => _optionsProvider.Value.DefaultForbidScheme != null - ? GetSchemeAsync(_optionsProvider.Value.DefaultForbidScheme) - : GetDefaultChallengeSchemeAsync(); - - public virtual Task GetDefaultSignInSchemeAsync() - => _optionsProvider.Value.DefaultSignInScheme != null - ? GetSchemeAsync(_optionsProvider.Value.DefaultSignInScheme) - : GetDefaultSchemeAsync(); - - public virtual Task GetDefaultSignOutSchemeAsync() - => _optionsProvider.Value.DefaultSignOutScheme != null - ? GetSchemeAsync(_optionsProvider.Value.DefaultSignOutScheme) - : GetDefaultSignInSchemeAsync(); - - public virtual Task GetSchemeAsync(string name) - => Task.FromResult(_schemes.ContainsKey(name) ? _schemes[name] : null); - - public virtual Task> GetRequestHandlerSchemesAsync() - => Task.FromResult>(_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> GetAllSchemesAsync() - => Task.FromResult>(_schemes.Values); - } -} diff --git a/Oqtane.Server/Infrastructure/Internal/SiteAuthenticationService.cs b/Oqtane.Server/Infrastructure/Internal/SiteAuthenticationService.cs deleted file mode 100644 index 40ccb519..00000000 --- a/Oqtane.Server/Infrastructure/Internal/SiteAuthenticationService.cs +++ /dev/null @@ -1,62 +0,0 @@ -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 : 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 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); - } - } -} diff --git a/Oqtane.Server/Infrastructure/Middleware/TenantMiddleware.cs b/Oqtane.Server/Infrastructure/Middleware/TenantMiddleware.cs index ff16773e..9a06903f 100644 --- a/Oqtane.Server/Infrastructure/Middleware/TenantMiddleware.cs +++ b/Oqtane.Server/Infrastructure/Middleware/TenantMiddleware.cs @@ -33,7 +33,7 @@ namespace Oqtane.Infrastructure alias.SiteSettings = cache.GetOrCreate("sitesettings:" + alias.SiteKey, entry => { var settingRepository = context.RequestServices.GetService(typeof(ISettingRepository)) as ISettingRepository; - return settingRepository.GetSettings(EntityNames.Site) + return settingRepository.GetSettings(EntityNames.Site, alias.SiteId) .ToDictionary(setting => setting.SettingName, setting => setting.SettingValue); }); // save alias in HttpContext diff --git a/Oqtane.Server/Security/PrincipalValidator.cs b/Oqtane.Server/Security/PrincipalValidator.cs index 5a45c820..ea668689 100644 --- a/Oqtane.Server/Security/PrincipalValidator.cs +++ b/Oqtane.Server/Security/PrincipalValidator.cs @@ -24,7 +24,7 @@ namespace Oqtane.Security if (alias != null) { // verify principal was authenticated for current tenant - if (context.Principal.Claims.FirstOrDefault(item => item.Type == ClaimTypes.GroupSid)?.Value != alias.AliasId.ToString()) + if (context.Principal.Claims.FirstOrDefault(item => item.Type == ClaimTypes.GroupSid)?.Value != alias.SiteKey) { // tenant agnostic requests must be ignored string path = context.Request.Path.ToString().ToLower(); diff --git a/Oqtane.Shared/Security/UserSecurity.cs b/Oqtane.Shared/Security/UserSecurity.cs index c052e903..7ef1ea55 100644 --- a/Oqtane.Shared/Security/UserSecurity.cs +++ b/Oqtane.Shared/Security/UserSecurity.cs @@ -134,7 +134,7 @@ namespace Oqtane.Security { identity.AddClaim(new Claim(ClaimTypes.Name, user.Username)); identity.AddClaim(new Claim(ClaimTypes.PrimarySid, user.UserId.ToString())); - identity.AddClaim(new Claim(ClaimTypes.GroupSid, alias.AliasId.ToString())); + identity.AddClaim(new Claim(ClaimTypes.GroupSid, alias.SiteKey)); if (user.Roles.Contains(RoleNames.Host)) { // host users are site admins by default From fb161ae783bf57666a64d7840dfaeeb97633999b Mon Sep 17 00:00:00 2001 From: Shaun Walker Date: Mon, 21 Mar 2022 10:39:35 -0400 Subject: [PATCH 32/68] OIDC improvements --- Oqtane.Client/Modules/Admin/Users/Index.razor | 8 ++++++++ .../OqtaneSiteAuthenticationBuilderExtensions.cs | 15 +++++++++------ 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/Oqtane.Client/Modules/Admin/Users/Index.razor b/Oqtane.Client/Modules/Admin/Users/Index.razor index f36ed1dc..0a05bcf8 100644 --- a/Oqtane.Client/Modules/Admin/Users/Index.razor +++ b/Oqtane.Client/Modules/Admin/Users/Index.razor @@ -154,6 +154,12 @@ else
+
+ +
+ +
+
@@ -201,6 +207,7 @@ else private string _authority; private string _clientid; private string _clientsecret; + private string _redirecturl; private string _metadata; private string _logouturl; private string _allowsitelogin; @@ -227,6 +234,7 @@ else _authority = SettingService.GetSetting(settings, "OpenIdConnectOptions:Authority", ""); _clientid = SettingService.GetSetting(settings, "OpenIdConnectOptions:ClientId", ""); _clientsecret = SettingService.GetSetting(settings, "OpenIdConnectOptions:ClientSecret", ""); + _redirecturl = PageState.Alias.Name + "/signin-oidc"; _metadata = SettingService.GetSetting(settings, "OpenIdConnectOptions:MetadataAddress", ""); _logouturl = SettingService.GetSetting(settings, "OpenIdConnectOptions:LogoutUrl", ""); _allowsitelogin = SettingService.GetSetting(settings, "AllowSiteLogin", "true"); diff --git a/Oqtane.Server/Extensions/OqtaneSiteAuthenticationBuilderExtensions.cs b/Oqtane.Server/Extensions/OqtaneSiteAuthenticationBuilderExtensions.cs index 09953d86..b6a5f38e 100644 --- a/Oqtane.Server/Extensions/OqtaneSiteAuthenticationBuilderExtensions.cs +++ b/Oqtane.Server/Extensions/OqtaneSiteAuthenticationBuilderExtensions.cs @@ -81,12 +81,14 @@ namespace Oqtane.Extensions private static async Task OnTokenValidated(TokenValidatedContext context) { - var email = context.Principal.FindFirstValue(ClaimTypes.Email); var providerKey = context.Principal.FindFirstValue(ClaimTypes.NameIdentifier); var loginProvider = context.HttpContext.GetAlias().SiteSettings["OpenIdConnectOptions:Authority"]; var alias = context.HttpContext.GetAlias(); var _logger = context.HttpContext.RequestServices.GetRequiredService(); + // custom logic may be needed here to manipulate Principal sent by Provider - use interface similar to IClaimsTransformation + + var email = context.Principal.FindFirstValue(ClaimTypes.Email); if (email != null) { var _identityUserManager = context.HttpContext.RequestServices.GetRequiredService>(); @@ -208,7 +210,7 @@ namespace Oqtane.Extensions } else { - _logger.Log(LogLevel.Information, nameof(OqtaneSiteAuthenticationBuilderExtensions), Enums.LogFunction.Security, "OpenId Connect Provider Did Not Return An Email Claim"); + _logger.Log(LogLevel.Information, nameof(OqtaneSiteAuthenticationBuilderExtensions), Enums.LogFunction.Security, "OpenID Connect Provider Did Not Return An Email Claim To Uniquely Identify The User"); } } @@ -236,7 +238,7 @@ namespace Oqtane.Extensions private static Task OnAccessDenied(AccessDeniedContext context) { var _logger = context.HttpContext.RequestServices.GetRequiredService(); - _logger.Log(LogLevel.Information, nameof(OqtaneSiteAuthenticationBuilderExtensions), Enums.LogFunction.Security, "OpenId Connect Access Denied - User May Have Cancelled Their External Login Attempt"); + _logger.Log(LogLevel.Information, nameof(OqtaneSiteAuthenticationBuilderExtensions), Enums.LogFunction.Security, "OpenID Connect Access Denied - User May Have Cancelled Their External Login Attempt"); // redirect to login page var alias = context.HttpContext.GetAlias(); context.Response.Redirect(alias.Path + "/login?returnurl=" + context.Properties.RedirectUri); @@ -247,9 +249,10 @@ namespace Oqtane.Extensions private static Task OnRemoteFailure(RemoteFailureContext context) { var _logger = context.HttpContext.RequestServices.GetRequiredService(); - _logger.Log(LogLevel.Error, nameof(OqtaneSiteAuthenticationBuilderExtensions), Enums.LogFunction.Security, "OpenId Connect Remote Failure - {Error}", context.Failure.Message); - // redirect to original page - context.Response.Redirect(context.Properties.RedirectUri); + _logger.Log(LogLevel.Error, nameof(OqtaneSiteAuthenticationBuilderExtensions), Enums.LogFunction.Security, "OpenID Connect Remote Failure - {Error}", context.Failure.Message); + // redirect to login page + var alias = context.HttpContext.GetAlias(); + context.Response.Redirect(alias.Path + "/login?returnurl=" + context.Properties.RedirectUri); context.HandleResponse(); return Task.CompletedTask; } From 76fc689337c823d24f7e9541abb09e0556dcc9ea Mon Sep 17 00:00:00 2001 From: Shaun Walker Date: Mon, 21 Mar 2022 10:50:01 -0400 Subject: [PATCH 33/68] Add scheme to Redirect Url --- Oqtane.Client/Modules/Admin/Users/Index.razor | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Oqtane.Client/Modules/Admin/Users/Index.razor b/Oqtane.Client/Modules/Admin/Users/Index.razor index 0a05bcf8..e2465bd2 100644 --- a/Oqtane.Client/Modules/Admin/Users/Index.razor +++ b/Oqtane.Client/Modules/Admin/Users/Index.razor @@ -234,7 +234,7 @@ else _authority = SettingService.GetSetting(settings, "OpenIdConnectOptions:Authority", ""); _clientid = SettingService.GetSetting(settings, "OpenIdConnectOptions:ClientId", ""); _clientsecret = SettingService.GetSetting(settings, "OpenIdConnectOptions:ClientSecret", ""); - _redirecturl = PageState.Alias.Name + "/signin-oidc"; + _redirecturl = PageState.Uri.Scheme + "://" + PageState.Alias.Name + "/signin-oidc"; _metadata = SettingService.GetSetting(settings, "OpenIdConnectOptions:MetadataAddress", ""); _logouturl = SettingService.GetSetting(settings, "OpenIdConnectOptions:LogoutUrl", ""); _allowsitelogin = SettingService.GetSetting(settings, "AllowSiteLogin", "true"); From ca17dd3ca3f8cdada909a0ad30befc69627edc6c Mon Sep 17 00:00:00 2001 From: Shaun Walker Date: Mon, 21 Mar 2022 16:29:28 -0400 Subject: [PATCH 34/68] Allow Email Claim Type to be configurable --- Oqtane.Client/Modules/Admin/Users/Index.razor | 9 ++++++ ...taneSiteAuthenticationBuilderExtensions.cs | 31 ++++++++++++++++--- 2 files changed, 35 insertions(+), 5 deletions(-) diff --git a/Oqtane.Client/Modules/Admin/Users/Index.razor b/Oqtane.Client/Modules/Admin/Users/Index.razor index e2465bd2..29a7d9cf 100644 --- a/Oqtane.Client/Modules/Admin/Users/Index.razor +++ b/Oqtane.Client/Modules/Admin/Users/Index.razor @@ -160,6 +160,12 @@ else
+
+ +
+ +
+
@@ -208,6 +214,7 @@ else private string _clientid; private string _clientsecret; private string _redirecturl; + private string _emailclaimtype; private string _metadata; private string _logouturl; private string _allowsitelogin; @@ -235,6 +242,7 @@ else _clientid = SettingService.GetSetting(settings, "OpenIdConnectOptions:ClientId", ""); _clientsecret = SettingService.GetSetting(settings, "OpenIdConnectOptions:ClientSecret", ""); _redirecturl = PageState.Uri.Scheme + "://" + PageState.Alias.Name + "/signin-oidc"; + _emailclaimtype = SettingService.GetSetting(settings, "OpenIdConnectOptions:EmailClaimType", "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"); _metadata = SettingService.GetSetting(settings, "OpenIdConnectOptions:MetadataAddress", ""); _logouturl = SettingService.GetSetting(settings, "OpenIdConnectOptions:LogoutUrl", ""); _allowsitelogin = SettingService.GetSetting(settings, "AllowSiteLogin", "true"); @@ -320,6 +328,7 @@ else settings = SettingService.SetSetting(settings, "OpenIdConnectOptions:Authority", _authority, true); settings = SettingService.SetSetting(settings, "OpenIdConnectOptions:ClientId", _clientid, true); settings = SettingService.SetSetting(settings, "OpenIdConnectOptions:ClientSecret", _clientsecret, true); + settings = SettingService.SetSetting(settings, "OpenIdConnectOptions:EmailClaimType", _emailclaimtype, true); settings = SettingService.SetSetting(settings, "OpenIdConnectOptions:MetadataAddress", _metadata, true); settings = SettingService.SetSetting(settings, "OpenIdConnectOptions:LogoutUrl", _logouturl, true); settings = SettingService.SetSetting(settings, "AllowSiteLogin", _allowsitelogin, false); diff --git a/Oqtane.Server/Extensions/OqtaneSiteAuthenticationBuilderExtensions.cs b/Oqtane.Server/Extensions/OqtaneSiteAuthenticationBuilderExtensions.cs index b6a5f38e..a1589bda 100644 --- a/Oqtane.Server/Extensions/OqtaneSiteAuthenticationBuilderExtensions.cs +++ b/Oqtane.Server/Extensions/OqtaneSiteAuthenticationBuilderExtensions.cs @@ -82,13 +82,34 @@ namespace Oqtane.Extensions private static async Task OnTokenValidated(TokenValidatedContext context) { var providerKey = context.Principal.FindFirstValue(ClaimTypes.NameIdentifier); - var loginProvider = context.HttpContext.GetAlias().SiteSettings["OpenIdConnectOptions:Authority"]; + var loginProvider = context.HttpContext.GetAlias().SiteSettings.GetValue("OpenIdConnectOptions:Authority", ""); + var emailClaimType = context.HttpContext.GetAlias().SiteSettings.GetValue("OpenIdConnectOptions:EmailClaimType", ""); + if (string.IsNullOrEmpty(emailClaimType)) + { + emailClaimType = ClaimTypes.Email; + } var alias = context.HttpContext.GetAlias(); var _logger = context.HttpContext.RequestServices.GetRequiredService(); // custom logic may be needed here to manipulate Principal sent by Provider - use interface similar to IClaimsTransformation - var email = context.Principal.FindFirstValue(ClaimTypes.Email); + var email = context.Principal.FindFirstValue(emailClaimType); + + // validate email claim + if (email == null || !email.Contains("@") || !email.Contains(".")) + { + var emailclaimtype = context.Principal.Claims.FirstOrDefault(item => item.Value.Contains("@") && item.Value.Contains(".")); + if (emailclaimtype != null) + { + email = emailclaimtype.Value; + _logger.Log(LogLevel.Information, nameof(OqtaneSiteAuthenticationBuilderExtensions), Enums.LogFunction.Security, "Please Update The Email Claim Type For The OpenID Connect Provider To {EmailClaimType} In Site Settings", emailclaimtype.Type); + } + else + { + email = null; + } + } + if (email != null) { var _identityUserManager = context.HttpContext.RequestServices.GetRequiredService>(); @@ -170,7 +191,7 @@ namespace Oqtane.Extensions else { // provider keys do not match - _logger.Log(LogLevel.Error, nameof(OqtaneSiteAuthenticationBuilderExtensions), Enums.LogFunction.Security, "OpenId Connect Provider Key Does Not Match For User {Email}", email); + _logger.Log(LogLevel.Error, nameof(OqtaneSiteAuthenticationBuilderExtensions), Enums.LogFunction.Security, "OpenId Connect Provider Key Does Not Match For User {Email}. Login Denied.", email); } } else @@ -208,9 +229,9 @@ namespace Oqtane.Extensions principal.AddClaim(new Claim("Provider", context.HttpContext.GetAlias().SiteSettings["OpenIdConnectOptions:Authority"])); } } - else + else // no email claim { - _logger.Log(LogLevel.Information, nameof(OqtaneSiteAuthenticationBuilderExtensions), Enums.LogFunction.Security, "OpenID Connect Provider Did Not Return An Email Claim To Uniquely Identify The User"); + _logger.Log(LogLevel.Error, nameof(OqtaneSiteAuthenticationBuilderExtensions), Enums.LogFunction.Security, "OpenID Connect Provider Did Not Return An Email Claim To Uniquely Identify The User"); } } From 9d86d923aaf4645ac94dcf3b07b6c3536dd9a1fe Mon Sep 17 00:00:00 2001 From: Shaun Walker Date: Wed, 23 Mar 2022 10:51:52 -0400 Subject: [PATCH 35/68] Add OAuth2 support --- Oqtane.Client/Modules/Admin/Login/Index.razor | 10 +- Oqtane.Client/Modules/Admin/Users/Index.razor | 258 +++++++---- .../Resources/Modules/Admin/Users/Index.resx | 121 ++++- ...taneSiteAuthenticationBuilderExtensions.cs | 414 +++++++++++------- Oqtane.Server/Pages/External.cshtml | 3 + Oqtane.Server/Pages/External.cshtml.cs | 29 ++ Oqtane.Server/Pages/Logout.cshtml.cs | 13 +- Oqtane.Server/Pages/OIDC.cshtml | 3 - Oqtane.Server/Pages/OIDC.cshtml.cs | 19 - Oqtane.Server/Startup.cs | 3 +- Oqtane.Shared/Security/UserSecurity.cs | 21 +- 11 files changed, 601 insertions(+), 293 deletions(-) create mode 100644 Oqtane.Server/Pages/External.cshtml create mode 100644 Oqtane.Server/Pages/External.cshtml.cs delete mode 100644 Oqtane.Server/Pages/OIDC.cshtml delete mode 100644 Oqtane.Server/Pages/OIDC.cshtml.cs diff --git a/Oqtane.Client/Modules/Admin/Login/Index.razor b/Oqtane.Client/Modules/Admin/Login/Index.razor index 234ebb0c..cd7bcd83 100644 --- a/Oqtane.Client/Modules/Admin/Login/Index.razor +++ b/Oqtane.Client/Modules/Admin/Login/Index.razor @@ -21,7 +21,7 @@
- +
- - + +
@@ -149,7 +149,7 @@ else
} - @if (_providertype == "oidc") + @if (_providertype == AuthenticationProviderTypes.OpenIDConnect) {
@@ -164,7 +164,7 @@ else
} - @if (_providertype == "oauth2") + @if (_providertype == AuthenticationProviderTypes.OAuth2) {
@@ -220,7 +220,7 @@ else
- @if (_providertype == "oidc") + @if (_providertype == AuthenticationProviderTypes.OpenIDConnect) {
@@ -440,7 +440,7 @@ else private void ProviderTypeChanged(ChangeEventArgs e) { _providertype = (string)e.Value; - if (_providertype == "oidc") + if (_providertype == AuthenticationProviderTypes.OpenIDConnect) { _scopes = "openid,profile,email"; } diff --git a/Oqtane.Client/Modules/Controls/FileManager.razor b/Oqtane.Client/Modules/Controls/FileManager.razor index 7cb80a1a..208e2f54 100644 --- a/Oqtane.Client/Modules/Controls/FileManager.razor +++ b/Oqtane.Client/Modules/Controls/FileManager.razor @@ -52,7 +52,7 @@ }
-
+
@if (ShowFiles && GetFileId() != -1) { diff --git a/Oqtane.Client/Resources/Modules/Admin/Login/Index.resx b/Oqtane.Client/Resources/Modules/Admin/Login/Index.resx index cc23230c..0760ce56 100644 --- a/Oqtane.Client/Resources/Modules/Admin/Login/Index.resx +++ b/Oqtane.Client/Resources/Modules/Admin/Login/Index.resx @@ -127,7 +127,7 @@ User Account Could Not Be Verified. Please Contact Your Administrator For Further Instructions. - Login Failed. Please Remember That Passwords Are Case Sensitive. If You Have Attempted To Sign In 3 Times Unsuccessfully, Your Account Will Be Locked Out For 10 Minutes. Note That User Accounts Require Verification When They Are Initially Created So You May Wish To Check Your Email If You Are A New User. + Login Failed. Please Remember That Passwords Are Case Sensitive. If You Have Attempted To Sign In Multiple Times Unsuccessfully, Your Account Will Be Locked Out For A Period Of Time. Note That User Accounts Require Verification When They Are Initially Created So You May Wish To Check Your Email If You Are A New User. Please Provide All Required Fields diff --git a/Oqtane.Client/Resources/Modules/Admin/UserProfile/Index.resx b/Oqtane.Client/Resources/Modules/Admin/UserProfile/Index.resx index 22a36db3..ba95f3c0 100644 --- a/Oqtane.Client/Resources/Modules/Admin/UserProfile/Index.resx +++ b/Oqtane.Client/Resources/Modules/Admin/UserProfile/Index.resx @@ -1,4 +1,4 @@ - +