-
Notifications
You must be signed in to change notification settings - Fork 184
💥 [BREAKING] enable TLS if api key provided #2740
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -39,8 +39,14 @@ public class ServiceStubsOptions { | |
|
|
||
| protected final @Nullable Consumer<ManagedChannelBuilder<?>> channelInitializer; | ||
|
|
||
| /** Indicates whether basic HTTPS/SSL/TLS should be enabled * */ | ||
| protected final boolean enableHttps; | ||
| /** | ||
| * Indicates whether basic HTTPS/SSL/TLS should be enabled. Null means not explicitly set by the | ||
| * user, allowing runtime derivation of the effective value. | ||
| */ | ||
| protected final Boolean enableHttps; | ||
|
|
||
| /** Indicates whether an API key was provided, used for automatic TLS enablement */ | ||
| protected final boolean apiKeyProvided; | ||
|
|
||
| /** The user provided context for SSL/TLS over gRPC * */ | ||
| protected final SslContext sslContext; | ||
|
|
@@ -113,6 +119,7 @@ public class ServiceStubsOptions { | |
| this.target = that.target; | ||
| this.channelInitializer = that.channelInitializer; | ||
| this.enableHttps = that.enableHttps; | ||
| this.apiKeyProvided = that.apiKeyProvided; | ||
| this.sslContext = that.sslContext; | ||
| this.healthCheckAttemptTimeout = that.healthCheckAttemptTimeout; | ||
| this.systemInfoTimeout = that.systemInfoTimeout; | ||
|
|
@@ -134,7 +141,8 @@ public class ServiceStubsOptions { | |
| ManagedChannel channel, | ||
| String target, | ||
| @Nullable Consumer<ManagedChannelBuilder<?>> channelInitializer, | ||
| boolean enableHttps, | ||
| Boolean enableHttps, | ||
| boolean apiKeyProvided, | ||
| SslContext sslContext, | ||
| Duration healthCheckAttemptTimeout, | ||
| Duration healthCheckTimeout, | ||
|
|
@@ -154,6 +162,7 @@ public class ServiceStubsOptions { | |
| this.target = target; | ||
| this.channelInitializer = channelInitializer; | ||
| this.enableHttps = enableHttps; | ||
| this.apiKeyProvided = apiKeyProvided; | ||
| this.sslContext = sslContext; | ||
| this.healthCheckAttemptTimeout = healthCheckAttemptTimeout; | ||
| this.healthCheckTimeout = healthCheckTimeout; | ||
|
|
@@ -202,11 +211,24 @@ public Consumer<ManagedChannelBuilder<?>> getChannelInitializer() { | |
| } | ||
|
|
||
| /** | ||
| * @return if gRPC should use SSL/TLS; Ignored and assumed {@code true} if {@link | ||
| * #getSslContext()} is set | ||
| * Returns whether gRPC should use SSL/TLS. This method computes the effective value based on: | ||
| * | ||
| * <ul> | ||
| * <li>If explicitly set via {@link Builder#setEnableHttps(boolean)}, returns that value | ||
| * <li>If an API key was provided and no custom SSL context or channel is set, returns {@code | ||
| * true} (auto-enabled) | ||
| * <li>Otherwise returns {@code false} | ||
| * </ul> | ||
| * | ||
| * <p>Note: When {@link #getSslContext()} is set, TLS is handled by the SSL context regardless of | ||
| * this value. | ||
| * | ||
| * @return if gRPC should use SSL/TLS | ||
| */ | ||
| public boolean getEnableHttps() { | ||
| return enableHttps; | ||
| return (enableHttps != null) | ||
| ? enableHttps | ||
| : (apiKeyProvided && sslContext == null && channel == null); | ||
| } | ||
|
|
||
| /** | ||
|
|
@@ -325,12 +347,13 @@ public boolean equals(Object o) { | |
| if (this == o) return true; | ||
| if (o == null || getClass() != o.getClass()) return false; | ||
| ServiceStubsOptions that = (ServiceStubsOptions) o; | ||
| return enableHttps == that.enableHttps | ||
| return apiKeyProvided == that.apiKeyProvided | ||
| && enableKeepAlive == that.enableKeepAlive | ||
| && keepAlivePermitWithoutStream == that.keepAlivePermitWithoutStream | ||
| && Objects.equals(channel, that.channel) | ||
| && Objects.equals(target, that.target) | ||
| && Objects.equals(channelInitializer, that.channelInitializer) | ||
| && Objects.equals(enableHttps, that.enableHttps) | ||
| && Objects.equals(sslContext, that.sslContext) | ||
| && Objects.equals(healthCheckAttemptTimeout, that.healthCheckAttemptTimeout) | ||
| && Objects.equals(healthCheckTimeout, that.healthCheckTimeout) | ||
|
|
@@ -353,6 +376,7 @@ public int hashCode() { | |
| target, | ||
| channelInitializer, | ||
| enableHttps, | ||
| apiKeyProvided, | ||
| sslContext, | ||
| healthCheckAttemptTimeout, | ||
| healthCheckTimeout, | ||
|
|
@@ -418,7 +442,7 @@ public String toString() { | |
| public static class Builder<T extends Builder<T>> { | ||
| private ManagedChannel channel; | ||
| private SslContext sslContext; | ||
| private boolean enableHttps; | ||
| private Boolean enableHttps; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Here's a contrived situation:
I don't mind if we break and/or don't-support this scenario, just wanted to mention it
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we should at least document that scenario since it is confusing. Arguably if
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm, maybe should revert the change: and make The semantics might be clearer this way tbh
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If we don't consider changing the return type of
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hm I think changing the return type to a
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I didn't figure it'd be that common, but meh. This works for me, but what should be the return value of
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah I don't think it is that common, my main concern is that a user who does call it doesn't realize the type can now be null now
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @maciejdudko do you have any thoughts on this point?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| private String target; | ||
| private Consumer<ManagedChannelBuilder<?>> channelInitializer; | ||
| private Duration healthCheckAttemptTimeout; | ||
|
|
@@ -435,6 +459,7 @@ public static class Builder<T extends Builder<T>> { | |
| private Collection<GrpcMetadataProvider> grpcMetadataProviders; | ||
| private Collection<ClientInterceptor> grpcClientInterceptors; | ||
| private Scope metricsScope; | ||
| private boolean apiKeyProvided; | ||
|
|
||
| protected Builder() {} | ||
|
|
||
|
|
@@ -443,6 +468,7 @@ protected Builder(ServiceStubsOptions options) { | |
| this.target = options.target; | ||
| this.channelInitializer = options.channelInitializer; | ||
| this.enableHttps = options.enableHttps; | ||
| this.apiKeyProvided = options.apiKeyProvided; | ||
| this.sslContext = options.sslContext; | ||
| this.healthCheckAttemptTimeout = options.healthCheckAttemptTimeout; | ||
| this.healthCheckTimeout = options.healthCheckTimeout; | ||
|
|
@@ -455,8 +481,15 @@ protected Builder(ServiceStubsOptions options) { | |
| this.connectionBackoffResetFrequency = options.connectionBackoffResetFrequency; | ||
| this.grpcReconnectFrequency = options.grpcReconnectFrequency; | ||
| this.headers = options.headers; | ||
| this.grpcMetadataProviders = options.grpcMetadataProviders; | ||
| this.grpcClientInterceptors = options.grpcClientInterceptors; | ||
| // Make mutable copies of collections to allow adding more items | ||
| this.grpcMetadataProviders = | ||
| options.grpcMetadataProviders != null && !options.grpcMetadataProviders.isEmpty() | ||
| ? new ArrayList<>(options.grpcMetadataProviders) | ||
| : null; | ||
| this.grpcClientInterceptors = | ||
| options.grpcClientInterceptors != null && !options.grpcClientInterceptors.isEmpty() | ||
| ? new ArrayList<>(options.grpcClientInterceptors) | ||
| : null; | ||
| this.metricsScope = options.metricsScope; | ||
| } | ||
|
|
||
|
|
@@ -613,6 +646,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(); | ||
|
|
@@ -804,6 +838,7 @@ public ServiceStubsOptions build() { | |
| this.target, | ||
| this.channelInitializer, | ||
| this.enableHttps, | ||
| this.apiKeyProvided, | ||
| this.sslContext, | ||
| this.healthCheckAttemptTimeout, | ||
| this.healthCheckTimeout, | ||
|
|
@@ -837,7 +872,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"); | ||
| } | ||
|
|
@@ -866,6 +901,7 @@ public ServiceStubsOptions validateAndBuildWithDefaults() { | |
| target, | ||
| this.channelInitializer, | ||
| this.enableHttps, | ||
| this.apiKeyProvided, | ||
| this.sslContext, | ||
| healthCheckAttemptTimeout, | ||
| healthCheckTimeout, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,154 @@ | ||
| package io.temporal.serviceclient; | ||
|
|
||
| import static org.junit.Assert.*; | ||
|
|
||
| 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 testBuilderFromOptionsPreservesDefaultTLSBehavior() { | ||
| ServiceStubsOptions options1 = | ||
| WorkflowServiceStubsOptions.newBuilder() | ||
| .setTarget("localhost:7233") | ||
| .validateAndBuildWithDefaults(); | ||
|
|
||
| assertFalse(options1.getEnableHttps()); | ||
|
|
||
| ServiceStubsOptions options2 = | ||
| WorkflowServiceStubsOptions.newBuilder(options1) | ||
| .addApiKey(() -> "test-api-key") | ||
| .validateAndBuildWithDefaults(); | ||
|
|
||
| assertTrue( | ||
| "TLS should auto-enable when API key is added to builder from options that had default TLS behavior", | ||
| options2.getEnableHttps()); | ||
| } | ||
|
|
||
| @Test | ||
| public void testBuilderFromOptionsWithExplicitTLSDisableStaysDisabled() { | ||
| ServiceStubsOptions options1 = | ||
| WorkflowServiceStubsOptions.newBuilder() | ||
| .setTarget("localhost:7233") | ||
| .setEnableHttps(false) | ||
| .validateAndBuildWithDefaults(); | ||
|
|
||
| assertFalse(options1.getEnableHttps()); | ||
|
|
||
| ServiceStubsOptions options2 = | ||
| WorkflowServiceStubsOptions.newBuilder(options1) | ||
| .addApiKey(() -> "test-api-key") | ||
| .validateAndBuildWithDefaults(); | ||
|
|
||
| assertFalse( | ||
| "TLS should stay disabled when explicitly set to false, even with API key", | ||
| options2.getEnableHttps()); | ||
| } | ||
|
|
||
| @Test | ||
| public void testBuilderFromOptionsWithExplicitTLSEnableStaysEnabled() { | ||
| ServiceStubsOptions options1 = | ||
| WorkflowServiceStubsOptions.newBuilder() | ||
| .setTarget("localhost:7233") | ||
| .setEnableHttps(true) | ||
| .validateAndBuildWithDefaults(); | ||
|
|
||
| assertTrue(options1.getEnableHttps()); | ||
|
|
||
| ServiceStubsOptions options2 = | ||
| WorkflowServiceStubsOptions.newBuilder(options1).validateAndBuildWithDefaults(); | ||
|
|
||
| assertTrue("TLS should stay enabled when explicitly set to true", options2.getEnableHttps()); | ||
| } | ||
|
|
||
| @Test | ||
| public void testSpringBootStyleAutoTLSWithApiKey() { | ||
| ServiceStubsOptions options1 = | ||
| WorkflowServiceStubsOptions.newBuilder() | ||
| .setTarget("my-namespace.tmprl.cloud:7233") | ||
| .addApiKey(() -> "my-api-key") | ||
| .validateAndBuildWithDefaults(); | ||
|
|
||
| assertTrue( | ||
| "TLS should auto-enable when API key is provided without explicit TLS setting", | ||
| options1.getEnableHttps()); | ||
|
|
||
| ServiceStubsOptions options2 = | ||
| WorkflowServiceStubsOptions.newBuilder() | ||
| .setTarget("localhost:7233") | ||
| .setEnableHttps(false) | ||
| .addApiKey(() -> "my-api-key") | ||
| .validateAndBuildWithDefaults(); | ||
|
|
||
| assertFalse( | ||
| "TLS should stay disabled when explicitly set to false, even with API key", | ||
| options2.getEnableHttps()); | ||
|
|
||
| ServiceStubsOptions options3 = | ||
| WorkflowServiceStubsOptions.newBuilder() | ||
| .setTarget("localhost:7233") | ||
| .validateAndBuildWithDefaults(); | ||
|
|
||
| assertFalse( | ||
| "TLS should be disabled when no API key and no explicit TLS setting", | ||
| options3.getEnableHttps()); | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I assume we don't want to support a user calling
setEnableHttps(null)to restore to "default"? I don't mind, just want to confirm