feat(plugin): add authentication, options, and infrastructure integration hooks - #36
Merged
Merged
Conversation
The default interface member cached the capability set in a static field shared by every plugin implementation, so the first plugin's capabilities leaked to all others. Compute the set per instance instead and add a regression test covering two plugins with disjoint capabilities.
Comment on lines
+13
to
+89
| extension(IAuthKitPlugin plugin) | ||
| { | ||
| /// <summary> | ||
| /// Binds strongly typed options from the plugin's configuration section. | ||
| /// </summary> | ||
| /// <typeparam name="TOptions">The plugin options type.</typeparam> | ||
| /// <param name="services">The host service collection.</param> | ||
| /// <param name="configuration">The application configuration.</param> | ||
| /// <remarks> | ||
| /// <para> | ||
| /// The section is resolved through <see cref="GetPluginConfiguration"/>: the | ||
| /// <c>Plugins:{Id}</c> section wins when it exists, otherwise the | ||
| /// <c>Plugins:{Name}</c> section is used. This matches the scoping used by | ||
| /// <see cref="AuthKitPluginContext"/>. | ||
| /// </para> | ||
| /// </remarks> | ||
| public void BindConfiguration<TOptions>(IServiceCollection services, | ||
| IConfiguration configuration) | ||
| where TOptions : class | ||
| { | ||
| ArgumentNullException.ThrowIfNull(plugin); | ||
| ArgumentNullException.ThrowIfNull(services); | ||
| ArgumentNullException.ThrowIfNull(configuration); | ||
|
|
||
| services.Configure<TOptions>(plugin.GetPluginConfiguration(configuration)); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Resolves the configuration section scoped to a plugin. | ||
| /// </summary> | ||
| /// <param name="configuration">The application configuration.</param> | ||
| /// <returns> | ||
| /// The <c>Plugins:{Id}</c> section when it has children; otherwise, the | ||
| /// <c>Plugins:{Name}</c> section. | ||
| /// </returns> | ||
| /// <remarks> | ||
| /// <para> | ||
| /// Plugin sections live under the Plugins root. The stable plugin ID is | ||
| /// checked first an empty or missing ID section falls back to the plugin name. | ||
| /// This is the single resolution rule used by the host | ||
| /// (<see cref="AuthKitPluginContext"/>) and by options binding. | ||
| /// </para> | ||
| /// </remarks> | ||
| public IConfiguration GetPluginConfiguration(IConfiguration configuration) | ||
| { | ||
| ArgumentNullException.ThrowIfNull(plugin); | ||
| ArgumentNullException.ThrowIfNull(configuration); | ||
|
|
||
| var plugins = configuration.GetSection("Plugins"); | ||
| var byId = plugins.GetSection(plugin.Id); | ||
|
|
||
| return byId.GetChildren().Any() | ||
| ? byId | ||
| : plugins.GetSection(plugin.Name); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Checks if the plugin supports the specified capability. | ||
| /// </summary> | ||
| /// <param name="capability">The ability to check.</param> | ||
| /// <returns>true if the plugin supports the capability; otherwise, <c>false</c>.</returns> | ||
| /// <remarks>The comparison is case-insensitive.</remarks> | ||
| public bool Supports(string capability) => | ||
| plugin == null | ||
| ? throw new ArgumentNullException(nameof(plugin)) | ||
| : plugin.Capabilities.Contains(capability, StringComparer.OrdinalIgnoreCase); | ||
|
|
||
| /// <summary> | ||
| /// Checks if the plugin has the specified dependency. | ||
| /// </summary> | ||
| /// <param name="dependencyId">The dependency ID to check.</param> | ||
| /// <returns><c>true</c> if the plugin depends on the specified dependency; otherwise, <c>false</c>.</returns> | ||
| public bool HasDependency(string dependencyId) => | ||
| plugin == null | ||
| ? throw new ArgumentNullException(nameof(plugin)) | ||
| : plugin.DependsOn.Contains(dependencyId, StringComparer.OrdinalIgnoreCase); | ||
| } |
Comment on lines
+92
to
+116
| extension(PluginManifest manifest) | ||
| { | ||
| /// <summary> | ||
| /// Checks if the plugin manifest supports the specified capability. | ||
| /// </summary> | ||
| /// <param name="capability">The ability to check.</param> | ||
| /// <returns>true if the manifest supports the capability; otherwise, <c>false</c>.</returns> | ||
| /// <remarks> | ||
| /// The comparison is case-insensitive. | ||
| /// </remarks> | ||
| public bool Supports(string capability) => | ||
| manifest == null | ||
| ? throw new ArgumentNullException(nameof(manifest)) | ||
| : manifest.Capabilities.Contains(capability, StringComparer.OrdinalIgnoreCase); | ||
|
|
||
| /// <summary> | ||
| /// Checks if the plugin manifest has the specified dependency. | ||
| /// </summary> | ||
| /// <param name="dependencyId">The dependency ID to check.</param> | ||
| /// <returns><c>true</c> if the manifest declares the specified dependency; otherwise, <c>false</c>.</returns> | ||
| public bool HasDependency(string dependencyId) => | ||
| manifest == null | ||
| ? throw new ArgumentNullException(nameof(manifest)) | ||
| : manifest.DependsOn.Contains(dependencyId, StringComparer.OrdinalIgnoreCase); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Continuation of the plugin host integration after the Host Lifecycle work: adds plugin contracts for contributing to host-owned configuration strongly typed options binding, authentication and authorization hooks plus optional Marten and OpenAPI integration interfaces. The host now consults plugins when building those infrastructures, with dedicated contract and security tests and two ADRs.
Plugin Integration Contracts
IAuthKitPlugin.ConfigureAuthentication(AuthenticationBuilder)andIAuthKitPlugin.ConfigureAuthorization(AuthorizationOptions)optional hooks with default noop implementationsIAuthKitPlugin.BindConfiguration<TOptions>andPluginExtensions.BindConfiguration<TOptions>binding options from the standardPlugins:{Name}sectionAuthKit.Plugins.Integrationsproject withIMartenPluginandIOpenApiPluginfor contributing to the host Marten store and Swagger generator optionsHost Configuration Integration
IMartenPluginconfiguration while the host builds its Marten storeIOpenApiPluginconfiguration while building the Swagger generatorProgramand references the integrations project from the HostValidation
dotnet buildcompletes with zero errors across the solutiongit diff --checkpassesDocumentation
Result
Plugins can now bind their options directly and contribute to host-owned authentication, authorization, Marten, and OpenAPI configuration without taking ownership of those infrastructures.
Closes #11
Closes #12
Closes #13