From 0c8ebc73d2f21d94a6f44d8eec12eb90fcca179b Mon Sep 17 00:00:00 2001 From: Thomas Hardy Date: Mon, 1 Dec 2025 16:55:21 -0800 Subject: [PATCH 1/7] enable TLS if api key specified --- src/Temporalio/Bridge/OptionsExtensions.cs | 12 +++++- .../Client/TemporalConnectionOptions.cs | 22 +++++++++-- .../Client/TemporalConnectionOptionsTests.cs | 39 +++++++++++++++++++ .../Temporalio.Tests/Temporalio.Tests.csproj | 1 + 4 files changed, 69 insertions(+), 5 deletions(-) create mode 100644 tests/Temporalio.Tests/Client/TemporalConnectionOptionsTests.cs diff --git a/src/Temporalio/Bridge/OptionsExtensions.cs b/src/Temporalio/Bridge/OptionsExtensions.cs index 2ad8cd19..641be98a 100644 --- a/src/Temporalio/Bridge/OptionsExtensions.cs +++ b/src/Temporalio/Bridge/OptionsExtensions.cs @@ -241,7 +241,15 @@ public static unsafe Interop.TemporalCoreClientOptions ToInteropOptions( { throw new ArgumentException("Identity missing from options."); } - var scheme = options.Tls == null ? "http" : "https"; + + // Auto-enable TLS when API key is provided and TLS is not explicitly set + var tls = options.Tls; + if (!string.IsNullOrEmpty(options.ApiKey) && !options.TlsExplicitlySet) + { + tls = new Temporalio.Client.TlsOptions(); + } + + var scheme = tls == null ? "http" : "https"; return new Interop.TemporalCoreClientOptions() { target_url = scope.ByteArray($"{scheme}://{options.TargetHost}"), @@ -251,7 +259,7 @@ public static unsafe Interop.TemporalCoreClientOptions ToInteropOptions( api_key = scope.ByteArray(options.ApiKey), identity = scope.ByteArray(options.Identity), tls_options = - options.Tls == null ? null : scope.Pointer(options.Tls.ToInteropOptions(scope)), + tls == null ? null : scope.Pointer(tls.ToInteropOptions(scope)), retry_options = options.RpcRetry == null ? null diff --git a/src/Temporalio/Client/TemporalConnectionOptions.cs b/src/Temporalio/Client/TemporalConnectionOptions.cs index 5ab5016f..5fbdb6d4 100644 --- a/src/Temporalio/Client/TemporalConnectionOptions.cs +++ b/src/Temporalio/Client/TemporalConnectionOptions.cs @@ -10,6 +10,8 @@ namespace Temporalio.Client /// public class TemporalConnectionOptions : ICloneable { + private TlsOptions? tls; + /// /// Initializes a new instance of the class. /// Create default options. @@ -38,9 +40,18 @@ public TemporalConnectionOptions() /// Gets or sets the TLS options for connection. /// /// - /// This must be set, even to a default instance, to do any TLS connection. + /// This must be set, even to a default instance, to do any TLS connection. If not set and + /// is provided, TLS will be automatically enabled with default options. /// - public TlsOptions? Tls { get; set; } + public TlsOptions? Tls + { + get => tls; + set + { + tls = value; + TlsExplicitlySet = true; + } + } /// /// Gets or sets retry options for this connection. @@ -103,6 +114,11 @@ public TemporalConnectionOptions() /// public TemporalRuntime? Runtime { get; set; } + /// + /// Gets a value indicating whether TLS was explicitly set (even to null). + /// + internal bool TlsExplicitlySet { get; private set; } + /// /// Create a shallow copy of these options. /// @@ -113,7 +129,7 @@ public virtual object Clone() var copy = (TemporalConnectionOptions)MemberwiseClone(); if (Tls != null) { - copy.Tls = (TlsOptions)Tls.Clone(); + copy.tls = (TlsOptions)Tls.Clone(); } if (RpcRetry != null) { diff --git a/tests/Temporalio.Tests/Client/TemporalConnectionOptionsTests.cs b/tests/Temporalio.Tests/Client/TemporalConnectionOptionsTests.cs new file mode 100644 index 00000000..f878b372 --- /dev/null +++ b/tests/Temporalio.Tests/Client/TemporalConnectionOptionsTests.cs @@ -0,0 +1,39 @@ +namespace Temporalio.Tests.Client; + +using Temporalio.Bridge; +using Temporalio.Client; +using Xunit; + +public unsafe class TemporalConnectionOptionsTests +{ + [Fact] + public void ToInteropOptions_AutoEnablesTls_WhenApiKeyProvidedAndTlsNotSet() + { + var options = new TemporalConnectionOptions("localhost:7233") + { + ApiKey = "test-api-key", + }; + + using var scope = new Scope(); + var interopOptions = options.ToInteropOptions(scope); + + // TLS should be auto-enabled when API key is provided and TLS not explicitly set + Assert.True(interopOptions.tls_options != null); + } + + [Fact] + public void ToInteropOptions_RespectsExplicitTlsNull_WhenApiKeyProvided() + { + var options = new TemporalConnectionOptions("localhost:7233") + { + ApiKey = "test-api-key", + Tls = null, // Explicitly disable TLS + }; + + using var scope = new Scope(); + var interopOptions = options.ToInteropOptions(scope); + + // TLS should remain disabled when explicitly set to null + Assert.True(interopOptions.tls_options == null); + } +} diff --git a/tests/Temporalio.Tests/Temporalio.Tests.csproj b/tests/Temporalio.Tests/Temporalio.Tests.csproj index b76421cd..96534999 100644 --- a/tests/Temporalio.Tests/Temporalio.Tests.csproj +++ b/tests/Temporalio.Tests/Temporalio.Tests.csproj @@ -1,6 +1,7 @@ + true true false enable From 6b8fe7cf36cd1ce5f740a2db58c02572f14b373b Mon Sep 17 00:00:00 2001 From: Thomas Hardy Date: Tue, 2 Dec 2025 10:39:07 -0800 Subject: [PATCH 2/7] add missing Identity argument --- tests/Temporalio.Tests/Client/TemporalConnectionOptionsTests.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/Temporalio.Tests/Client/TemporalConnectionOptionsTests.cs b/tests/Temporalio.Tests/Client/TemporalConnectionOptionsTests.cs index f878b372..faae3afb 100644 --- a/tests/Temporalio.Tests/Client/TemporalConnectionOptionsTests.cs +++ b/tests/Temporalio.Tests/Client/TemporalConnectionOptionsTests.cs @@ -12,6 +12,7 @@ public void ToInteropOptions_AutoEnablesTls_WhenApiKeyProvidedAndTlsNotSet() var options = new TemporalConnectionOptions("localhost:7233") { ApiKey = "test-api-key", + Identity = "test-identity", }; using var scope = new Scope(); @@ -27,6 +28,7 @@ public void ToInteropOptions_RespectsExplicitTlsNull_WhenApiKeyProvided() var options = new TemporalConnectionOptions("localhost:7233") { ApiKey = "test-api-key", + Identity = "test-identity", Tls = null, // Explicitly disable TLS }; From 30b8aa393794747aa3e88eb1fae88569db67f176 Mon Sep 17 00:00:00 2001 From: Thomas Hardy Date: Tue, 2 Dec 2025 18:12:57 -0800 Subject: [PATCH 3/7] add a couple tests, use notnull assertion --- .../Client/TemporalConnectionOptionsTests.cs | 32 +++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/tests/Temporalio.Tests/Client/TemporalConnectionOptionsTests.cs b/tests/Temporalio.Tests/Client/TemporalConnectionOptionsTests.cs index faae3afb..d763e518 100644 --- a/tests/Temporalio.Tests/Client/TemporalConnectionOptionsTests.cs +++ b/tests/Temporalio.Tests/Client/TemporalConnectionOptionsTests.cs @@ -19,7 +19,7 @@ public void ToInteropOptions_AutoEnablesTls_WhenApiKeyProvidedAndTlsNotSet() var interopOptions = options.ToInteropOptions(scope); // TLS should be auto-enabled when API key is provided and TLS not explicitly set - Assert.True(interopOptions.tls_options != null); + Assert.NotNull(interopOptions.tls_options); } [Fact] @@ -36,6 +36,34 @@ public void ToInteropOptions_RespectsExplicitTlsNull_WhenApiKeyProvided() var interopOptions = options.ToInteropOptions(scope); // TLS should remain disabled when explicitly set to null - Assert.True(interopOptions.tls_options == null); + Assert.NotNull(interopOptions.tls_options); + } + + [Fact] + public void ToInteropOptions_TlsDisabled_WhenNoApiKeyAndTlsNotSet() + { + var options = new TemporalConnectionOptions("localhost:7233"); + + using var scope = new Scope(); + var interopOptions = options.ToInteropOptions(scope); + + // TLS should be disabled when no API key and TLS not set + Assert.Null(interopOptions.tls_options); + } + + [Fact] + public void ToInteropOptions_TlsEnabled_WhenExplicitlySet() + { + var options = new TemporalConnectionOptions("localhost:7233") + { + Identity = "test-identity", + Tls = new TlsOptions(), + }; + + using var scope = new Scope(); + var interopOptions = options.ToInteropOptions(scope); + + // TLS should be enabled when explicitly set + Assert.NotNull(interopOptions.tls_options); } } From 706389c48d3bb949cf3b49bd026b31943d1c6243 Mon Sep 17 00:00:00 2001 From: Thomas Hardy Date: Tue, 2 Dec 2025 18:14:04 -0800 Subject: [PATCH 4/7] whoops, fix one assertion, should be Null, not NotNull --- tests/Temporalio.Tests/Client/TemporalConnectionOptionsTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Temporalio.Tests/Client/TemporalConnectionOptionsTests.cs b/tests/Temporalio.Tests/Client/TemporalConnectionOptionsTests.cs index d763e518..aee52e46 100644 --- a/tests/Temporalio.Tests/Client/TemporalConnectionOptionsTests.cs +++ b/tests/Temporalio.Tests/Client/TemporalConnectionOptionsTests.cs @@ -36,7 +36,7 @@ public void ToInteropOptions_RespectsExplicitTlsNull_WhenApiKeyProvided() var interopOptions = options.ToInteropOptions(scope); // TLS should remain disabled when explicitly set to null - Assert.NotNull(interopOptions.tls_options); + Assert.Null(interopOptions.tls_options); } [Fact] From a713465045ec18c91d2e08604c6ec1b717cd631d Mon Sep 17 00:00:00 2001 From: Thomas Hardy Date: Wed, 3 Dec 2025 10:37:44 -0800 Subject: [PATCH 5/7] pr suggestions --- src/Temporalio/Bridge/OptionsExtensions.cs | 8 +++++-- .../Client/TemporalConnectionOptions.cs | 20 +++--------------- src/Temporalio/Client/TlsOptions.cs | 9 ++++++++ .../Client/TemporalConnectionOptionsTests.cs | 21 +++++++++++-------- 4 files changed, 30 insertions(+), 28 deletions(-) diff --git a/src/Temporalio/Bridge/OptionsExtensions.cs b/src/Temporalio/Bridge/OptionsExtensions.cs index 641be98a..3b9214eb 100644 --- a/src/Temporalio/Bridge/OptionsExtensions.cs +++ b/src/Temporalio/Bridge/OptionsExtensions.cs @@ -242,9 +242,13 @@ public static unsafe Interop.TemporalCoreClientOptions ToInteropOptions( throw new ArgumentException("Identity missing from options."); } - // Auto-enable TLS when API key is provided and TLS is not explicitly set + // Auto-enable TLS when API key is provided and TLS is not explicitly disabled var tls = options.Tls; - if (!string.IsNullOrEmpty(options.ApiKey) && !options.TlsExplicitlySet) + if (tls?.Disabled == true) + { + tls = null; + } + else if (tls == null && !string.IsNullOrEmpty(options.ApiKey)) { tls = new Temporalio.Client.TlsOptions(); } diff --git a/src/Temporalio/Client/TemporalConnectionOptions.cs b/src/Temporalio/Client/TemporalConnectionOptions.cs index 5fbdb6d4..733ec85f 100644 --- a/src/Temporalio/Client/TemporalConnectionOptions.cs +++ b/src/Temporalio/Client/TemporalConnectionOptions.cs @@ -10,8 +10,6 @@ namespace Temporalio.Client /// public class TemporalConnectionOptions : ICloneable { - private TlsOptions? tls; - /// /// Initializes a new instance of the class. /// Create default options. @@ -42,16 +40,9 @@ public TemporalConnectionOptions() /// /// This must be set, even to a default instance, to do any TLS connection. If not set and /// is provided, TLS will be automatically enabled with default options. + /// To explicitly disable TLS, set instance with set to true. /// - public TlsOptions? Tls - { - get => tls; - set - { - tls = value; - TlsExplicitlySet = true; - } - } + public TlsOptions? Tls { get; set; } /// /// Gets or sets retry options for this connection. @@ -114,11 +105,6 @@ public TlsOptions? Tls /// public TemporalRuntime? Runtime { get; set; } - /// - /// Gets a value indicating whether TLS was explicitly set (even to null). - /// - internal bool TlsExplicitlySet { get; private set; } - /// /// Create a shallow copy of these options. /// @@ -129,7 +115,7 @@ public virtual object Clone() var copy = (TemporalConnectionOptions)MemberwiseClone(); if (Tls != null) { - copy.tls = (TlsOptions)Tls.Clone(); + copy.Tls = (TlsOptions)Tls.Clone(); } if (RpcRetry != null) { diff --git a/src/Temporalio/Client/TlsOptions.cs b/src/Temporalio/Client/TlsOptions.cs index 9cc1caf8..38565a65 100644 --- a/src/Temporalio/Client/TlsOptions.cs +++ b/src/Temporalio/Client/TlsOptions.cs @@ -34,6 +34,15 @@ public class TlsOptions : ICloneable /// public byte[]? ClientPrivateKey { get; set; } + /// + /// Gets or sets a value indicating whether TLS should be explicitly disabled. + /// + /// + /// When set to true, TLS will be disabled even when an API key is provided (which normally + /// auto-enables TLS). + /// + public bool Disabled { get; set; } + /// /// Create a shallow copy of these options. /// diff --git a/tests/Temporalio.Tests/Client/TemporalConnectionOptionsTests.cs b/tests/Temporalio.Tests/Client/TemporalConnectionOptionsTests.cs index aee52e46..e5314b64 100644 --- a/tests/Temporalio.Tests/Client/TemporalConnectionOptionsTests.cs +++ b/tests/Temporalio.Tests/Client/TemporalConnectionOptionsTests.cs @@ -18,37 +18,40 @@ public void ToInteropOptions_AutoEnablesTls_WhenApiKeyProvidedAndTlsNotSet() using var scope = new Scope(); var interopOptions = options.ToInteropOptions(scope); - // TLS should be auto-enabled when API key is provided and TLS not explicitly set - Assert.NotNull(interopOptions.tls_options); + // TLS should be auto-enabled when API key is provided and TLS not set + Assert.True(interopOptions.tls_options != null); } [Fact] - public void ToInteropOptions_RespectsExplicitTlsNull_WhenApiKeyProvided() + public void ToInteropOptions_TlsDisabled_WhenExplicitlyDisabledWithApiKey() { var options = new TemporalConnectionOptions("localhost:7233") { ApiKey = "test-api-key", Identity = "test-identity", - Tls = null, // Explicitly disable TLS + Tls = new TlsOptions { Disabled = true }, }; using var scope = new Scope(); var interopOptions = options.ToInteropOptions(scope); - // TLS should remain disabled when explicitly set to null - Assert.Null(interopOptions.tls_options); + // TLS should remain disabled when explicitly disabled, even with API key + Assert.True(interopOptions.tls_options == null); } [Fact] public void ToInteropOptions_TlsDisabled_WhenNoApiKeyAndTlsNotSet() { - var options = new TemporalConnectionOptions("localhost:7233"); + var options = new TemporalConnectionOptions("localhost:7233") + { + Identity = "test-identity", + }; using var scope = new Scope(); var interopOptions = options.ToInteropOptions(scope); // TLS should be disabled when no API key and TLS not set - Assert.Null(interopOptions.tls_options); + Assert.True(interopOptions.tls_options == null); } [Fact] @@ -64,6 +67,6 @@ public void ToInteropOptions_TlsEnabled_WhenExplicitlySet() var interopOptions = options.ToInteropOptions(scope); // TLS should be enabled when explicitly set - Assert.NotNull(interopOptions.tls_options); + Assert.True(interopOptions.tls_options != null); } } From c982d45b60f155de093200d9f11873784bcbba9c Mon Sep 17 00:00:00 2001 From: Thomas Hardy Date: Wed, 3 Dec 2025 11:13:25 -0800 Subject: [PATCH 6/7] update cloud test to use api key instead of mTLS --- .github/workflows/ci.yml | 6 +++--- tests/Temporalio.Tests/WorkflowEnvironment.cs | 8 +++++++- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ae93d03a..65d43862 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -99,13 +99,13 @@ jobs: run: dotnet run --project tests/Temporalio.SimpleBench/Temporalio.SimpleBench.csproj -- --workflow-count 5 --max-cached-workflows 100 --max-concurrent 100 - name: Test cloud - # Only supported in non-fork runs, since secrets are not available in forks + # Only supported in non-fork runs, since secrets are not available in forks. + # Uses API key auth to test auto-TLS feature (TLS auto-enabled when API key provided). if: ${{ matrix.cloudTestTarget && (github.event.pull_request.head.repo.full_name == '' || github.event.pull_request.head.repo.full_name == 'temporalio/sdk-dotnet') }} env: TEMPORAL_TEST_CLIENT_TARGET_HOST: ${{ vars.TEMPORAL_CLIENT_NAMESPACE }}.tmprl.cloud:7233 TEMPORAL_TEST_CLIENT_NAMESPACE: ${{ vars.TEMPORAL_CLIENT_NAMESPACE }} - TEMPORAL_TEST_CLIENT_CERT: ${{ secrets.TEMPORAL_CLIENT_CERT }} - TEMPORAL_TEST_CLIENT_KEY: ${{ secrets.TEMPORAL_CLIENT_KEY }} + TEMPORAL_CLIENT_CLOUD_API_KEY: ${{ secrets.TEMPORAL_CLIENT_CLOUD_API_KEY }} run: dotnet run --project tests/Temporalio.Tests -- -verbose -method "*.ExecuteWorkflowAsync_Simple_Succeeds" - name: Test cloud operations client diff --git a/tests/Temporalio.Tests/WorkflowEnvironment.cs b/tests/Temporalio.Tests/WorkflowEnvironment.cs index 82dced9a..43653835 100644 --- a/tests/Temporalio.Tests/WorkflowEnvironment.cs +++ b/tests/Temporalio.Tests/WorkflowEnvironment.cs @@ -40,7 +40,8 @@ public async Task InitializeAsync() }; var clientCert = Environment.GetEnvironmentVariable("TEMPORAL_TEST_CLIENT_CERT"); var clientKey = Environment.GetEnvironmentVariable("TEMPORAL_TEST_CLIENT_KEY"); - if ((clientCert == null) != (clientKey == null)) + var apiKey = Environment.GetEnvironmentVariable("TEMPORAL_CLIENT_CLOUD_API_KEY"); + if (clientCert == null != (clientKey == null)) { throw new InvalidOperationException("Must have both cert/key or neither"); } @@ -52,6 +53,11 @@ public async Task InitializeAsync() ClientPrivateKey = System.Text.Encoding.ASCII.GetBytes(clientKey), }; } + if (apiKey != null) + { + // API key auto-enables TLS when Tls is not set + options.ApiKey = apiKey; + } env = new(await TemporalClient.ConnectAsync(options)); } else From 7db5704af838b2b14f1d8c23d1c7a0971a19298b Mon Sep 17 00:00:00 2001 From: Thomas Hardy Date: Wed, 3 Dec 2025 13:05:44 -0800 Subject: [PATCH 7/7] split cloud tests to test mTLS and api key (auto tls) --- .github/workflows/ci.yml | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 65d43862..ee799ca0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -98,13 +98,23 @@ jobs: - name: Confirm bench works run: dotnet run --project tests/Temporalio.SimpleBench/Temporalio.SimpleBench.csproj -- --workflow-count 5 --max-cached-workflows 100 --max-concurrent 100 - - name: Test cloud + - name: Test cloud (mTLS) # Only supported in non-fork runs, since secrets are not available in forks. - # Uses API key auth to test auto-TLS feature (TLS auto-enabled when API key provided). if: ${{ matrix.cloudTestTarget && (github.event.pull_request.head.repo.full_name == '' || github.event.pull_request.head.repo.full_name == 'temporalio/sdk-dotnet') }} env: TEMPORAL_TEST_CLIENT_TARGET_HOST: ${{ vars.TEMPORAL_CLIENT_NAMESPACE }}.tmprl.cloud:7233 TEMPORAL_TEST_CLIENT_NAMESPACE: ${{ vars.TEMPORAL_CLIENT_NAMESPACE }} + TEMPORAL_TEST_CLIENT_CERT: ${{ secrets.TEMPORAL_CLIENT_CERT }} + TEMPORAL_TEST_CLIENT_KEY: ${{ secrets.TEMPORAL_CLIENT_KEY }} + run: dotnet run --project tests/Temporalio.Tests -- -verbose -method "*.ExecuteWorkflowAsync_Simple_Succeeds" + + - name: Test cloud (API key) + # Only supported in non-fork runs, since secrets are not available in forks. + # Uses API key auth to test auto-TLS feature (TLS auto-enabled when API key provided). + if: ${{ matrix.cloudTestTarget && (github.event.pull_request.head.repo.full_name == '' || github.event.pull_request.head.repo.full_name == 'temporalio/sdk-dotnet') }} + env: + TEMPORAL_TEST_CLIENT_TARGET_HOST: us-west-2.aws.api.temporal.io:7233 + TEMPORAL_TEST_CLIENT_NAMESPACE: ${{ vars.TEMPORAL_CLIENT_NAMESPACE }} TEMPORAL_CLIENT_CLOUD_API_KEY: ${{ secrets.TEMPORAL_CLIENT_CLOUD_API_KEY }} run: dotnet run --project tests/Temporalio.Tests -- -verbose -method "*.ExecuteWorkflowAsync_Simple_Succeeds"