From 99f4361e2dd37e1bb110dac06c58ee6004dc08a9 Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Thu, 25 Jun 2026 20:22:41 +0100 Subject: [PATCH] Api Resources seed --- .vscode/settings.json | 3 +- src/PostIt/PostIt/App.axaml.cs | 2 - src/PostIt/PostIt/Settings/Settings.cs | 26 ++++++-- src/Yavsc.Org/Contants.cs | 20 +++++- src/Yavsc.Org/Extensions/HostingExtensions.cs | 66 ++++++++++--------- 5 files changed, 76 insertions(+), 41 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 728a2954..d5229aed 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,6 +1,6 @@ { "dotnet-test-explorer.testProjectPath": "test/**/*Tests.csproj", - + "cSpell.words": [ "appsettings", "asciidoctor", @@ -10,6 +10,7 @@ "DESTDIR", "dotnet", "DOTNET", + "ecdsa", "envsubst", "Newtonsoft", "Npgsql", diff --git a/src/PostIt/PostIt/App.axaml.cs b/src/PostIt/PostIt/App.axaml.cs index 204ce0eb..5454d478 100644 --- a/src/PostIt/PostIt/App.axaml.cs +++ b/src/PostIt/PostIt/App.axaml.cs @@ -64,8 +64,6 @@ public partial class App : Application DataContext = new MainPageViewModel(client, settings) }; } - else - throw new NotSupportedException("ApplicationLifetime not supported."); } diff --git a/src/PostIt/PostIt/Settings/Settings.cs b/src/PostIt/PostIt/Settings/Settings.cs index 3da729ec..ddac7199 100644 --- a/src/PostIt/PostIt/Settings/Settings.cs +++ b/src/PostIt/PostIt/Settings/Settings.cs @@ -96,6 +96,20 @@ public partial class Settings : ObservableObject internal void Load() { if (Loaded) return; + + // Trust an already-populated Authority: tests pre-fill Settings + // with the OIDC stub's random loopback port, and programmatic + // callers (CLI flags, integration tests) wire their own. If we + // fall through to the disk / embedded read here we'd silently + // overwrite their value with the bundled default + // (yavsc.pschneider.fr), break the stubbed discovery URL, and + // turn a passing login into an invalid_grant. + if (!string.IsNullOrWhiteSpace(Authentication?.Authority)) + { + Loaded = true; + return; + } + string configDir = Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "PostIt" @@ -109,16 +123,14 @@ public partial class Settings : ObservableObject if (!configFileInfo.Exists) { Console.Error.WriteLine($"🩎 Settings file not found at {configFileInfo.FullName}"); - // Only fall back to the embedded default when the in-memory - // settings haven't been populated yet. This protects callers - // (notably tests) that pre-load Settings with explicit values - // from being silently overwritten by the bundled default. - if (string.IsNullOrWhiteSpace(this.Authentication?.Authority) - && !TryLoadEmbeddedFallback()) + // No user-level config: fall back to the embedded default. + // We only get here when Authentication.Authority is empty + // (the early-return above) so the redundant guard is gone. + if (!TryLoadEmbeddedFallback()) { Console.Error.WriteLine("🩎 No embedded default settings; running with empty configuration."); } - return; // no user settings file + return; } Console.WriteLine($"🔎 Loading settings from {configFileInfo.FullName}"); diff --git a/src/Yavsc.Org/Contants.cs b/src/Yavsc.Org/Contants.cs index e28597c0..9a9b8a09 100644 --- a/src/Yavsc.Org/Contants.cs +++ b/src/Yavsc.Org/Contants.cs @@ -1,7 +1,25 @@ + +using IdentityServer8.EntityFramework.Entities; + public static class Constants { public static readonly string[] BuildInApiScopes = { "profile", "openid", "offline_access", - "blogs", "admin", "moderation", "performer", "client" }; + "admin", "moderation", "performer", "client" }; + + public static readonly ApiResourceScopeSpecification[] ApiResourcesScopes = { + + new ApiResourceScopeSpecification { ScopeName = "admin", Description = "Admin access" }, + new ApiResourceScopeSpecification { ScopeName = "moderation", Description = "Moderation access" }, + new ApiResourceScopeSpecification { ScopeName = "performer", Description = "Performer access" }, + new ApiResourceScopeSpecification { ScopeName = "client", Description = "Client access" }, + new ApiResourceScopeSpecification { ScopeName = "blogs", Description = "Blogs access" } + }; +} + +public class ApiResourceScopeSpecification +{ + public string ScopeName { get; set; } + public string Description { get; set; } } diff --git a/src/Yavsc.Org/Extensions/HostingExtensions.cs b/src/Yavsc.Org/Extensions/HostingExtensions.cs index b4fbfb40..d764f362 100644 --- a/src/Yavsc.Org/Extensions/HostingExtensions.cs +++ b/src/Yavsc.Org/Extensions/HostingExtensions.cs @@ -102,10 +102,10 @@ public static class HostingExtensions options.ResourcesPath = "Resources"; }).AddDataAnnotationsLocalization(); - services.AddTransient() - .AddTransient(); + services.AddTransient() + .AddTransient(); - services.TryAddSingleton(); + services.TryAddSingleton(); services.AddTransient() @@ -342,7 +342,7 @@ public static class HostingExtensions }); - // Skip the production signing-cert requirement when running with + // Skip the production signing-cert requirement when running with // an in-memory database (test fixtures) or in the Development // environment. In those cases IdentityServer8 falls back to // AddDeveloperSigningCredential which mints an ephemeral key @@ -438,7 +438,7 @@ public static class HostingExtensions { // Validate the cert is readable (used downstream for token // audience/subject validation; signing itself uses the key). - _ = new X509Certificate2(certPath); + string keyPem = File.ReadAllText(keyPath); // BouncyCastle's PemReader accepts every flavour of unencrypted @@ -471,7 +471,9 @@ public static class HostingExtensions { case RsaPrivateCrtKeyParameters rsa: { +#pragma warning disable CA1416 // Valider la compatibilité de la plateforme var rsaDotNet = DotNetUtilities.ToRSA(rsa); +#pragma warning restore CA1416 // Valider la compatibilité de la plateforme var key = new RsaSecurityKey(rsaDotNet); return new SigningCredentials(key, SecurityAlgorithms.RsaSha256); } @@ -534,32 +536,36 @@ public static class HostingExtensions context.SaveChanges(); } } - var identityResources = context.Set(); - var apiScopes = context.Set(); + var identityResources = context.Set(); + var apiScopes = context.Set(); - // IdentityResources standards - if (!identityResources.Any(r => r.Name == "openid")) - { - var openid = new IdentityResources.OpenId().ToEntity(); - identityResources.Add(openid); - } - - if (!identityResources.Any(r => r.Name == "profile")) - { - var profile = new IdentityResources.Profile().ToEntity(); - identityResources.Add(profile); - } - - // ApiScope custom - if (!apiScopes.Any(s => s.Name == "blogs")) - { - apiScopes.Add(new IdentityServer8.EntityFramework.Entities.ApiScope + // IdentityResources standards + if (!identityResources.Any(r => r.Name == "openid")) { - Name = "blogs", - DisplayName = "Yavsc Blogs API", - Enabled = true - }); - } + var openid = new IdentityResources.OpenId().ToEntity(); + identityResources.Add(openid); + } + + if (!identityResources.Any(r => r.Name == "profile")) + { + var profile = new IdentityResources.Profile().ToEntity(); + identityResources.Add(profile); + } + foreach (var scope in Constants.ApiResourcesScopes) + { + + if (!identityResources.Any(s => s.Name == scope.ScopeName)) + { + identityResources.Add( + new IdentityResources.Profile() + { + Name = scope.ScopeName, + DisplayName = scope.Description, + Enabled = true + }.ToEntity()); + } + } + context.SaveChanges(); }; } @@ -720,7 +726,7 @@ public static class HostingExtensions } - public async static Task ConfigurePipeline(this WebApplication app, string staticAssetsManifestPath=null) + public async static Task ConfigurePipeline(this WebApplication app, string staticAssetsManifestPath = null) { ILoggerFactory loggerFactory = app.Services.GetRequiredService(); var logger = loggerFactory.CreateLogger();