From b2ab9035c186dd6bb9bc6268a6799b86f369d657 Mon Sep 17 00:00:00 2001 From: vnugent Date: Tue, 17 Sep 2024 14:32:41 -0400 Subject: test some mvc extensions updates --- lib/vnlib.browser/Taskfile.yaml | 18 +++-- .../src/AppDataEntry.cs | 2 +- .../src/Endpoints/WebEndpoint.cs | 94 +++++++++++++++------- .../src/Model/HttpExtensions.cs | 9 ++- .../src/Stores/Sql/SqlBackingStore.cs | 3 +- .../src/RegistrationContext.cs | 13 ++- .../src/TokenRevocation/RevokedToken.cs | 1 + .../src/TokenRevocation/RevokedTokenStore.cs | 3 +- .../src/Endpoints/LoginEndpoint.cs | 4 - .../src/ManagedRouteStore.cs | 17 +++- 10 files changed, 110 insertions(+), 54 deletions(-) diff --git a/lib/vnlib.browser/Taskfile.yaml b/lib/vnlib.browser/Taskfile.yaml index 81b726d..bf1c7e7 100644 --- a/lib/vnlib.browser/Taskfile.yaml +++ b/lib/vnlib.browser/Taskfile.yaml @@ -13,10 +13,10 @@ tasks: #called by build pipeline to build module build: cmds: - - echo "building module {{.MODULE_NAME}}" + - echo "building module {{ .PROJECT_NAME }}" #update internal package version - - cmd: npm version {{.BUILD_VERSION}} + - cmd: npm version {{ .BUILD_VERSION }} ignore_error: true #install dependencies and build @@ -24,11 +24,19 @@ tasks: - npm run build postbuild_success: + vars: + TAR_FILES: + dist/ + LICENSE.txt + README.md + package.json + package-lock.json + tsconfig.json + cmds: - powershell -Command "mkdir bin -Force" #tgz the dist folder - - tar --exclude="./node_modules" --exclude="./src" --exclude="./.git" --exclude="./bin" --exclude=".gitignore" --exclude="*.yaml" --exclude="*.yml" -czf bin/release.tgz . - + - tar -czf bin/release.tgz {{ .TAR_FILES }} #called by build pipeline to clean module clean: @@ -36,4 +44,4 @@ tasks: cmds: #delete dist folder - for: ['bin/', 'dist/', 'node_modules/'] - cmd: powershell -Command "Remove-Item -Recurse -Force {{.ITEM_NAME}}" \ No newline at end of file + cmd: powershell -Command "Remove-Item -Recurse -Force {{ .ITEM_NAME }}" \ No newline at end of file diff --git a/plugins/VNLib.Plugins.Essentials.Accounts.AppData/src/AppDataEntry.cs b/plugins/VNLib.Plugins.Essentials.Accounts.AppData/src/AppDataEntry.cs index d6d936f..d39000b 100644 --- a/plugins/VNLib.Plugins.Essentials.Accounts.AppData/src/AppDataEntry.cs +++ b/plugins/VNLib.Plugins.Essentials.Accounts.AppData/src/AppDataEntry.cs @@ -23,7 +23,7 @@ */ using VNLib.Utils.Logging; -using VNLib.Plugins.Extensions.Loading.Routing; +using VNLib.Plugins.Extensions.Loading.Routing.Mvc; using VNLib.Plugins.Essentials.Accounts.AppData.Endpoints; diff --git a/plugins/VNLib.Plugins.Essentials.Accounts.AppData/src/Endpoints/WebEndpoint.cs b/plugins/VNLib.Plugins.Essentials.Accounts.AppData/src/Endpoints/WebEndpoint.cs index 3c4f3e5..41b8b30 100644 --- a/plugins/VNLib.Plugins.Essentials.Accounts.AppData/src/Endpoints/WebEndpoint.cs +++ b/plugins/VNLib.Plugins.Essentials.Accounts.AppData/src/Endpoints/WebEndpoint.cs @@ -22,10 +22,11 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ +using System; using System.Net; using System.Linq; -using System.Collections.Generic; using System.Threading.Tasks; +using System.Collections.Generic; using VNLib.Net.Http; using VNLib.Hashing.Checksums; @@ -37,14 +38,16 @@ using VNLib.Plugins.Extensions.Loading.Routing; using VNLib.Plugins.Essentials.Accounts.AppData.Model; using VNLib.Plugins.Essentials.Accounts.AppData.Stores; +using VNLib.Plugins.Extensions.Loading.Routing.Mvc; +using static VNLib.Plugins.Essentials.Endpoints.ResourceEndpointBase; +using static VNLib.Plugins.Essentials.Accounts.AppData.Model.HttpExtensions; namespace VNLib.Plugins.Essentials.Accounts.AppData.Endpoints { - - [EndpointPath("{{path}}")] + [EndpointLogName("Endpoint")] [ConfigurationName("web_endpoint")] - internal sealed class WebEndpoint(PluginBase plugin, IConfigScope config) : ProtectedWebEndpoint + internal sealed class WebEndpoint(PluginBase plugin, IConfigScope config) : IHttpController { const int DefaultMaxDataSize = 8 * 1024; @@ -52,19 +55,24 @@ namespace VNLib.Plugins.Essentials.Accounts.AppData.Endpoints private readonly int MaxDataSize = config.GetValueOrDefault("max_data_size", DefaultMaxDataSize); private readonly string[] AllowedScopes = config.GetRequiredProperty("allowed_scopes"); - protected async override ValueTask GetAsync(HttpEntity entity) + /// + public ProtectionSettings GetProtectionSettings() => default; + + [HttpStaticRoute("{{path}}", HttpMethod.GET)] + [HttpRouteProtection(AuthorzationCheckLevel.Critical)] + public async ValueTask GetDataAsync(HttpEntity entity) { WebMessage webm = new(); - string? scopeId = entity.QueryArgs.GetValueOrDefault("scope"); - bool noCache = entity.QueryArgs.ContainsKey("no_cache"); + string? scopeId = GetScopeId(entity); + bool noCache = NoCacheQuery(entity); if (webm.Assert(scopeId != null, "Missing scope")) { return VirtualClose(entity, webm, HttpStatusCode.BadRequest); } - if (webm.Assert(AllowedScopes.Contains(scopeId), "Invalid scope")) + if (webm.Assert(IsScopeAllowed(scopeId), "Invalid scope")) { return VirtualClose(entity, webm, HttpStatusCode.BadRequest); } @@ -72,26 +80,28 @@ namespace VNLib.Plugins.Essentials.Accounts.AppData.Endpoints //If the connection has the no-cache header set, also bypass the cache noCache |= entity.Server.NoCache(); - //optionally bypass cache if the user requests it - RecordOpFlags flags = noCache ? RecordOpFlags.NoCache : RecordOpFlags.None; - - UserRecordData? record = await _store.GetRecordAsync(entity.Session.UserID, scopeId, flags, entity.EventCancellation); - - if (record is null) - { - return VirtualClose(entity, webm, HttpStatusCode.NotFound); - } + UserRecordData? record = await _store.GetRecordAsync( + entity.Session.UserID, + recordKey: scopeId, + flags: noCache ? RecordOpFlags.NoCache : RecordOpFlags.None, //optionally bypass cache if the user requests it + entity.EventCancellation + ); //return the raw data with the checksum header - entity.SetRecordResponse(record, HttpStatusCode.OK); - return VfReturnType.VirtualSkip; + + return record is null + ? VirtualClose(entity, webm, HttpStatusCode.NotFound) + : CloseWithRecord(entity, record, HttpStatusCode.OK); } - protected override async ValueTask PutAsync(HttpEntity entity) + + [HttpStaticRoute("{{path}}", HttpMethod.PUT)] + [HttpRouteProtection(AuthorzationCheckLevel.Critical)] + public async ValueTask UpdateDataAsync(HttpEntity entity) { WebMessage webm = new(); - string? scopeId = entity.QueryArgs.GetValueOrDefault("scope"); - bool flush = entity.QueryArgs.ContainsKey("flush"); + string? scopeId = GetScopeId(entity); + bool flush = NoCacheQuery(entity); if (webm.Assert(entity.Files.Count == 1, "Invalid file count")) { @@ -103,7 +113,7 @@ namespace VNLib.Plugins.Essentials.Accounts.AppData.Endpoints return VirtualClose(entity, webm, HttpStatusCode.BadRequest); } - if (webm.Assert(AllowedScopes.Contains(scopeId), "Invalid scope")) + if (webm.Assert(IsScopeAllowed(scopeId), "Invalid scope")) { return VirtualClose(entity, webm, HttpStatusCode.BadRequest); } @@ -125,7 +135,7 @@ namespace VNLib.Plugins.Essentials.Accounts.AppData.Endpoints //Compute checksum on sent data and compare to the header if it exists ulong checksum = FNV1a.Compute64(recordData); - ulong? userChecksum = entity.Server.GetUserDataChecksum(); + ulong? userChecksum = GetUserDataChecksum(entity.Server); if (userChecksum.HasValue) { @@ -144,28 +154,54 @@ namespace VNLib.Plugins.Essentials.Accounts.AppData.Endpoints RecordOpFlags flags = flush ? RecordOpFlags.WriteThrough : RecordOpFlags.None; //Write the record to the store - await _store.SetRecordAsync(entity.Session.UserID, scopeId, recordData, checksum, flags, entity.EventCancellation); + await _store.SetRecordAsync( + userId: entity.Session.UserID, + recordKey: scopeId, + recordData, + checksum, + flags, + entity.EventCancellation + ); + return VirtualClose(entity, HttpStatusCode.Accepted); } - protected override async ValueTask DeleteAsync(HttpEntity entity) + [HttpStaticRoute("{{path}}", HttpMethod.DELETE)] + [HttpRouteProtection(AuthorzationCheckLevel.Critical)] + public async ValueTask DeleteDataAsync(HttpEntity entity) { WebMessage webm = new(); - string? scopeId = entity.QueryArgs.GetValueOrDefault("scope"); + string? scopeId = GetScopeId(entity); if (webm.Assert(scopeId != null, "Missing scope")) { return VirtualClose(entity, webm, HttpStatusCode.BadRequest); } - if (webm.Assert(AllowedScopes.Contains(scopeId), "Invalid scope")) + if (webm.Assert(IsScopeAllowed(scopeId), "Invalid scope")) { return VirtualClose(entity, webm, HttpStatusCode.BadRequest); } //Write the record to the store - await _store.DeleteRecordAsync(entity.Session.UserID, scopeId, entity.EventCancellation); + await _store.DeleteRecordAsync( + userId: entity.Session.UserID, + recordKey: scopeId, + entity.EventCancellation + ); + return VirtualClose(entity, HttpStatusCode.Accepted); + } + + private bool IsScopeAllowed(string scopeId) + { + return AllowedScopes.Contains(scopeId, StringComparer.OrdinalIgnoreCase); } + + private static string? GetScopeId(HttpEntity entity) + => entity.QueryArgs.GetValueOrDefault("scope"); + + private static bool NoCacheQuery(HttpEntity entity) + => entity.QueryArgs.ContainsKey("no_cache"); } } diff --git a/plugins/VNLib.Plugins.Essentials.Accounts.AppData/src/Model/HttpExtensions.cs b/plugins/VNLib.Plugins.Essentials.Accounts.AppData/src/Model/HttpExtensions.cs index 9628b79..c3b990c 100644 --- a/plugins/VNLib.Plugins.Essentials.Accounts.AppData/src/Model/HttpExtensions.cs +++ b/plugins/VNLib.Plugins.Essentials.Accounts.AppData/src/Model/HttpExtensions.cs @@ -23,6 +23,7 @@ */ using System; +using System.Collections.Generic; using System.Net; using VNLib.Net.Http; @@ -33,7 +34,7 @@ namespace VNLib.Plugins.Essentials.Accounts.AppData.Model { const string ChecksumHeader = "X-Data-Checksum"; - public static void SetRecordResponse(this HttpEntity entity, UserRecordData record, HttpStatusCode code) + public static VfReturnType CloseWithRecord(HttpEntity entity, UserRecordData record, HttpStatusCode code) { //Set checksum header entity.Server.Headers.Append(ChecksumHeader, $"{record.Checksum}"); @@ -44,14 +45,16 @@ namespace VNLib.Plugins.Essentials.Accounts.AppData.Model ContentType.Binary, new BinDataRecordReader(record.Data) ); + + return VfReturnType.VirtualSkip; } - public static ulong? GetUserDataChecksum(this IConnectionInfo server) + public static ulong? GetUserDataChecksum(IConnectionInfo server) { string? checksumStr = server.Headers[ChecksumHeader]; return string.IsNullOrWhiteSpace(checksumStr) && ulong.TryParse(checksumStr, out ulong checksum) ? checksum : null; } - + sealed class BinDataRecordReader(byte[] recordData) : IMemoryResponseReader { private int _read; diff --git a/plugins/VNLib.Plugins.Essentials.Accounts.AppData/src/Stores/Sql/SqlBackingStore.cs b/plugins/VNLib.Plugins.Essentials.Accounts.AppData/src/Stores/Sql/SqlBackingStore.cs index 2347c66..3761f07 100644 --- a/plugins/VNLib.Plugins.Essentials.Accounts.AppData/src/Stores/Sql/SqlBackingStore.cs +++ b/plugins/VNLib.Plugins.Essentials.Accounts.AppData/src/Stores/Sql/SqlBackingStore.cs @@ -42,7 +42,8 @@ using VNLib.Plugins.Essentials.Accounts.AppData.Model; namespace VNLib.Plugins.Essentials.Accounts.AppData.Stores.Sql { - internal sealed class SqlBackingStore(PluginBase plugin) : IEntityStore, IAsyncConfigurable + internal sealed class SqlBackingStore(PluginBase plugin) + : IEntityStore, IAsyncConfigurable { private readonly DbRecordStore _store = new(plugin.GetContextOptionsAsync()); diff --git a/plugins/VNLib.Plugins.Essentials.Accounts.Registration/src/RegistrationContext.cs b/plugins/VNLib.Plugins.Essentials.Accounts.Registration/src/RegistrationContext.cs index c19d163..fb28600 100644 --- a/plugins/VNLib.Plugins.Essentials.Accounts.Registration/src/RegistrationContext.cs +++ b/plugins/VNLib.Plugins.Essentials.Accounts.Registration/src/RegistrationContext.cs @@ -43,15 +43,12 @@ namespace VNLib.Plugins.Essentials.Accounts.Registration public void OnDatabaseCreating(IDbContextBuilder builder, object? state) { //Define a table for the revoked tokens - builder.DefineTable(nameof(RevokedRegistrationTokens)) - //Define the token column and the created column, let the framework determine the data-types - .WithColumn(p => p.Token) - .MaxLength(200) - .Next() + builder.DefineTable(nameof(RevokedRegistrationTokens), table => + { + table.WithColumn(p => p.Token); + table.WithColumn(p => p.Created); + }); - //Define the next column - .WithColumn(p => p.Created) - .AllowNull(false); } } } \ No newline at end of file diff --git a/plugins/VNLib.Plugins.Essentials.Accounts.Registration/src/TokenRevocation/RevokedToken.cs b/plugins/VNLib.Plugins.Essentials.Accounts.Registration/src/TokenRevocation/RevokedToken.cs index c2b7715..1e3ea38 100644 --- a/plugins/VNLib.Plugins.Essentials.Accounts.Registration/src/TokenRevocation/RevokedToken.cs +++ b/plugins/VNLib.Plugins.Essentials.Accounts.Registration/src/TokenRevocation/RevokedToken.cs @@ -38,6 +38,7 @@ namespace VNLib.Plugins.Essentials.Accounts.Registration.TokenRevocation /// The token that was revoked. /// [Key] + [MaxLength(200)] public string? Token { get; set; } } } \ No newline at end of file diff --git a/plugins/VNLib.Plugins.Essentials.Accounts.Registration/src/TokenRevocation/RevokedTokenStore.cs b/plugins/VNLib.Plugins.Essentials.Accounts.Registration/src/TokenRevocation/RevokedTokenStore.cs index e63b02e..3f28b4e 100644 --- a/plugins/VNLib.Plugins.Essentials.Accounts.Registration/src/TokenRevocation/RevokedTokenStore.cs +++ b/plugins/VNLib.Plugins.Essentials.Accounts.Registration/src/TokenRevocation/RevokedTokenStore.cs @@ -78,7 +78,8 @@ namespace VNLib.Plugins.Essentials.Accounts.Registration.TokenRevocation await using RegistrationContext context = new (options.Value); //Select any that match tokens - RevokedToken[] expired = await context.RevokedRegistrationTokens.Where(t => t.Created < expiredBefore) + RevokedToken[] expired = await context.RevokedRegistrationTokens + .Where(t => t.Created < expiredBefore) .Select(static t => t) .ToArrayAsync(cancellation); diff --git a/plugins/VNLib.Plugins.Essentials.Accounts/src/Endpoints/LoginEndpoint.cs b/plugins/VNLib.Plugins.Essentials.Accounts/src/Endpoints/LoginEndpoint.cs index 5bc286e..83bb9a2 100644 --- a/plugins/VNLib.Plugins.Essentials.Accounts/src/Endpoints/LoginEndpoint.cs +++ b/plugins/VNLib.Plugins.Essentials.Accounts/src/Endpoints/LoginEndpoint.cs @@ -27,7 +27,6 @@ using System.Net; using System.Text.Json; using System.Threading; using System.Threading.Tasks; -using System.Collections.Generic; using System.Security.Cryptography; using System.Text.Json.Serialization; @@ -46,9 +45,6 @@ using VNLib.Plugins.Extensions.Loading; using VNLib.Plugins.Extensions.Loading.Users; using VNLib.Plugins.Extensions.Loading.Routing; using static VNLib.Plugins.Essentials.Statics; -using VNLib.Plugins.Essentials.Accounts.MFA.Totp; -using VNLib.Plugins.Essentials.Accounts.MFA.Fido; - /* * Password only log-ins should be immune to repeat attacks on the same backend, because sessions are diff --git a/plugins/VNLib.Plugins.Essentials.Content.Routing/src/ManagedRouteStore.cs b/plugins/VNLib.Plugins.Essentials.Content.Routing/src/ManagedRouteStore.cs index 8c32e71..65b115c 100644 --- a/plugins/VNLib.Plugins.Essentials.Content.Routing/src/ManagedRouteStore.cs +++ b/plugins/VNLib.Plugins.Essentials.Content.Routing/src/ManagedRouteStore.cs @@ -26,6 +26,7 @@ using System.Threading; using System.Threading.Tasks; using System.Collections.Generic; +using VNLib.Utils.Logging; using VNLib.Plugins.Extensions.Loading; using VNLib.Plugins.Extensions.Loading.Sql; using VNLib.Plugins.Essentials.Content.Routing.Model; @@ -33,10 +34,16 @@ using VNLib.Plugins.Essentials.Content.Routing.stores; namespace VNLib.Plugins.Essentials.Content.Routing { - [ConfigurationName("store")] + [ConfigurationName("store", Required = false)] internal sealed class ManagedRouteStore : IRouteStore { - private readonly IRouteStore _routeStore; + private readonly IRouteStore _routeStore = new DummyRouteStore(); + + //empty constructor for + public ManagedRouteStore(PluginBase plugin) + { + plugin.Log.Warn("Page router loaded but no route store was loaded. Routing funtionality is disabled."); + } public ManagedRouteStore(PluginBase plugin, IConfigScope config) { @@ -60,5 +67,11 @@ namespace VNLib.Plugins.Essentials.Content.Routing { return _routeStore.GetAllRoutesAsync(routes, cancellation); } + + private sealed class DummyRouteStore : IRouteStore + { + public Task GetAllRoutesAsync(ICollection routes, CancellationToken cancellation) + => Task.CompletedTask; + } } } -- cgit