feat(blueprint-platform): add provider-agnostic secret provider contract

This commit is contained in:
José René White Enciso 2026-03-11 03:43:13 -06:00
parent c23ab0cbdc
commit d4c3bf8f8a
9 changed files with 196 additions and 5 deletions

View File

@ -12,10 +12,11 @@ This repository is a modular multi-package platform library set.
| Core.Blueprint.Redis | Redis integration helpers and extension points | Core.Blueprint.Common |
| Core.Blueprint.SQLServer | SQL Server integration helpers and extension points | Core.Blueprint.Common |
| Core.Blueprint.Storage | Blob/file storage integration helpers | Core.Blueprint.Common |
| Core.Blueprint.KeyVault | Key vault access integration helpers | Core.Blueprint.Common |
| Core.Blueprint.KeyVault | Key vault and provider-agnostic secret contract helpers | Core.Blueprint.Common |
## Boundary Rules
- Blueprint remains library-only.
- Identity abstractions are not owned by this repository.
- Downstream repositories consume package contracts from this repo.
- Secret provider abstractions are provider-agnostic; concrete provider adapters are bound at runtime.

View File

@ -0,0 +1,34 @@
# Provider-Agnostic Secret Provider Rollout
This package defines a provider-agnostic contract for secret lookup without binding to Vault, cloud providers, or environment files in core layers.
## Contract Surface
- `IBlueprintSecretProvider`
- `BlueprintSecretReference`
- `BlueprintSecretResolutionResult`
## Runtime Defaults
- `AddBlueprintKeyVaultModule(...)` now registers:
- `BlueprintKeyVaultRuntimeSettings` with:
- `VaultName`
- `SecretProviderName`
- `NoOpBlueprintSecretProvider` as default fallback.
The default fallback returns unresolved lookups and never introduces provider-specific behavior.
## Binding Strategy
1. Keep domain and application layers dependent only on `IBlueprintSecretProvider`.
2. Bind provider implementation at runtime through DI:
- Vault adapter
- Cloud secret manager adapter
- Environment/test adapter
3. Keep one active provider per deployment profile.
## Rollout Notes
- Stage 33 keeps this contract-only baseline.
- Concrete Vault/OIDC provider integration should be implemented in infrastructure/runtime layers only.
- Existing identity logic ownership remains in Thalos repositories.

View File

@ -0,0 +1,12 @@
namespace Core.Blueprint.KeyVault.Contracts;
/// <summary>
/// Defines a provider-agnostic secret lookup reference.
/// </summary>
/// <param name="Scope">Secret namespace, path, or scope name.</param>
/// <param name="Name">Secret key name inside the scope.</param>
/// <param name="Version">Optional secret version marker.</param>
public sealed record BlueprintSecretReference(
string Scope,
string Name,
string? Version = null);

View File

@ -0,0 +1,36 @@
namespace Core.Blueprint.KeyVault.Contracts;
/// <summary>
/// Represents the outcome of a secret provider lookup.
/// </summary>
/// <param name="IsResolved">True when a secret value was found.</param>
/// <param name="Value">Resolved secret value, if found.</param>
/// <param name="ProviderName">Name of the active secret provider.</param>
/// <param name="Version">Resolved version marker, if available.</param>
public sealed record BlueprintSecretResolutionResult(
bool IsResolved,
string? Value,
string ProviderName,
string? Version)
{
/// <summary>
/// Creates a resolved secret result.
/// </summary>
public static BlueprintSecretResolutionResult Resolved(
string value,
string providerName,
string? version = null)
{
return new BlueprintSecretResolutionResult(true, value, providerName, version);
}
/// <summary>
/// Creates a missing secret result.
/// </summary>
public static BlueprintSecretResolutionResult Missing(
string providerName,
string? version = null)
{
return new BlueprintSecretResolutionResult(false, null, providerName, version);
}
}

View File

@ -0,0 +1,17 @@
namespace Core.Blueprint.KeyVault.Contracts;
/// <summary>
/// Defines provider-agnostic secret retrieval operations.
/// </summary>
public interface IBlueprintSecretProvider
{
/// <summary>
/// Resolves a secret from the configured provider.
/// </summary>
/// <param name="reference">Provider-agnostic secret reference.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>Resolution result describing value and provider metadata.</returns>
ValueTask<BlueprintSecretResolutionResult> GetSecretAsync(
BlueprintSecretReference reference,
CancellationToken cancellationToken = default);
}

View File

