Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
using Amazon;
using Shouldly;
using Wolverine.AmazonSns.Internal;
using Wolverine.Configuration.Capabilities;
using Xunit;

namespace Wolverine.AmazonSns.Tests;

// GH-3269: the SNS summary is the region (or an explicit ServiceURL such as LocalStack).
public class broker_connection_summary_3269
{
[Fact]
public void describe_endpoint_reports_the_region()
{
var transport = new AmazonSnsTransport();
transport.SnsConfig.RegionEndpoint = RegionEndpoint.USEast1;

transport.DescribeEndpoint().ShouldBe("us-east-1");
}

[Fact]
public void describe_endpoint_prefers_an_explicit_service_url()
{
var transport = new AmazonSnsTransport();
transport.SnsConfig.ServiceURL = "http://localhost:4566";

// The AWS SDK may normalize the URL (e.g. a trailing slash); DescribeEndpoint reports it verbatim.
transport.DescribeEndpoint().ShouldBe(transport.SnsConfig.ServiceURL);
transport.DescribeEndpoint()!.ShouldContain("localhost:4566");
}

[Fact]
public void broker_description_endpoint_is_populated()
{
var transport = new AmazonSnsTransport();
transport.SnsConfig.RegionEndpoint = RegionEndpoint.EUWest2;

new BrokerDescription(transport).Endpoint.ShouldBe("eu-west-2");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,24 @@ public override Uri ResourceUri
internal IAmazonSimpleNotificationService? SnsClient { get; private set; }
internal IAmazonSQS? SqsClient { get; private set; }

public override string? DescribeEndpoint()
{
// An explicit ServiceURL (e.g. LocalStack) is a plain endpoint URL; AWS credentials are supplied separately.
if (SnsConfig.ServiceURL.IsNotEmpty()) return SnsConfig.ServiceURL;

try
{
var region = SnsConfig.RegionEndpoint?.SystemName;
if (region.IsNotEmpty()) return region;
}
catch (Exception)
{
// RegionEndpoint resolution can probe ambient configuration; ignore.
}

return null;
}

public int LocalStackPort { get; set; }

public bool UseLocalStackInDevelopment { get; set; }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
using Amazon;
using Shouldly;
using Wolverine.AmazonSqs.Internal;
using Wolverine.Configuration.Capabilities;
using Xunit;

namespace Wolverine.AmazonSqs.Tests;

// GH-3269: the SQS summary is the region (or an explicit ServiceURL such as LocalStack). AWS credentials are supplied
// separately (CredentialSource), never as a string on the transport.
public class broker_connection_summary_3269
{
[Fact]
public void describe_endpoint_reports_the_region()
{
var transport = new AmazonSqsTransport();
transport.Config.RegionEndpoint = RegionEndpoint.USEast1;

transport.DescribeEndpoint().ShouldBe("us-east-1");
}

[Fact]
public void describe_endpoint_prefers_an_explicit_service_url()
{
var transport = new AmazonSqsTransport();
transport.Config.ServiceURL = "http://localhost:4566";

// The AWS SDK may normalize the URL (e.g. a trailing slash); DescribeEndpoint reports it verbatim.
transport.DescribeEndpoint().ShouldBe(transport.Config.ServiceURL);
transport.DescribeEndpoint()!.ShouldContain("localhost:4566");
}

[Fact]
public void broker_description_endpoint_is_populated()
{
var transport = new AmazonSqsTransport();
transport.Config.RegionEndpoint = RegionEndpoint.EUWest2;

new BrokerDescription(transport).Endpoint.ShouldBe("eu-west-2");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,24 @@ internal AmazonSqsTransport(IAmazonSQS client) : this()
Client = client;
}

public override string? DescribeEndpoint()
{
// An explicit ServiceURL (e.g. LocalStack) is a plain endpoint URL; AWS credentials are supplied separately.
if (Config.ServiceURL.IsNotEmpty()) return Config.ServiceURL;

try
{
var region = Config.RegionEndpoint?.SystemName;
if (region.IsNotEmpty()) return region;
}
catch (Exception)
{
// RegionEndpoint resolution can probe ambient configuration; ignore.
}

return null;
}

[DescribeAsConfigurationState]
public Func<IWolverineRuntime, AWSCredentials>? CredentialSource { get; set; }

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
using JasperFx.Descriptors;
using Shouldly;
using Wolverine.AzureServiceBus;
using Wolverine.Configuration.Capabilities;
using Xunit;

namespace Wolverine.AzureServiceBus.Tests;

// GH-3269: the Azure Service Bus summary is the namespace FQDN, never the SharedAccessKey.
public class broker_connection_summary_3269
{
private const string Secret = "aSharedAccessKeySecretValue123=";

[Fact]
public void describe_endpoint_uses_the_fully_qualified_namespace()
{
var transport = new AzureServiceBusTransport { FullyQualifiedNamespace = "myns.servicebus.windows.net" };
transport.DescribeEndpoint().ShouldBe("myns.servicebus.windows.net");
}

[Fact]
public void describe_endpoint_extracts_namespace_from_connection_string()
{
var transport = new AzureServiceBusTransport
{
ConnectionString =
$"Endpoint=sb://myns.servicebus.windows.net/;SharedAccessKeyName=root;SharedAccessKey={Secret}"
};

transport.DescribeEndpoint().ShouldBe("myns.servicebus.windows.net");
}

[Fact]
public void shared_access_key_never_leaks_into_the_description()
{
var transport = new AzureServiceBusTransport
{
ConnectionString =
$"Endpoint=sb://myns.servicebus.windows.net/;SharedAccessKeyName=root;SharedAccessKey={Secret}"
};

var description = new BrokerDescription(transport);

description.Endpoint.ShouldBe("myns.servicebus.windows.net");
BrokerSecretScanner.AssertNoSecret(description, Secret);
}
}

internal static class BrokerSecretScanner
{
public static void AssertNoSecret(BrokerDescription description, string secret)
{
(description.Endpoint ?? "").ShouldNotContain(secret);
foreach (var text in AllText(description))
{
text.ShouldNotContain(secret);
}
}

private static IEnumerable<string> AllText(OptionsDescription description)
{
if (description.Subject != null) yield return description.Subject;
foreach (var p in description.Properties)
{
if (p.Name != null) yield return p.Name;
if (p.Value != null) yield return p.Value;
}

foreach (var child in description.Children.Values)
foreach (var text in AllText(child))
yield return text;

foreach (var set in description.Sets.Values)
foreach (var row in set.Rows)
foreach (var text in AllText(row))
yield return text;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,16 @@ public string HostName
}
}

public override string? DescribeEndpoint()
{
if (!string.IsNullOrEmpty(FullyQualifiedNamespace)) return FullyQualifiedNamespace;

// The Endpoint host inside the connection string is the namespace FQDN; only the SharedAccessKey is secret.
if (!string.IsNullOrEmpty(ConnectionString)) return HostName;

return null;
}

protected override IEnumerable<Endpoint> explicitEndpoints()
{
foreach (var queue in Queues) yield return queue;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
using Google.Api.Gax;
using Shouldly;
using Wolverine.Configuration.Capabilities;
using Wolverine.Pubsub;
using Xunit;

namespace Wolverine.Pubsub.Tests;

// GH-3269: the GCP Pub/Sub summary is the project id (with an emulator marker when applicable).
public class broker_connection_summary_3269
{
[Fact]
public void describe_endpoint_reports_the_project_id()
{
var transport = new PubsubTransport("my-project");
transport.DescribeEndpoint().ShouldBe("project: my-project");
}

[Fact]
public void describe_endpoint_marks_the_emulator()
{
var transport = new PubsubTransport("my-project") { EmulatorDetection = EmulatorDetection.EmulatorOnly };
transport.DescribeEndpoint().ShouldBe("project: my-project (emulator)");
}

[Fact]
public void broker_description_endpoint_is_populated()
{
var transport = new PubsubTransport("my-project");
new BrokerDescription(transport).Endpoint.ShouldBe("project: my-project");
}
}
12 changes: 12 additions & 0 deletions src/Transports/GCP/Wolverine.Pubsub/PubsubTransport.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using Google.Api.Gax;
using Google.Cloud.PubSub.V1;
using JasperFx.Core;
using JasperFx.Descriptors;
using Wolverine.Configuration;
using Wolverine.Runtime;
using Wolverine.Transports;
Expand Down Expand Up @@ -37,20 +38,23 @@ public class PubsubTransport : BrokerTransport<PubsubEndpoint>, IAsyncDisposable
/// Multiple calls compose in order. Use the async signature when credential construction requires I/O
/// (e.g. fetching a token from Azure Key Vault or Azure IMDS).
/// </summary>
[IgnoreDescription]
public Func<PublisherServiceApiClientBuilder, ValueTask>? ConfigurePublisherApiBuilder { get; set; }

/// <summary>
/// Optional async callback to configure the <see cref="SubscriberServiceApiClientBuilder" /> before it is built.
/// Applied after <see cref="EmulatorDetection" /> is set, so it may override any transport-level defaults.
/// Multiple calls compose in order. Use the async signature when credential construction requires I/O.
/// </summary>
[IgnoreDescription]
public Func<SubscriberServiceApiClientBuilder, ValueTask>? ConfigureSubscriberApiBuilder { get; set; }

/// <summary>
/// Optional async callback to configure the <see cref="SubscriberClientBuilder" /> before it is built.
/// Applied after <see cref="EmulatorDetection" /> is set, so it may override any transport-level defaults.
/// Multiple calls compose in order. Use the async signature when credential construction requires I/O.
/// </summary>
[IgnoreDescription]
public Func<SubscriberClientBuilder, ValueTask>? ConfigureSubscriberClientBuilder { get; set; }

public PubsubTransport() : base(ProtocolName, "Google Cloud Platform Pub/Sub", ["gcp", ProtocolName])
Expand All @@ -66,6 +70,14 @@ public PubsubTransport(string projectId) : this()

public override Uri ResourceUri => new Uri("pubsub://" + ProjectId);

public override string? DescribeEndpoint()
{
if (string.IsNullOrWhiteSpace(ProjectId)) return null;
return EmulatorDetection == EmulatorDetection.None
? $"project: {ProjectId}"
: $"project: {ProjectId} (emulator)";
}

public ValueTask DisposeAsync()
{
return ValueTask.CompletedTask;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
using JasperFx.Descriptors;
using Shouldly;
using Wolverine.Configuration.Capabilities;
using Wolverine.Kafka.Internals;
using Xunit;

namespace Wolverine.Kafka.Tests;

// GH-3269: BrokerDescription must carry a sanitized, credential-free connection summary per transport. For Kafka the
// summary is the bootstrap servers, and the SASL credentials on the Confluent configs must NEVER leak into the
// diagnostic tree.
public class broker_connection_summary_3269
{
private const string Secret = "S3CR3T_SASL_PASSWORD";

[Fact]
public void describe_endpoint_reports_bootstrap_servers()
{
var transport = new KafkaTransport();
transport.ProducerConfig.BootstrapServers = "broker1:9092,broker2:9092";

transport.DescribeEndpoint().ShouldBe("broker1:9092,broker2:9092");
}

[Fact]
public void broker_description_endpoint_is_populated()
{
var transport = new KafkaTransport();
transport.ConsumerConfig.BootstrapServers = "kafka.internal:9092";

new BrokerDescription(transport).Endpoint.ShouldBe("kafka.internal:9092");
}

[Fact]
public void sasl_credentials_never_leak_into_the_description()
{
var transport = new KafkaTransport();
transport.ProducerConfig.BootstrapServers = "broker:9092";
transport.ProducerConfig.SaslUsername = "admin";
transport.ProducerConfig.SaslPassword = Secret;
transport.ConsumerConfig.SaslPassword = Secret;
transport.AdminClientConfig.SaslPassword = Secret;

var description = new BrokerDescription(transport);

// The sanitized target is present, the secret is not — anywhere in the reflected diagnostic tree.
description.Endpoint.ShouldBe("broker:9092");
BrokerSecretScanner.AssertNoSecret(description, Secret);
}
}

// Shared, project-local helper: recursively scan an OptionsDescription (Properties, Children, Sets) plus the typed
// BrokerDescription fields for a forbidden secret substring.
internal static class BrokerSecretScanner
{
public static void AssertNoSecret(BrokerDescription description, string secret)
{
(description.Endpoint ?? "").ShouldNotContain(secret);
foreach (var text in AllText(description))
{
text.ShouldNotContain(secret);
}
}

private static IEnumerable<string> AllText(OptionsDescription description)
{
if (description.Subject != null) yield return description.Subject;
foreach (var p in description.Properties)
{
if (p.Name != null) yield return p.Name;
if (p.Value != null) yield return p.Value;
}

foreach (var child in description.Children.Values)
foreach (var text in AllText(child))
yield return text;

foreach (var set in description.Sets.Values)
foreach (var row in set.Rows)
foreach (var text in AllText(row))
yield return text;
}
}
Loading
Loading