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 77bf2bd794..52d8b31831 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; @@ -39,13 +40,19 @@ class ActivityClientImpl implements ActivityClient, ActivityClientInternal { ActivityClientImpl(WorkflowServiceStubs stubs, ActivityClientOptions options) { 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 b69327eae6..811fdfee36 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,40 @@ 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(); + + // 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() + .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