@ -1,4 +1,5 @@
using Core.Blueprint.Common.DependencyInjection;
using Core.Blueprint.KeyVault.Contracts;
using Core.Blueprint.KeyVault.Runtime;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
@ -15,13 +16,18 @@ public static class BlueprintKeyVaultServiceCollectionExtensions
/// </summary>
/// <param name="services">Service collection.</param>
/// <param name="vaultName">Target key vault name.</param>
/// <param name="secretProviderName">Active provider name for secret resolution.</param>
/// <returns>Service collection for fluent chaining.</returns>
public static IServiceCollection AddBlueprintKeyVaultModule(
this IServiceCollection services,
string vaultName = "default-vault")
string vaultName = "default-vault",
string secretProviderName = "unconfigured")
{
services.AddBlueprintRuntimeCore();
services.TryAddSingleton(new BlueprintKeyVaultRuntimeSettings(ResolveVaultName(vaultName)));
services.TryAddSingleton(new BlueprintKeyVaultRuntimeSettings(
ResolveVaultName(vaultName),
ResolveProviderName(secretProviderName)));
services.TryAddSingleton<IBlueprintSecretProvider, NoOpBlueprintSecretProvider>();
return services;
}
@ -35,4 +41,14 @@ public static class BlueprintKeyVaultServiceCollectionExtensions
return "default-vault";
}
private static string ResolveProviderName(string secretProviderName)
{
if (!string.IsNullOrWhiteSpace(secretProviderName))
{
return secretProviderName;
}
return "unconfigured";
}
}

View File

@ -4,4 +4,7 @@ namespace Core.Blueprint.KeyVault.Runtime;
/// Defines runtime settings for key vault integration helpers.
/// </summary>
/// <param name="VaultName">Target key vault name.</param>
public sealed record BlueprintKeyVaultRuntimeSettings(string VaultName);
/// <param name="SecretProviderName">Active secret provider name bound through DI.</param>
public sealed record BlueprintKeyVaultRuntimeSettings(
string VaultName,
string SecretProviderName);

View File

@ -0,0 +1,20 @@
using Core.Blueprint.KeyVault.Contracts;
namespace Core.Blueprint.KeyVault.Runtime;
/// <summary>
/// Default provider used when no concrete secret manager adapter is configured.
/// </summary>
public sealed class NoOpBlueprintSecretProvider(BlueprintKeyVaultRuntimeSettings settings)
: IBlueprintSecretProvider
{
/// <inheritdoc />
public ValueTask<BlueprintSecretResolutionResult> GetSecretAsync(
BlueprintSecretReference reference,
CancellationToken cancellationToken = default)
{
var providerName = settings.SecretProviderName;
var result = BlueprintSecretResolutionResult.Missing(providerName, reference.Version);
return ValueTask.FromResult(result);
}
}

View File

@ -1,3 +1,4 @@
using Core.Blueprint.KeyVault.Contracts;
using Core.Blueprint.KeyVault.DependencyInjection;
using Core.Blueprint.KeyVault.Runtime;
using Microsoft.Extensions.DependencyInjection;
@ -11,11 +12,62 @@ public class UnitTest1
{
var services = new ServiceCollection();
services.AddBlueprintKeyVaultModule("agile-vault");
services.AddBlueprintKeyVaultModule("agile-vault", "vault");
using var provider = services.BuildServiceProvider();
var settings = provider.GetRequiredService<BlueprintKeyVaultRuntimeSettings>();
Assert.Equal("agile-vault", settings.VaultName);
Assert.Equal("vault", settings.SecretProviderName);
}
[Fact]
public async Task AddBlueprintKeyVaultModule_WhenNoProviderBound_UsesNoOpProvider()
{
var services = new ServiceCollection();
services.AddBlueprintKeyVaultModule("agile-vault");
await using var provider = services.BuildServiceProvider();
var secretProvider = provider.GetRequiredService<IBlueprintSecretProvider>();
var result = await secretProvider.GetSecretAsync(
new BlueprintSecretReference("thalos/oidc", "google-client-secret"));
Assert.False(result.IsResolved);
Assert.Null(result.Value);
Assert.Equal("unconfigured", result.ProviderName);
}
[Fact]
public async Task AddBlueprintKeyVaultModule_WhenProviderAlreadyRegistered_PreservesCustomProvider()
{
var services = new ServiceCollection();
services.AddSingleton<IBlueprintSecretProvider, FakeSecretProvider>();
services.AddBlueprintKeyVaultModule();
await using var provider = services.BuildServiceProvider();
var secretProvider = provider.GetRequiredService<IBlueprintSecretProvider>();
var result = await secretProvider.GetSecretAsync(
new BlueprintSecretReference("scope", "name", "v1"));
Assert.True(result.IsResolved);
Assert.Equal("custom-provider", result.ProviderName);
Assert.Equal("secret-value", result.Value);
Assert.Equal("v1", result.Version);
}
private sealed class FakeSecretProvider : IBlueprintSecretProvider
{
public ValueTask<BlueprintSecretResolutionResult> GetSecretAsync(
BlueprintSecretReference reference,
CancellationToken cancellationToken = default)
{
var result = BlueprintSecretResolutionResult.Resolved(
"secret-value",
"custom-provider",
reference.Version);
return ValueTask.FromResult(result);
}
}
}