feat(api): schema-driven IConnectionSecretPolicy from provider SecretFields
This commit is contained in:
@@ -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;
|
|
||||||
}
|
|
||||||
22
src/VolcanicCore.Api/Auth/SchemaSecretPolicy.cs
Normal file
22
src/VolcanicCore.Api/Auth/SchemaSecretPolicy.cs
Normal 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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -78,7 +78,7 @@ builder.Services.AddSingleton<Migrator>(_ => new Migrator(new IMigration[]
|
|||||||
builder.Services.AddSingleton<PasswordService>();
|
builder.Services.AddSingleton<PasswordService>();
|
||||||
builder.Services.AddSingleton<ConnectionRepository>();
|
builder.Services.AddSingleton<ConnectionRepository>();
|
||||||
builder.Services.AddSingleton<WorkflowRepository>();
|
builder.Services.AddSingleton<WorkflowRepository>();
|
||||||
builder.Services.AddSingleton<IConnectionSecretPolicy, HardcodedSecretPolicy>();
|
builder.Services.AddSingleton<IConnectionSecretPolicy, SchemaSecretPolicy>();
|
||||||
|
|
||||||
builder.Services
|
builder.Services
|
||||||
.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
|
.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ public class ConnectionsEndpointsTests : IClassFixture<WebFactory>
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[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 c = await LoggedIn();
|
||||||
var create = await c.PostAsJsonAsync("/api/connections", new
|
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}");
|
var get = await c.GetFromJsonAsync<ConnectionDto>($"/api/connections/{created.Id}");
|
||||||
Assert.NotNull(get);
|
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}");
|
var del = await c.DeleteAsync($"/api/connections/{created.Id}");
|
||||||
Assert.Equal(HttpStatusCode.NoContent, del.StatusCode);
|
Assert.Equal(HttpStatusCode.NoContent, del.StatusCode);
|
||||||
|
|||||||
45
tests/VolcanicCore.Tests/SchemaSecretPolicyTests.cs
Normal file
45
tests/VolcanicCore.Tests/SchemaSecretPolicyTests.cs
Normal 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"));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user