From 0f848727c34774cb8a6d9bf77d789fa874a90019 Mon Sep 17 00:00:00 2001 From: Sebastian Bruckner Date: Wed, 14 Oct 2026 12:16:00 +0200 Subject: [PATCH 1/2] Send temporal-namespace header from ActivityClient WorkflowClient, ScheduleClient and NexusClient wrap their service stubs in NamespaceInjectWorkflowServiceStubs so every call carries the temporal-namespace header. ActivityClient did not, so Standalone Activity calls such as StartActivityExecution went out without it, and Temporal Cloud rejects them for API-key authentication with PERMISSION_DENIED: Request unauthorized. Co-Authored-By: Claude Opus 5.5 --- .../temporal/client/ActivityClientImpl.java | 2 ++ .../authorization/AuthorizationTokenTest.java | 34 +++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/temporal-sdk/src/main/java/io/temporal/client/ActivityClientImpl.java b/temporal-sdk/src/main/java/io/temporal/client/ActivityClientImpl.java index 77bf2bd794..f73c80db9e 100644 --- a/temporal-sdk/src/main/java/io/temporal/client/ActivityClientImpl.java +++ b/temporal-sdk/src/main/java/io/temporal/client/ActivityClientImpl.java @@ -8,6 +8,7 @@ import io.temporal.common.interceptors.Header; import io.temporal.internal.client.ActivityClientInternal; import io.temporal.internal.client.ActivityHandleImpl; +import io.temporal.internal.client.NamespaceInjectWorkflowServiceStubs; import io.temporal.internal.client.RootActivityClientInvoker; import io.temporal.internal.client.external.GenericWorkflowClientImpl; import io.temporal.internal.client.external.ManualActivityCompletionClientFactory; @@ -37,6 +38,7 @@ class ActivityClientImpl implements ActivityClient, ActivityClientInternal { private final Scope metricsScope; ActivityClientImpl(WorkflowServiceStubs stubs, ActivityClientOptions options) { + stubs = new NamespaceInjectWorkflowServiceStubs(stubs, options.getNamespace()); this.stubs = stubs; this.options = options; this.metricsScope = diff --git a/temporal-sdk/src/test/java/io/temporal/authorization/AuthorizationTokenTest.java b/temporal-sdk/src/test/java/io/temporal/authorization/AuthorizationTokenTest.java index b69327eae6..85adeea53c 100644 --- a/temporal-sdk/src/test/java/io/temporal/authorization/AuthorizationTokenTest.java +++ b/temporal-sdk/src/test/java/io/temporal/authorization/AuthorizationTokenTest.java @@ -3,6 +3,8 @@ import static org.junit.Assert.*; import io.grpc.*; +import io.temporal.client.ActivityClient; +import io.temporal.client.StartActivityOptions; import io.temporal.client.WorkflowClient; import io.temporal.client.WorkflowOptions; import io.temporal.serviceclient.WorkflowServiceStubsOptions; @@ -18,6 +20,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.stream.Collectors; import org.junit.After; import org.junit.Before; import org.junit.Rule; @@ -125,6 +128,37 @@ public void allRequestsShouldHaveAnAuthToken() { } } + @Test + public void activityClientRequestsShouldHaveANamespace() { + testEnvironment.start(); + ActivityClient client = testEnvironment.getActivityClient(); + StartActivityOptions options = + StartActivityOptions.newBuilder() + .setId("activity-id") + .setTaskQueue(TASK_QUEUE) + .setStartToCloseTimeout(Duration.ofMinutes(1)) + .build(); + + // The test server does not implement StartActivityExecution; only the outgoing headers matter. + assertThrows(StatusRuntimeException.class, () -> client.start("SomeActivity", options)); + + List startRequests = + loggedRequests.stream() + .filter(request -> request.methodName.equals("StartActivityExecution")) + .collect(Collectors.toList()); + assertFalse("StartActivityExecution should have been called", startRequests.isEmpty()); + for (GrpcRequest grpcRequest : startRequests) { + assertEquals( + "StartActivityExecution should have an auth token", + AUTH_TOKEN, + grpcRequest.authTokenValue); + assertEquals( + "StartActivityExecution should have a namespace", + testEnvironment.getNamespace(), + grpcRequest.namespace); + } + } + public static class EmptyWorkflowImpl implements TestWorkflows.TestWorkflow1 { @Override From 740dadadde394ec16fb61f975cac2a86d80498f1 Mon Sep 17 00:00:00 2001 From: Sebastian Bruckner Date: Wed, 14 Oct 2026 12:16:01 +0200 Subject: [PATCH 2/2] Keep caller's stubs on ActivityClientImpl, add changelog entry TestWorkflowEnvironmentActivityClientTest asserts the client holds the exact WorkflowServiceStubs it was given, so only wrap them for outgoing calls. The authorization test no longer assumes the server rejects StartActivityExecution. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 3 +++ .../java/io/temporal/client/ActivityClientImpl.java | 11 ++++++++--- .../authorization/AuthorizationTokenTest.java | 7 +++++-- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5474179818..ba9acc9c8c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,6 +41,9 @@ to docs, or any other relevant information. specific to the Temporal CLI dev server implementation and may no longer be valid if that implementation changes. ### Fixed +- `ActivityClient` now sends the `temporal-namespace` gRPC header like `WorkflowClient` and `ScheduleClient` do, + so standalone activity calls are no longer rejected with `PERMISSION_DENIED` by namespace-scoped auth (e.g. + Temporal Cloud API keys). - Kotlin `Async` method references compiled with Kotlin 2.4 now retain their inline behavior, preventing `NonDeterministicException` when replaying workflows started with Kotlin 2.2. For workflows already started with Kotlin 2.4 and SDK 1.40 or earlier, set the diff --git a/temporal-sdk/src/main/java/io/temporal/client/ActivityClientImpl.java b/temporal-sdk/src/main/java/io/temporal/client/ActivityClientImpl.java index f73c80db9e..52d8b31831 100644 --- a/temporal-sdk/src/main/java/io/temporal/client/ActivityClientImpl.java +++ b/temporal-sdk/src/main/java/io/temporal/client/ActivityClientImpl.java @@ -38,16 +38,21 @@ class ActivityClientImpl implements ActivityClient, ActivityClientInternal { private final Scope metricsScope; ActivityClientImpl(WorkflowServiceStubs stubs, ActivityClientOptions options) { - stubs = new NamespaceInjectWorkflowServiceStubs(stubs, options.getNamespace()); this.stubs = stubs; this.options = options; + WorkflowServiceStubs namespacedStubs = + new NamespaceInjectWorkflowServiceStubs(stubs, options.getNamespace()); this.metricsScope = stubs.getOptions().getMetricsScope().tagged(MetricsTag.defaultTags(options.getNamespace())); - GenericWorkflowClientImpl genericClient = new GenericWorkflowClientImpl(stubs, metricsScope); + GenericWorkflowClientImpl genericClient = + new GenericWorkflowClientImpl(namespacedStubs, metricsScope); this.invoker = initializeClientInvoker(genericClient, options); this.manualActivityCompletionClientFactory = ManualActivityCompletionClientFactory.newFactory( - stubs, options.getNamespace(), options.getIdentity(), options.getDataConverter()); + namespacedStubs, + options.getNamespace(), + options.getIdentity(), + options.getDataConverter()); } private static ActivityClientCallsInterceptor initializeClientInvoker( diff --git a/temporal-sdk/src/test/java/io/temporal/authorization/AuthorizationTokenTest.java b/temporal-sdk/src/test/java/io/temporal/authorization/AuthorizationTokenTest.java index 85adeea53c..811fdfee36 100644 --- a/temporal-sdk/src/test/java/io/temporal/authorization/AuthorizationTokenTest.java +++ b/temporal-sdk/src/test/java/io/temporal/authorization/AuthorizationTokenTest.java @@ -139,8 +139,11 @@ public void activityClientRequestsShouldHaveANamespace() { .setStartToCloseTimeout(Duration.ofMinutes(1)) .build(); - // The test server does not implement StartActivityExecution; only the outgoing headers matter. - assertThrows(StatusRuntimeException.class, () -> client.start("SomeActivity", options)); + // Only the outgoing request headers matter, not whether the server supports the call. + try { + client.start("SomeActivity", options); + } catch (StatusRuntimeException ignored) { + } List startRequests = loggedRequests.stream()