diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ae93d03a..ee799ca0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -98,8 +98,8 @@ 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 - # Only supported in non-fork runs, since secrets are not available in forks + - name: Test cloud (mTLS) + # Only supported in non-fork runs, since secrets are not available in forks. 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 @@ -108,6 +108,16 @@ jobs: 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" + - name: Test cloud operations client # Only supported in non-fork runs, since secrets are not available in forks if: ${{ matrix.cloudTestTarget && (github.event.pull_request.head.repo.full_name == '' || github.event.pull_request.head.repo.full_name == 'temporalio/sdk-dotnet') }} diff --git a/src/Temporalio/Bridge/OptionsExtensions.cs b/src/Temporalio/Bridge/OptionsExtensions.cs index 2ad8cd19..3b9214eb 100644 --- a/src/Temporalio/Bridge/OptionsExtensions.cs +++ b/src/Temporalio/Bridge/OptionsExtensions.cs @@ -241,7 +241,19 @@ 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 disabled + var tls = options.Tls; + if (tls?.Disabled == true) + { + tls = null; + } + else if (tls == null && !string.IsNullOrEmpty(options.ApiKey)) + { + tls = new Temporalio.Client.TlsOptions(); + } + + var scheme = tls == null ? "http" : "https"; return new Interop.TemporalCoreClientOptions() { target_url = scope.ByteArray($"{scheme}://{options.TargetHost}"), @@ -251,7 +263,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..733ec85f 100644 --- a/src/Temporalio/Client/TemporalConnectionOptions.cs +++ b/src/Temporalio/Client/TemporalConnectionOptions.cs @@ -38,7 +38,9 @@ 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. + /// To explicitly disable TLS, set instance with set to true. /// public TlsOptions? Tls { get; set; } 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 new file mode 100644 index 00000000..e5314b64 --- /dev/null +++ b/tests/Temporalio.Tests/Client/TemporalConnectionOptionsTests.cs @@ -0,0 +1,72 @@ +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", + Identity = "test-identity", + }; + + using var scope = new Scope(); + var interopOptions = options.ToInteropOptions(scope); + + // TLS should be auto-enabled when API key is provided and TLS not set + Assert.True(interopOptions.tls_options != null); + } + + [Fact] + public void ToInteropOptions_TlsDisabled_WhenExplicitlyDisabledWithApiKey() + { + var options = new TemporalConnectionOptions("localhost:7233") + { + ApiKey = "test-api-key", + Identity = "test-identity", + Tls = new TlsOptions { Disabled = true }, + }; + + using var scope = new Scope(); + var interopOptions = options.ToInteropOptions(scope); + + // 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") + { + 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.True(interopOptions.tls_options == null); + } + + [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.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 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