diff --git a/temporal-serviceclient/src/main/java/io/temporal/serviceclient/ServiceStubsOptions.java b/temporal-serviceclient/src/main/java/io/temporal/serviceclient/ServiceStubsOptions.java index 4f6356665..a5595f101 100644 --- a/temporal-serviceclient/src/main/java/io/temporal/serviceclient/ServiceStubsOptions.java +++ b/temporal-serviceclient/src/main/java/io/temporal/serviceclient/ServiceStubsOptions.java @@ -418,7 +418,7 @@ public String toString() { public static class Builder> { private ManagedChannel channel; private SslContext sslContext; - private boolean enableHttps; + private Boolean enableHttps; private String target; private Consumer> channelInitializer; private Duration healthCheckAttemptTimeout; @@ -435,6 +435,7 @@ public static class Builder> { private Collection grpcMetadataProviders; private Collection grpcClientInterceptors; private Scope metricsScope; + private boolean apiKeyProvided; protected Builder() {} @@ -613,6 +614,7 @@ public T addGrpcMetadataProvider(GrpcMetadataProvider grpcMetadataProvider) { * @return {@code this} */ public T addApiKey(AuthorizationTokenSupplier apiKey) { + this.apiKeyProvided = true; addGrpcMetadataProvider( new AuthorizationGrpcMetadataProvider(() -> "Bearer " + apiKey.supply())); return self(); @@ -803,7 +805,7 @@ public ServiceStubsOptions build() { this.channel, this.target, this.channelInitializer, - this.enableHttps, + this.enableHttps != null ? this.enableHttps : false, this.sslContext, this.healthCheckAttemptTimeout, this.healthCheckTimeout, @@ -837,7 +839,7 @@ public ServiceStubsOptions validateAndBuildWithDefaults() { "Only one of the 'sslContext' or 'channel' options can be set at a time"); } - if (this.enableHttps && this.channel != null) { + if (Boolean.TRUE.equals(this.enableHttps) && this.channel != null) { throw new IllegalStateException( "Only one of the 'enableHttps' or 'channel' options can be set at a time"); } @@ -851,6 +853,14 @@ public ServiceStubsOptions validateAndBuildWithDefaults() { Collection grpcClientInterceptors = MoreObjects.firstNonNull(this.grpcClientInterceptors, Collections.emptyList()); + // Resolve enableHttps: explicit value, auto-enable with API key, or default false + boolean enableHttps = false; + if (this.enableHttps != null) { + enableHttps = this.enableHttps; + } else if (this.apiKeyProvided && this.sslContext == null && this.channel == null) { + enableHttps = true; + } + Scope metricsScope = this.metricsScope != null ? this.metricsScope : new NoopScope(); Duration healthCheckAttemptTimeout = this.healthCheckAttemptTimeout != null @@ -865,7 +875,7 @@ public ServiceStubsOptions validateAndBuildWithDefaults() { this.channel, target, this.channelInitializer, - this.enableHttps, + enableHttps, this.sslContext, healthCheckAttemptTimeout, healthCheckTimeout, diff --git a/temporal-serviceclient/src/test/java/io/temporal/serviceclient/ServiceStubsOptionsTest.java b/temporal-serviceclient/src/test/java/io/temporal/serviceclient/ServiceStubsOptionsTest.java new file mode 100644 index 000000000..1e7d7571c --- /dev/null +++ b/temporal-serviceclient/src/test/java/io/temporal/serviceclient/ServiceStubsOptionsTest.java @@ -0,0 +1,100 @@ +package io.temporal.serviceclient; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.mock; + +import io.grpc.ManagedChannel; +import io.grpc.netty.shaded.io.netty.handler.ssl.SslContext; +import org.junit.Test; + +public class ServiceStubsOptionsTest { + + @Test + public void testTLSEnabledByDefaultWhenAPIKeyProvided() { + ServiceStubsOptions options = + WorkflowServiceStubsOptions.newBuilder() + .setTarget("localhost:7233") + .addApiKey(() -> "test-api-key") + .validateAndBuildWithDefaults(); + + assertTrue(options.getEnableHttps()); + } + + @Test + public void testExplicitTLSDisableBeforeAPIKeyStillDisables() { + ServiceStubsOptions options = + WorkflowServiceStubsOptions.newBuilder() + .setTarget("localhost:7233") + .setEnableHttps(false) + .addApiKey(() -> "test-api-key") + .validateAndBuildWithDefaults(); + + // Explicit TLS=false should take precedence regardless of order + assertFalse(options.getEnableHttps()); + } + + @Test + public void testExplicitTLSDisableAfterAPIKeyStillDisables() { + ServiceStubsOptions options = + WorkflowServiceStubsOptions.newBuilder() + .setTarget("localhost:7233") + .addApiKey(() -> "test-api-key") + .setEnableHttps(false) + .validateAndBuildWithDefaults(); + + // Explicit TLS=false should take precedence regardless of order + assertFalse(options.getEnableHttps()); + } + + @Test + public void testTLSDisabledByDefaultWithoutAPIKey() { + ServiceStubsOptions options = + WorkflowServiceStubsOptions.newBuilder() + .setTarget("localhost:7233") + .validateAndBuildWithDefaults(); + + assertFalse(options.getEnableHttps()); + } + + @Test + public void testExplicitTLSEnableWithoutAPIKey() { + ServiceStubsOptions options = + WorkflowServiceStubsOptions.newBuilder() + .setTarget("localhost:7233") + .setEnableHttps(true) + .validateAndBuildWithDefaults(); + + assertTrue(options.getEnableHttps()); + } + + @Test + public void testTLSNotAutoEnabledWhenSslContextProvided() { + // When user provides custom sslContext, they're handling TLS themselves + // so enableHttps should not be auto-enabled + SslContext sslContext = mock(SslContext.class); + ServiceStubsOptions options = + WorkflowServiceStubsOptions.newBuilder() + .setTarget("localhost:7233") + .addApiKey(() -> "test-api-key") + .setSslContext(sslContext) + .validateAndBuildWithDefaults(); + + // enableHttps stays false because sslContext handles TLS + assertFalse(options.getEnableHttps()); + assertNotNull(options.getSslContext()); + } + + @Test + public void testTLSNotAutoEnabledWhenCustomChannelProvided() { + // When user provides custom channel, they're managing connection themselves + // so enableHttps should not be auto-enabled + ManagedChannel channel = mock(ManagedChannel.class); + ServiceStubsOptions options = + WorkflowServiceStubsOptions.newBuilder() + .setChannel(channel) + .addApiKey(() -> "test-api-key") + .validateAndBuildWithDefaults(); + + assertFalse(options.getEnableHttps()); + } +}