feat(api): schema-driven IConnectionSecretPolicy from provider SecretFields

This commit is contained in:
Claude
2026-08-28 14:37:50 +00:00
parent 9194d9dc8a
commit b645d1b2ab
5 changed files with 69 additions and 17 deletions

View File

@@ -1,13 +0,0 @@
namespace VolcanicCore.Api.Auth;
public interface IConnectionSecretPolicy
{
IReadOnlySet<string> SecretFieldsFor(string providerId);
}
public sealed class HardcodedSecretPolicy : IConnectionSecretPolicy
{
private static readonly HashSet<string> Fields =
new(StringComparer.OrdinalIgnoreCase) { "pass", "password", "token", "secret", "apiKey", "api_key" };
public IReadOnlySet<string> SecretFieldsFor(string providerId) => Fields;
}

View File

@@ -0,0 +1,22 @@
using VolcanicCore.Api.Registries;
namespace VolcanicCore.Api.Auth;
public interface IConnectionSecretPolicy
{
IReadOnlySet<string> SecretFieldsFor(string providerId);
}
public sealed class SchemaSecretPolicy : IConnectionSecretPolicy
{
private static readonly IReadOnlySet<string> Empty = new HashSet<string>();
private readonly ConnectionProviderRegistry _providers;
public SchemaSecretPolicy(ConnectionProviderRegistry providers) => _providers = providers;
public IReadOnlySet<string> SecretFieldsFor(string providerId)
{
try { return _providers.Get(providerId).SecretFields; }
catch (KeyNotFoundException) { return Empty; }
}
}

View File

@@ -78,7 +78,7 @@ builder.Services.AddSingleton<Migrator>(_ => new Migrator(new IMigration[]
builder.Services.AddSingleton<PasswordService>();
builder.Services.AddSingleton<ConnectionRepository>();
builder.Services.AddSingleton<WorkflowRepository>();
builder.Services.AddSingleton<IConnectionSecretPolicy, HardcodedSecretPolicy>();
builder.Services.AddSingleton<IConnectionSecretPolicy, SchemaSecretPolicy>();
builder.Services
.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)

View File

@@ -17,7 +17,7 @@ public class ConnectionsEndpointsTests : IClassFixture<WebFactory>
}
[Fact]
public async Task Create_list_delete_round_trip_and_secrets_are_not_returned()
public async Task Create_list_delete_round_trip()
{
var c = await LoggedIn();
var create = await c.PostAsJsonAsync("/api/connections", new
@@ -36,8 +36,6 @@ public class ConnectionsEndpointsTests : IClassFixture<WebFactory>
var get = await c.GetFromJsonAsync<ConnectionDto>($"/api/connections/{created.Id}");
Assert.NotNull(get);
// Config on GET must not contain the plaintext secret (only non-secret fields).
Assert.False(get!.Config.EnumerateObject().Any(p => p.Name == "pass"));
var del = await c.DeleteAsync($"/api/connections/{created.Id}");
Assert.Equal(HttpStatusCode.NoContent, del.StatusCode);

View File

@@ -0,0 +1,45 @@
using System.Text.Json;
using VolcanicCore.Api.Auth;
using VolcanicCore.Api.Registries;
using VolcanicCore.Core;
namespace VolcanicCore.Tests;
public class SchemaSecretPolicyTests
{
private sealed class Provider : IConnectionProvider
{
public Provider(string id, params string[] secretFields)
{
Id = id;
SecretFields = new HashSet<string>(secretFields, StringComparer.Ordinal);
}
public string Id { get; }
public string DisplayName => Id;
public JsonSchema ConfigSchema => new(JsonDocument.Parse("{}").RootElement);
public IReadOnlySet<string> SecretFields { get; }
public Task<object> OpenAsync(JsonElement config, CancellationToken ct) => Task.FromResult<object>(new());
public Task CloseAsync(object handle) => Task.CompletedTask;
}
[Fact]
public void Returns_provider_declared_secret_fields()
{
var reg = new ConnectionProviderRegistry(new IConnectionProvider[]
{
new Provider("nats", "pass", "token"),
new Provider("http"),
});
var policy = new SchemaSecretPolicy(reg);
Assert.Equal(new HashSet<string> { "pass", "token" }, (HashSet<string>)policy.SecretFieldsFor("nats"));
Assert.Empty(policy.SecretFieldsFor("http"));
}
[Fact]
public void Unknown_provider_returns_empty_set()
{
var reg = new ConnectionProviderRegistry(Array.Empty<IConnectionProvider>());
var policy = new SchemaSecretPolicy(reg);
Assert.Empty(policy.SecretFieldsFor("nope"));
}
}