From 5c7fc54832b87a57ca32912955c520a2c7b8857e Mon Sep 17 00:00:00 2001 From: Filinto Duran <1373693+filintod@users.noreply.github.com> Date: Thu, 23 Oct 2025 17:01:30 -0500 Subject: [PATCH 01/14] allow to create clients with different dapr-api-token, fallback to original env var DAPR_API_TOKEN if found Signed-off-by: Filinto Duran <1373693+filintod@users.noreply.github.com> --- dapr/aio/clients/__init__.py | 7 +- dapr/aio/clients/grpc/client.py | 14 +- dapr/clients/__init__.py | 7 +- dapr/clients/grpc/client.py | 14 +- dapr/clients/health.py | 13 +- dapr/clients/http/client.py | 14 +- dapr/clients/http/dapr_actor_http_client.py | 5 +- .../http/dapr_invocation_http_client.py | 5 +- tests/clients/test_dapr_grpc_client.py | 34 +++ tests/clients/test_dapr_grpc_client_async.py | 34 +++ tests/clients/test_heatlhcheck.py | 18 ++ tests/clients/test_multi_token_clients.py | 232 ++++++++++++++++++ 12 files changed, 376 insertions(+), 21 deletions(-) create mode 100644 tests/clients/test_multi_token_clients.py diff --git a/dapr/aio/clients/__init__.py b/dapr/aio/clients/__init__.py index e945b1307..a07dc4718 100644 --- a/dapr/aio/clients/__init__.py +++ b/dapr/aio/clients/__init__.py @@ -66,6 +66,7 @@ def __init__( ] = None, http_timeout_seconds: Optional[int] = None, max_grpc_message_length: Optional[int] = None, + api_token: Optional[str] = None, ): """Connects to Dapr Runtime and via gRPC and HTTP. @@ -79,8 +80,10 @@ def __init__( http_timeout_seconds (int): specify a timeout for http connections max_grpc_messsage_length (int, optional): The maximum grpc send and receive message length in bytes. + api_token (str, optional): Dapr API token for authentication. If not provided, + falls back to DAPR_API_TOKEN environment variable. """ - super().__init__(address, interceptors, max_grpc_message_length) + super().__init__(address, interceptors, max_grpc_message_length, api_token=api_token) self.invocation_client = None invocation_protocol = settings.DAPR_API_METHOD_INVOCATION_PROTOCOL.upper() @@ -89,7 +92,7 @@ def __init__( if http_timeout_seconds is None: http_timeout_seconds = settings.DAPR_HTTP_TIMEOUT_SECONDS self.invocation_client = DaprInvocationHttpClient( - headers_callback=headers_callback, timeout=http_timeout_seconds + headers_callback=headers_callback, timeout=http_timeout_seconds, api_token=api_token ) elif invocation_protocol == 'GRPC': pass diff --git a/dapr/aio/clients/grpc/client.py b/dapr/aio/clients/grpc/client.py index 995b82680..58b64ffb6 100644 --- a/dapr/aio/clients/grpc/client.py +++ b/dapr/aio/clients/grpc/client.py @@ -141,6 +141,7 @@ def __init__( ] = None, max_grpc_message_length: Optional[int] = None, retry_policy: Optional[RetryPolicy] = None, + api_token: Optional[str] = None, ): """Connects to Dapr Runtime and initialize gRPC client stub. @@ -152,8 +153,13 @@ def __init__( StreamStreamClientInterceptor, optional): gRPC interceptors. max_grpc_message_length (int, optional): The maximum grpc send and receive message length in bytes. + api_token (str, optional): Dapr API token for authentication. If not provided, + falls back to DAPR_API_TOKEN environment variable. """ - DaprHealth.wait_until_ready() + self._api_token = api_token + # For health check, use explicit token or fall back to global setting + health_token = api_token if api_token is not None else settings.DAPR_API_TOKEN + DaprHealth.wait_until_ready(api_token=health_token) self.retry_policy = retry_policy or RetryPolicy() useragent = f'dapr-sdk-python/{__version__}' @@ -184,10 +190,12 @@ def __init__( else: interceptors.append(DaprClientTimeoutInterceptorAsync()) - if settings.DAPR_API_TOKEN: + # Use explicit token if provided, otherwise fall back to global setting + token = self._api_token if self._api_token is not None else settings.DAPR_API_TOKEN + if token: api_token_interceptor = DaprClientInterceptorAsync( [ - ('dapr-api-token', settings.DAPR_API_TOKEN), + ('dapr-api-token', token), ] ) interceptors.append(api_token_interceptor) diff --git a/dapr/clients/__init__.py b/dapr/clients/__init__.py index 78ad99eb4..bcbd8627a 100644 --- a/dapr/clients/__init__.py +++ b/dapr/clients/__init__.py @@ -71,6 +71,7 @@ def __init__( http_timeout_seconds: Optional[int] = None, max_grpc_message_length: Optional[int] = None, retry_policy: Optional[RetryPolicy] = None, + api_token: Optional[str] = None, ): """Connects to Dapr Runtime via gRPC and HTTP. @@ -84,8 +85,10 @@ def __init__( http_timeout_seconds (int): specify a timeout for http connections max_grpc_message_length (int, optional): The maximum grpc send and receive message length in bytes. + api_token (str, optional): Dapr API token for authentication. If not provided, + falls back to DAPR_API_TOKEN environment variable. """ - super().__init__(address, interceptors, max_grpc_message_length, retry_policy) + super().__init__(address, interceptors, max_grpc_message_length, retry_policy, api_token) self.invocation_client = None invocation_protocol = settings.DAPR_API_METHOD_INVOCATION_PROTOCOL.upper() @@ -94,7 +97,7 @@ def __init__( if http_timeout_seconds is None: http_timeout_seconds = settings.DAPR_HTTP_TIMEOUT_SECONDS self.invocation_client = DaprInvocationHttpClient( - headers_callback=headers_callback, timeout=http_timeout_seconds + headers_callback=headers_callback, timeout=http_timeout_seconds, api_token=api_token ) elif invocation_protocol == 'GRPC': pass diff --git a/dapr/clients/grpc/client.py b/dapr/clients/grpc/client.py index e4ffb2646..0f5b064e1 100644 --- a/dapr/clients/grpc/client.py +++ b/dapr/clients/grpc/client.py @@ -132,6 +132,7 @@ def __init__( ] = None, max_grpc_message_length: Optional[int] = None, retry_policy: Optional[RetryPolicy] = None, + api_token: Optional[str] = None, ): """Connects to Dapr Runtime and initializes gRPC client stub. @@ -144,8 +145,13 @@ def __init__( max_grpc_message_length (int, optional): The maximum grpc send and receive message length in bytes. retry_policy (RetryPolicy optional): Specifies retry behaviour + api_token (str, optional): Dapr API token for authentication. If not provided, + falls back to DAPR_API_TOKEN environment variable. """ - DaprHealth.wait_until_ready() + self._api_token = api_token + # For health check, use explicit token or fall back to global setting + health_token = api_token if api_token is not None else settings.DAPR_API_TOKEN + DaprHealth.wait_until_ready(api_token=health_token) self.retry_policy = retry_policy or RetryPolicy() useragent = f'dapr-sdk-python/{__version__}' @@ -184,10 +190,12 @@ def __init__( self._channel = grpc.intercept_channel(self._channel, DaprClientTimeoutInterceptor()) # type: ignore - if settings.DAPR_API_TOKEN: + # Use explicit token if provided, otherwise fall back to global setting + token = self._api_token if self._api_token is not None else settings.DAPR_API_TOKEN + if token: api_token_interceptor = DaprClientInterceptor( [ - ('dapr-api-token', settings.DAPR_API_TOKEN), + ('dapr-api-token', token), ] ) self._channel = grpc.intercept_channel( # type: ignore diff --git a/dapr/clients/health.py b/dapr/clients/health.py index e3daec79d..b61e2ab14 100644 --- a/dapr/clients/health.py +++ b/dapr/clients/health.py @@ -12,22 +12,23 @@ See the License for the specific language governing permissions and limitations under the License. """ -import urllib.request -import urllib.error import time +import urllib.error +import urllib.request -from dapr.clients.http.conf import DAPR_API_TOKEN_HEADER, USER_AGENT_HEADER, DAPR_USER_AGENT +from dapr.clients.http.conf import DAPR_API_TOKEN_HEADER, DAPR_USER_AGENT, USER_AGENT_HEADER from dapr.clients.http.helpers import get_api_url from dapr.conf import settings class DaprHealth: @staticmethod - def wait_until_ready(): + def wait_until_ready(api_token: str = None): health_url = f'{get_api_url()}/healthz/outbound' headers = {USER_AGENT_HEADER: DAPR_USER_AGENT} - if settings.DAPR_API_TOKEN is not None: - headers[DAPR_API_TOKEN_HEADER] = settings.DAPR_API_TOKEN + token = api_token if api_token is not None else settings.DAPR_API_TOKEN + if token is not None: + headers[DAPR_API_TOKEN_HEADER] = token timeout = float(settings.DAPR_HEALTH_TIMEOUT) start = time.time() diff --git a/dapr/clients/http/client.py b/dapr/clients/http/client.py index 86e9ab6f0..4769b932e 100644 --- a/dapr/clients/http/client.py +++ b/dapr/clients/http/client.py @@ -43,6 +43,7 @@ def __init__( timeout: Optional[int] = 60, headers_callback: Optional[Callable[[], Dict[str, str]]] = None, retry_policy: Optional[RetryPolicy] = None, + api_token: Optional[str] = None, ): """Invokes Dapr over HTTP. @@ -50,8 +51,13 @@ def __init__( message_serializer (Serializer): Dapr serializer. timeout (int, optional): Timeout in seconds, defaults to 60. headers_callback (lambda: Dict[str, str]], optional): Generates header for each request. + api_token (str, optional): Dapr API token for authentication. If not provided, + falls back to DAPR_API_TOKEN environment variable. """ - DaprHealth.wait_until_ready() + self._api_token = api_token + # For health check, use explicit token or fall back to global setting + health_token = api_token if api_token is not None else settings.DAPR_API_TOKEN + DaprHealth.wait_until_ready(api_token=health_token) self._timeout = aiohttp.ClientTimeout(total=timeout) self._serializer = message_serializer @@ -71,8 +77,10 @@ async def send_bytes( if not headers_map.get(CONTENT_TYPE_HEADER): headers_map[CONTENT_TYPE_HEADER] = DEFAULT_JSON_CONTENT_TYPE - if settings.DAPR_API_TOKEN is not None: - headers_map[DAPR_API_TOKEN_HEADER] = settings.DAPR_API_TOKEN + # Use explicit token if provided, otherwise fall back to global setting + token = self._api_token if self._api_token is not None else settings.DAPR_API_TOKEN + if token is not None: + headers_map[DAPR_API_TOKEN_HEADER] = token if self._headers_callback is not None: trace_headers = self._headers_callback() diff --git a/dapr/clients/http/dapr_actor_http_client.py b/dapr/clients/http/dapr_actor_http_client.py index 186fdbc1c..cfc3ff44b 100644 --- a/dapr/clients/http/dapr_actor_http_client.py +++ b/dapr/clients/http/dapr_actor_http_client.py @@ -36,6 +36,7 @@ def __init__( timeout: int = 60, headers_callback: Optional[Callable[[], Dict[str, str]]] = None, retry_policy: Optional[RetryPolicy] = None, + api_token: Optional[str] = None, ): """Invokes Dapr Actors over HTTP. @@ -44,8 +45,10 @@ def __init__( timeout (int, optional): Timeout in seconds, defaults to 60. headers_callback (lambda: Dict[str, str]], optional): Generates header for each request. retry_policy (RetryPolicy optional): Specifies retry behaviour + api_token (str, optional): Dapr API token for authentication. If not provided, + falls back to DAPR_API_TOKEN environment variable. """ - self._client = DaprHttpClient(message_serializer, timeout, headers_callback, retry_policy) + self._client = DaprHttpClient(message_serializer, timeout, headers_callback, retry_policy, api_token=api_token) async def invoke_method( self, actor_type: str, actor_id: str, method: str, data: Optional[bytes] = None diff --git a/dapr/clients/http/dapr_invocation_http_client.py b/dapr/clients/http/dapr_invocation_http_client.py index df4e6d222..b816fabd9 100644 --- a/dapr/clients/http/dapr_invocation_http_client.py +++ b/dapr/clients/http/dapr_invocation_http_client.py @@ -39,6 +39,7 @@ def __init__( timeout: int = 60, headers_callback: Optional[Callable[[], Dict[str, str]]] = None, retry_policy: Optional[RetryPolicy] = None, + api_token: Optional[str] = None, ): """Invokes Dapr's API for method invocation over HTTP. @@ -46,9 +47,11 @@ def __init__( timeout (int, optional): Timeout in seconds, defaults to 60. headers_callback (lambda: Dict[str, str]], optional): Generates header for each request. retry_policy (RetryPolicy optional): Specifies retry behaviour + api_token (str, optional): Dapr API token for authentication. If not provided, + falls back to DAPR_API_TOKEN environment variable. """ self._client = DaprHttpClient( - DefaultJSONSerializer(), timeout, headers_callback, retry_policy=retry_policy + DefaultJSONSerializer(), timeout, headers_callback, retry_policy=retry_policy, api_token=api_token ) async def invoke_method_async( diff --git a/tests/clients/test_dapr_grpc_client.py b/tests/clients/test_dapr_grpc_client.py index e0713f703..61abf639d 100644 --- a/tests/clients/test_dapr_grpc_client.py +++ b/tests/clients/test_dapr_grpc_client.py @@ -413,6 +413,40 @@ def test_dapr_api_token_insertion(self): self.assertEqual(['value1'], resp.headers['hkey1']) self.assertEqual(['test-token'], resp.headers['hdapr-api-token']) + def test_explicit_api_token(self): + """Test that explicit api_token parameter is used in client""" + dapr = DaprGrpcClient( + f'{self.scheme}localhost:{self.grpc_port}', api_token='explicit-token' + ) + resp = dapr.invoke_method( + app_id='targetId', + method_name='bytes', + data=b'test', + content_type='text/plain', + metadata=(('key1', 'value1'),), + ) + + self.assertEqual(b'test', resp.data) + self.assertEqual('text/plain', resp.content_type) + self.assertEqual(['explicit-token'], resp.headers['hdapr-api-token']) + + @patch.object(settings, 'DAPR_API_TOKEN', 'global-token') + def test_explicit_api_token_overrides_global(self): + """Test that explicit api_token parameter overrides global setting""" + dapr = DaprGrpcClient( + f'{self.scheme}localhost:{self.grpc_port}', api_token='override-token' + ) + resp = dapr.invoke_method( + app_id='targetId', + method_name='bytes', + data=b'test', + content_type='text/plain', + ) + + self.assertEqual(b'test', resp.data) + # Should use explicit token, not global + self.assertEqual(['override-token'], resp.headers['hdapr-api-token']) + def test_get_save_delete_state(self): dapr = DaprGrpcClient(f'{self.scheme}localhost:{self.grpc_port}') key = 'key_1' diff --git a/tests/clients/test_dapr_grpc_client_async.py b/tests/clients/test_dapr_grpc_client_async.py index 50043912d..6ba30f1b5 100644 --- a/tests/clients/test_dapr_grpc_client_async.py +++ b/tests/clients/test_dapr_grpc_client_async.py @@ -406,6 +406,40 @@ async def test_dapr_api_token_insertion(self): self.assertEqual(['value1'], resp.headers['hkey1']) self.assertEqual(['test-token'], resp.headers['hdapr-api-token']) + async def test_explicit_api_token(self): + """Test that explicit api_token parameter is used in client""" + dapr = DaprGrpcClientAsync( + f'{self.scheme}localhost:{self.grpc_port}', api_token='explicit-token' + ) + resp = await dapr.invoke_method( + app_id='targetId', + method_name='bytes', + data=b'test', + content_type='text/plain', + metadata=(('key1', 'value1'),), + ) + + self.assertEqual(b'test', resp.data) + self.assertEqual('text/plain', resp.content_type) + self.assertEqual(['explicit-token'], resp.headers['hdapr-api-token']) + + @patch.object(settings, 'DAPR_API_TOKEN', 'global-token') + async def test_explicit_api_token_overrides_global(self): + """Test that explicit api_token parameter overrides global setting""" + dapr = DaprGrpcClientAsync( + f'{self.scheme}localhost:{self.grpc_port}', api_token='override-token' + ) + resp = await dapr.invoke_method( + app_id='targetId', + method_name='bytes', + data=b'test', + content_type='text/plain', + ) + + self.assertEqual(b'test', resp.data) + # Should use explicit token, not global + self.assertEqual(['override-token'], resp.headers['hdapr-api-token']) + async def test_get_save_delete_state(self): dapr = DaprGrpcClientAsync(f'{self.scheme}localhost:{self.grpc_port}') key = 'key_1' diff --git a/tests/clients/test_heatlhcheck.py b/tests/clients/test_heatlhcheck.py index f3be8a475..4799cff96 100644 --- a/tests/clients/test_heatlhcheck.py +++ b/tests/clients/test_heatlhcheck.py @@ -62,6 +62,24 @@ def test_wait_until_ready_success_with_api_token(self, mock_urlopen): self.assertIn('Dapr-api-token', headers) self.assertEqual(headers['Dapr-api-token'], 'mytoken') + @patch.object(settings, 'DAPR_HTTP_ENDPOINT', 'http://domain.com:3500') + @patch('urllib.request.urlopen') + def test_wait_until_ready_with_explicit_token(self, mock_urlopen): + """Test that explicit api_token parameter overrides global setting""" + mock_urlopen.return_value.__enter__.return_value = MagicMock(status=200) + + try: + DaprHealth.wait_until_ready(api_token='explicit-token') + except Exception as e: + self.fail(f'wait_until_ready() raised an exception unexpectedly: {e}') + + mock_urlopen.assert_called_once() + + # Check headers are properly set + headers = mock_urlopen.call_args[0][0].headers + self.assertIn('Dapr-api-token', headers) + self.assertEqual(headers['Dapr-api-token'], 'explicit-token') + @patch.object(settings, 'DAPR_HEALTH_TIMEOUT', '2.5') @patch('urllib.request.urlopen') def test_wait_until_ready_timeout(self, mock_urlopen): diff --git a/tests/clients/test_multi_token_clients.py b/tests/clients/test_multi_token_clients.py new file mode 100644 index 000000000..f86e38620 --- /dev/null +++ b/tests/clients/test_multi_token_clients.py @@ -0,0 +1,232 @@ +# -*- coding: utf-8 -*- + +""" +Copyright 2024 The Dapr Authors +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +import unittest +from unittest.mock import patch + +from dapr.aio.clients.grpc.client import DaprGrpcClientAsync +from dapr.clients.grpc.client import DaprGrpcClient +from dapr.clients.health import DaprHealth +from dapr.conf import settings + +from .fake_dapr_server import FakeDaprSidecar + + +class MultiTokenClientTests(unittest.TestCase): + """Integration tests for multiple clients with different API tokens""" + + grpc_port_1 = 50011 + grpc_port_2 = 50012 + http_port_1 = 3501 + http_port_2 = 3502 + + @classmethod + def setUpClass(cls): + """Set up two fake Dapr sidecars to simulate different instances""" + cls._fake_dapr_server_1 = FakeDaprSidecar( + grpc_port=cls.grpc_port_1, http_port=cls.http_port_1 + ) + cls._fake_dapr_server_2 = FakeDaprSidecar( + grpc_port=cls.grpc_port_2, http_port=cls.http_port_2 + ) + cls._fake_dapr_server_1.start() + cls._fake_dapr_server_2.start() + + # Set default HTTP endpoint to first server for health checks + settings.DAPR_HTTP_PORT = cls.http_port_1 + settings.DAPR_HTTP_ENDPOINT = f'http://127.0.0.1:{cls.http_port_1}' + + @classmethod + def tearDownClass(cls): + """Clean up fake servers""" + cls._fake_dapr_server_1.stop() + cls._fake_dapr_server_2.stop() + + @patch.object(DaprHealth, 'wait_until_ready') + def test_multiple_sync_clients_different_tokens(self, mock_health): + """Test that multiple synchronous clients can use different tokens""" + # Mock health check to avoid connection issues + mock_health.return_value = None + + # Create two clients with different tokens + client1 = DaprGrpcClient(f'localhost:{self.grpc_port_1}', api_token='token-client-1') + client2 = DaprGrpcClient(f'localhost:{self.grpc_port_2}', api_token='token-client-2') + + try: + # Make requests with both clients + resp1 = client1.invoke_method( + app_id='app1', + method_name='test', + data=b'client1', + content_type='text/plain', + ) + + resp2 = client2.invoke_method( + app_id='app2', + method_name='test', + data=b'client2', + content_type='text/plain', + ) + + # Verify each client used its own token + self.assertEqual(b'client1', resp1.data) + self.assertEqual(['token-client-1'], resp1.headers['hdapr-api-token']) + + self.assertEqual(b'client2', resp2.data) + self.assertEqual(['token-client-2'], resp2.headers['hdapr-api-token']) + + finally: + client1.close() + client2.close() + + @patch.object(DaprHealth, 'wait_until_ready') + async def test_multiple_async_clients_different_tokens(self, mock_health): + """Test that multiple async clients can use different tokens""" + # Mock health check to avoid connection issues + mock_health.return_value = None + + # Create two async clients with different tokens + client1 = DaprGrpcClientAsync(f'localhost:{self.grpc_port_1}', api_token='async-token-1') + client2 = DaprGrpcClientAsync(f'localhost:{self.grpc_port_2}', api_token='async-token-2') + + try: + # Make requests with both clients + resp1 = await client1.invoke_method( + app_id='app1', + method_name='test', + data=b'async-client1', + content_type='text/plain', + ) + + resp2 = await client2.invoke_method( + app_id='app2', + method_name='test', + data=b'async-client2', + content_type='text/plain', + ) + + # Verify each client used its own token + self.assertEqual(b'async-client1', resp1.data) + self.assertEqual(['async-token-1'], resp1.headers['hdapr-api-token']) + + self.assertEqual(b'async-client2', resp2.data) + self.assertEqual(['async-token-2'], resp2.headers['hdapr-api-token']) + + finally: + await client1.close() + await client2.close() + + @patch.object(DaprHealth, 'wait_until_ready') + @patch.object(settings, 'DAPR_API_TOKEN', 'global-default-token') + def test_mixing_explicit_and_global_tokens(self, mock_health): + """Test that clients with explicit tokens coexist with clients using global token""" + # Mock health check to avoid connection issues + mock_health.return_value = None + + # Client with explicit token + client_explicit = DaprGrpcClient( + f'localhost:{self.grpc_port_1}', api_token='explicit-token' + ) + # Client using global token + client_global = DaprGrpcClient(f'localhost:{self.grpc_port_2}') + + try: + resp_explicit = client_explicit.invoke_method( + app_id='app1', + method_name='test', + data=b'explicit', + content_type='text/plain', + ) + + resp_global = client_global.invoke_method( + app_id='app2', + method_name='test', + data=b'global', + content_type='text/plain', + ) + + # Verify explicit token client used its token + self.assertEqual(b'explicit', resp_explicit.data) + self.assertEqual(['explicit-token'], resp_explicit.headers['hdapr-api-token']) + + # Verify global token client used the global setting + self.assertEqual(b'global', resp_global.data) + self.assertEqual(['global-default-token'], resp_global.headers['hdapr-api-token']) + + finally: + client_explicit.close() + client_global.close() + + @patch.object(DaprHealth, 'wait_until_ready') + def test_client_isolation(self, mock_health): + """Test that modifying one client's token doesn't affect another""" + # Mock health check to avoid connection issues + mock_health.return_value = None + + # Create two clients with different tokens + client1 = DaprGrpcClient(f'localhost:{self.grpc_port_1}', api_token='isolated-token-1') + client2 = DaprGrpcClient(f'localhost:{self.grpc_port_2}', api_token='isolated-token-2') + + try: + # Make a request with client1 + resp1 = client1.invoke_method( + app_id='app1', method_name='test', data=b'test1', content_type='text/plain' + ) + + # Make a request with client2 + resp2 = client2.invoke_method( + app_id='app2', method_name='test', data=b'test2', content_type='text/plain' + ) + + # Make another request with client1 to verify it still uses its token + resp1_again = client1.invoke_method( + app_id='app1', method_name='test', data=b'test1_again', content_type='text/plain' + ) + + # Verify tokens are isolated + self.assertEqual(['isolated-token-1'], resp1.headers['hdapr-api-token']) + self.assertEqual(['isolated-token-2'], resp2.headers['hdapr-api-token']) + self.assertEqual(['isolated-token-1'], resp1_again.headers['hdapr-api-token']) + + finally: + client1.close() + client2.close() + + @patch.object(DaprHealth, 'wait_until_ready') + @patch.object(settings, 'DAPR_API_TOKEN', None) + def test_no_token_clients(self, mock_health): + """Test that clients without tokens work when no global token is set""" + # Mock health check to avoid connection issues + mock_health.return_value = None + + client = DaprGrpcClient(f'localhost:{self.grpc_port_1}') + + try: + resp = client.invoke_method( + app_id='app1', + method_name='test', + data=b'no-token', + content_type='text/plain', + ) + + # Verify no token was sent + self.assertNotIn('hdapr-api-token', resp.headers) + + finally: + client.close() + + +if __name__ == '__main__': + unittest.main() From 43b0b682181febd0f406f89c65e2c274353694db Mon Sep 17 00:00:00 2001 From: Filinto Duran <1373693+filintod@users.noreply.github.com> Date: Thu, 23 Oct 2025 17:25:39 -0500 Subject: [PATCH 02/14] ruff Signed-off-by: Filinto Duran <1373693+filintod@users.noreply.github.com> --- dapr/clients/http/dapr_actor_http_client.py | 4 +++- dapr/clients/http/dapr_invocation_http_client.py | 6 +++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/dapr/clients/http/dapr_actor_http_client.py b/dapr/clients/http/dapr_actor_http_client.py index cfc3ff44b..ac63672bc 100644 --- a/dapr/clients/http/dapr_actor_http_client.py +++ b/dapr/clients/http/dapr_actor_http_client.py @@ -48,7 +48,9 @@ def __init__( api_token (str, optional): Dapr API token for authentication. If not provided, falls back to DAPR_API_TOKEN environment variable. """ - self._client = DaprHttpClient(message_serializer, timeout, headers_callback, retry_policy, api_token=api_token) + self._client = DaprHttpClient( + message_serializer, timeout, headers_callback, retry_policy, api_token=api_token + ) async def invoke_method( self, actor_type: str, actor_id: str, method: str, data: Optional[bytes] = None diff --git a/dapr/clients/http/dapr_invocation_http_client.py b/dapr/clients/http/dapr_invocation_http_client.py index b816fabd9..043d292fd 100644 --- a/dapr/clients/http/dapr_invocation_http_client.py +++ b/dapr/clients/http/dapr_invocation_http_client.py @@ -51,7 +51,11 @@ def __init__( falls back to DAPR_API_TOKEN environment variable. """ self._client = DaprHttpClient( - DefaultJSONSerializer(), timeout, headers_callback, retry_policy=retry_policy, api_token=api_token + DefaultJSONSerializer(), + timeout, + headers_callback, + retry_policy=retry_policy, + api_token=api_token, ) async def invoke_method_async( From 46c78ecbaaf6ed4d6a991f2c69df6c30d820075c Mon Sep 17 00:00:00 2001 From: Filinto Duran <1373693+filintod@users.noreply.github.com> Date: Thu, 23 Oct 2025 17:27:26 -0500 Subject: [PATCH 03/14] add multi token support to workflows Signed-off-by: Filinto Duran <1373693+filintod@users.noreply.github.com> --- .../dapr/ext/workflow/dapr_workflow_client.py | 7 +- .../dapr/ext/workflow/workflow_runtime.py | 7 +- .../tests/test_multi_token_workflow.py | 154 ++++++++++++++++++ .../tests/test_workflow_client.py | 56 +++++++ .../tests/test_workflow_runtime.py | 56 +++++++ 5 files changed, 276 insertions(+), 4 deletions(-) create mode 100644 ext/dapr-ext-workflow/tests/test_multi_token_workflow.py diff --git a/ext/dapr-ext-workflow/dapr/ext/workflow/dapr_workflow_client.py b/ext/dapr-ext-workflow/dapr/ext/workflow/dapr_workflow_client.py index cc384503a..31e36ac2c 100644 --- a/ext/dapr-ext-workflow/dapr/ext/workflow/dapr_workflow_client.py +++ b/ext/dapr-ext-workflow/dapr/ext/workflow/dapr_workflow_client.py @@ -52,6 +52,7 @@ def __init__( host: Optional[str] = None, port: Optional[str] = None, logger_options: Optional[LoggerOptions] = None, + api_token: Optional[str] = None, ): address = getAddress(host, port) @@ -61,10 +62,12 @@ def __init__( raise DaprInternalError(f'{error}') from error self._logger = Logger('DaprWorkflowClient', logger_options) + self._api_token = api_token metadata = tuple() - if settings.DAPR_API_TOKEN: - metadata = ((DAPR_API_TOKEN_HEADER, settings.DAPR_API_TOKEN),) + token = self._api_token if self._api_token is not None else settings.DAPR_API_TOKEN + if token: + metadata = ((DAPR_API_TOKEN_HEADER, token),) options = self._logger.get_options() self.__obj = client.TaskHubGrpcClient( host_address=uri.endpoint, diff --git a/ext/dapr-ext-workflow/dapr/ext/workflow/workflow_runtime.py b/ext/dapr-ext-workflow/dapr/ext/workflow/workflow_runtime.py index d1f02b354..5ada9c2af 100644 --- a/ext/dapr-ext-workflow/dapr/ext/workflow/workflow_runtime.py +++ b/ext/dapr-ext-workflow/dapr/ext/workflow/workflow_runtime.py @@ -43,11 +43,14 @@ def __init__( host: Optional[str] = None, port: Optional[str] = None, logger_options: Optional[LoggerOptions] = None, + api_token: Optional[str] = None, ): self._logger = Logger('WorkflowRuntime', logger_options) + self._api_token = api_token metadata = tuple() - if settings.DAPR_API_TOKEN: - metadata = ((DAPR_API_TOKEN_HEADER, settings.DAPR_API_TOKEN),) + token = self._api_token if self._api_token is not None else settings.DAPR_API_TOKEN + if token: + metadata = ((DAPR_API_TOKEN_HEADER, token),) address = getAddress(host, port) try: diff --git a/ext/dapr-ext-workflow/tests/test_multi_token_workflow.py b/ext/dapr-ext-workflow/tests/test_multi_token_workflow.py new file mode 100644 index 000000000..6a187ceab --- /dev/null +++ b/ext/dapr-ext-workflow/tests/test_multi_token_workflow.py @@ -0,0 +1,154 @@ +# -*- coding: utf-8 -*- + +""" +Copyright 2024 The Dapr Authors +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +import unittest +from unittest import mock + +from dapr.conf import settings +from dapr.ext.workflow.dapr_workflow_client import DaprWorkflowClient +from dapr.ext.workflow.workflow_runtime import WorkflowRuntime + + +class FakeTaskHubGrpcClient: + """Fake gRPC client for testing""" + + pass + + +class FakeTaskHubGrpcWorker: + """Fake gRPC worker for testing""" + + def add_named_orchestrator(self, name, fn): + pass + + def add_named_activity(self, name, fn): + pass + + +class MultiTokenWorkflowTests(unittest.TestCase): + """Integration tests for multiple workflow instances with different API tokens""" + + def test_multiple_workflow_clients_different_tokens(self): + """Test multiple DaprWorkflowClient instances with different tokens""" + with mock.patch( + 'durabletask.client.TaskHubGrpcClient', return_value=FakeTaskHubGrpcClient() + ) as mock_grpc_client: + # Create two clients with different tokens + _ = DaprWorkflowClient(api_token='token-client-1') + _ = DaprWorkflowClient(api_token='token-client-2') + + # Verify first client uses its token + first_call = mock_grpc_client.call_args_list[0] + metadata1 = first_call[1]['metadata'] + assert len(metadata1) == 1 + assert metadata1[0] == ('dapr-api-token', 'token-client-1') + + # Verify second client uses its token + second_call = mock_grpc_client.call_args_list[1] + metadata2 = second_call[1]['metadata'] + assert len(metadata2) == 1 + assert metadata2[0] == ('dapr-api-token', 'token-client-2') + + def test_multiple_workflow_runtimes_different_tokens(self): + """Test multiple WorkflowRuntime instances with different tokens""" + with mock.patch( + 'durabletask.worker.TaskHubGrpcWorker', return_value=FakeTaskHubGrpcWorker() + ) as mock_grpc_worker: + # Create two runtimes with different tokens + _ = WorkflowRuntime(api_token='token-runtime-1') + _ = WorkflowRuntime(api_token='token-runtime-2') + + # Verify first runtime uses its token + first_call = mock_grpc_worker.call_args_list[0] + metadata1 = first_call[1]['metadata'] + assert len(metadata1) == 1 + assert metadata1[0] == ('dapr-api-token', 'token-runtime-1') + + # Verify second runtime uses its token + second_call = mock_grpc_worker.call_args_list[1] + metadata2 = second_call[1]['metadata'] + assert len(metadata2) == 1 + assert metadata2[0] == ('dapr-api-token', 'token-runtime-2') + + @mock.patch.object(settings, 'DAPR_API_TOKEN', 'global-token') + def test_mixing_explicit_and_global_tokens(self): + """Test mixing workflow instances with explicit tokens and global token""" + with mock.patch( + 'durabletask.client.TaskHubGrpcClient', return_value=FakeTaskHubGrpcClient() + ) as mock_grpc_client: + # Client with explicit token + _ = DaprWorkflowClient(api_token='explicit-token') + # Client using global token + _ = DaprWorkflowClient() + + # Verify explicit client uses its token + first_call = mock_grpc_client.call_args_list[0] + metadata_explicit = first_call[1]['metadata'] + assert len(metadata_explicit) == 1 + assert metadata_explicit[0] == ('dapr-api-token', 'explicit-token') + + # Verify global client uses global token + second_call = mock_grpc_client.call_args_list[1] + metadata_global = second_call[1]['metadata'] + assert len(metadata_global) == 1 + assert metadata_global[0] == ('dapr-api-token', 'global-token') + + def test_client_and_runtime_isolation(self): + """Test that client and runtime tokens don't interfere with each other""" + with mock.patch( + 'durabletask.client.TaskHubGrpcClient', return_value=FakeTaskHubGrpcClient() + ) as mock_client, mock.patch( + 'durabletask.worker.TaskHubGrpcWorker', return_value=FakeTaskHubGrpcWorker() + ) as mock_worker: + # Create client and runtime with different tokens + _ = DaprWorkflowClient(api_token='client-token') + _ = WorkflowRuntime(api_token='runtime-token') + + # Verify client uses its token + client_metadata = mock_client.call_args[1]['metadata'] + assert len(client_metadata) == 1 + assert client_metadata[0] == ('dapr-api-token', 'client-token') + + # Verify runtime uses its token + runtime_metadata = mock_worker.call_args[1]['metadata'] + assert len(runtime_metadata) == 1 + assert runtime_metadata[0] == ('dapr-api-token', 'runtime-token') + + def test_multiple_instances_token_isolation(self): + """Test that modifying one instance's token doesn't affect another""" + with mock.patch( + 'durabletask.client.TaskHubGrpcClient', return_value=FakeTaskHubGrpcClient() + ) as mock_grpc_client: + # Create three clients with different tokens + _ = DaprWorkflowClient(api_token='isolated-1') + _ = DaprWorkflowClient(api_token='isolated-2') + _ = DaprWorkflowClient(api_token='isolated-3') + + # Verify all three clients use their respective tokens + calls = mock_grpc_client.call_args_list + assert len(calls) == 3 + + metadata1 = calls[0][1]['metadata'] + assert metadata1[0] == ('dapr-api-token', 'isolated-1') + + metadata2 = calls[1][1]['metadata'] + assert metadata2[0] == ('dapr-api-token', 'isolated-2') + + metadata3 = calls[2][1]['metadata'] + assert metadata3[0] == ('dapr-api-token', 'isolated-3') + + +if __name__ == '__main__': + unittest.main() diff --git a/ext/dapr-ext-workflow/tests/test_workflow_client.py b/ext/dapr-ext-workflow/tests/test_workflow_client.py index 540c0e801..968e8724c 100644 --- a/ext/dapr-ext-workflow/tests/test_workflow_client.py +++ b/ext/dapr-ext-workflow/tests/test_workflow_client.py @@ -22,6 +22,7 @@ from durabletask import client import durabletask.internal.orchestrator_service_pb2 as pb from grpc import RpcError +from dapr.conf import settings mock_schedule_result = 'workflow001' mock_raise_event_result = 'event001' @@ -171,3 +172,58 @@ def test_client_functions(self): actual_purge_result = wfClient.purge_workflow(instance_id=mock_instance_id) assert actual_purge_result == mock_purge_result + + def test_client_with_explicit_api_token(self): + """Test that DaprWorkflowClient accepts and uses explicit api_token""" + with mock.patch( + 'durabletask.client.TaskHubGrpcClient', return_value=FakeTaskHubGrpcClient() + ) as mock_grpc_client: + _ = DaprWorkflowClient(api_token='explicit-token') + + # Verify the client was created with the correct metadata + call_kwargs = mock_grpc_client.call_args[1] + assert 'metadata' in call_kwargs + metadata = call_kwargs['metadata'] + assert len(metadata) == 1 + assert metadata[0] == ('dapr-api-token', 'explicit-token') + + @mock.patch.object(settings, 'DAPR_API_TOKEN', 'global-token') + def test_client_explicit_token_overrides_global(self): + """Test that explicit api_token overrides global DAPR_API_TOKEN""" + with mock.patch( + 'durabletask.client.TaskHubGrpcClient', return_value=FakeTaskHubGrpcClient() + ) as mock_grpc_client: + _ = DaprWorkflowClient(api_token='explicit-token') + + # Verify explicit token is used, not global + call_kwargs = mock_grpc_client.call_args[1] + metadata = call_kwargs['metadata'] + assert len(metadata) == 1 + assert metadata[0] == ('dapr-api-token', 'explicit-token') + + @mock.patch.object(settings, 'DAPR_API_TOKEN', 'global-token') + def test_client_falls_back_to_global_token(self): + """Test that client falls back to global DAPR_API_TOKEN when no explicit token""" + with mock.patch( + 'durabletask.client.TaskHubGrpcClient', return_value=FakeTaskHubGrpcClient() + ) as mock_grpc_client: + _ = DaprWorkflowClient() + + # Verify global token is used + call_kwargs = mock_grpc_client.call_args[1] + metadata = call_kwargs['metadata'] + assert len(metadata) == 1 + assert metadata[0] == ('dapr-api-token', 'global-token') + + @mock.patch.object(settings, 'DAPR_API_TOKEN', None) + def test_client_no_token(self): + """Test that client works without any token""" + with mock.patch( + 'durabletask.client.TaskHubGrpcClient', return_value=FakeTaskHubGrpcClient() + ) as mock_grpc_client: + _ = DaprWorkflowClient() + + # Verify no token metadata is passed + call_kwargs = mock_grpc_client.call_args[1] + metadata = call_kwargs['metadata'] + assert len(metadata) == 0 diff --git a/ext/dapr-ext-workflow/tests/test_workflow_runtime.py b/ext/dapr-ext-workflow/tests/test_workflow_runtime.py index 02d6c6f3b..af53a07f7 100644 --- a/ext/dapr-ext-workflow/tests/test_workflow_runtime.py +++ b/ext/dapr-ext-workflow/tests/test_workflow_runtime.py @@ -19,6 +19,7 @@ from unittest import mock from dapr.ext.workflow.workflow_runtime import WorkflowRuntime, alternate_name from dapr.ext.workflow.workflow_activity_context import WorkflowActivityContext +from dapr.conf import settings listOrchestrators: List[str] = [] listActivities: List[str] = [] @@ -170,3 +171,58 @@ def test_decorator_register_optinal_name(self): wanted_activity = ['test_act'] assert listActivities == wanted_activity assert client_act._dapr_alternate_name == 'test_act' + + def test_runtime_with_explicit_api_token(self): + """Test that WorkflowRuntime accepts and uses explicit api_token""" + with mock.patch( + 'durabletask.worker.TaskHubGrpcWorker', return_value=FakeTaskHubGrpcWorker() + ) as mock_grpc_worker: + _ = WorkflowRuntime(api_token='explicit-token-runtime') + + # Verify the worker was created with the correct metadata + call_kwargs = mock_grpc_worker.call_args[1] + assert 'metadata' in call_kwargs + metadata = call_kwargs['metadata'] + assert len(metadata) == 1 + assert metadata[0] == ('dapr-api-token', 'explicit-token-runtime') + + @mock.patch.object(settings, 'DAPR_API_TOKEN', 'global-token') + def test_runtime_explicit_token_overrides_global(self): + """Test that explicit api_token overrides global DAPR_API_TOKEN""" + with mock.patch( + 'durabletask.worker.TaskHubGrpcWorker', return_value=FakeTaskHubGrpcWorker() + ) as mock_grpc_worker: + _ = WorkflowRuntime(api_token='explicit-token-override') + + # Verify explicit token is used, not global + call_kwargs = mock_grpc_worker.call_args[1] + metadata = call_kwargs['metadata'] + assert len(metadata) == 1 + assert metadata[0] == ('dapr-api-token', 'explicit-token-override') + + @mock.patch.object(settings, 'DAPR_API_TOKEN', 'global-token') + def test_runtime_falls_back_to_global_token(self): + """Test that runtime falls back to global DAPR_API_TOKEN when no explicit token""" + with mock.patch( + 'durabletask.worker.TaskHubGrpcWorker', return_value=FakeTaskHubGrpcWorker() + ) as mock_grpc_worker: + _ = WorkflowRuntime() + + # Verify global token is used + call_kwargs = mock_grpc_worker.call_args[1] + metadata = call_kwargs['metadata'] + assert len(metadata) == 1 + assert metadata[0] == ('dapr-api-token', 'global-token') + + @mock.patch.object(settings, 'DAPR_API_TOKEN', None) + def test_runtime_no_token(self): + """Test that runtime works without any token""" + with mock.patch( + 'durabletask.worker.TaskHubGrpcWorker', return_value=FakeTaskHubGrpcWorker() + ) as mock_grpc_worker: + _ = WorkflowRuntime() + + # Verify no token metadata is passed + call_kwargs = mock_grpc_worker.call_args[1] + metadata = call_kwargs['metadata'] + assert len(metadata) == 0 From aa3ef52a4bde17e7c7300e4f7cbff10944403582 Mon Sep 17 00:00:00 2001 From: Filinto Duran <1373693+filintod@users.noreply.github.com> Date: Mon, 27 Oct 2025 00:55:02 -0500 Subject: [PATCH 04/14] Update ext/dapr-ext-workflow/tests/test_multi_token_workflow.py Co-authored-by: Albert Callarisa Signed-off-by: Filinto Duran <1373693+filintod@users.noreply.github.com> --- ext/dapr-ext-workflow/tests/test_multi_token_workflow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ext/dapr-ext-workflow/tests/test_multi_token_workflow.py b/ext/dapr-ext-workflow/tests/test_multi_token_workflow.py index 6a187ceab..448bc49a0 100644 --- a/ext/dapr-ext-workflow/tests/test_multi_token_workflow.py +++ b/ext/dapr-ext-workflow/tests/test_multi_token_workflow.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- """ -Copyright 2024 The Dapr Authors +Copyright 2025 The Dapr Authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at From b092daaf9f57dbf6e1f9f68c4356a83138d7ddac Mon Sep 17 00:00:00 2001 From: Filinto Duran <1373693+filintod@users.noreply.github.com> Date: Mon, 27 Oct 2025 00:57:21 -0500 Subject: [PATCH 05/14] Update dapr/clients/http/dapr_actor_http_client.py Co-authored-by: Albert Callarisa Signed-off-by: Filinto Duran <1373693+filintod@users.noreply.github.com> --- dapr/clients/http/dapr_actor_http_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dapr/clients/http/dapr_actor_http_client.py b/dapr/clients/http/dapr_actor_http_client.py index ac63672bc..202994487 100644 --- a/dapr/clients/http/dapr_actor_http_client.py +++ b/dapr/clients/http/dapr_actor_http_client.py @@ -49,7 +49,7 @@ def __init__( falls back to DAPR_API_TOKEN environment variable. """ self._client = DaprHttpClient( - message_serializer, timeout, headers_callback, retry_policy, api_token=api_token + message_serializer, timeout, headers_callback, retry_policy, api_token ) async def invoke_method( From 0f6edfac2432f17c1e676b7864746cbe52cc01f0 Mon Sep 17 00:00:00 2001 From: Filinto Duran <1373693+filintod@users.noreply.github.com> Date: Mon, 27 Oct 2025 00:57:58 -0500 Subject: [PATCH 06/14] Update dapr/clients/health.py Co-authored-by: Albert Callarisa Signed-off-by: Filinto Duran <1373693+filintod@users.noreply.github.com> --- dapr/clients/health.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dapr/clients/health.py b/dapr/clients/health.py index b61e2ab14..7f694e3ac 100644 --- a/dapr/clients/health.py +++ b/dapr/clients/health.py @@ -23,7 +23,7 @@ class DaprHealth: @staticmethod - def wait_until_ready(api_token: str = None): + def wait_until_ready(api_token: Optional[str] = None): health_url = f'{get_api_url()}/healthz/outbound' headers = {USER_AGENT_HEADER: DAPR_USER_AGENT} token = api_token if api_token is not None else settings.DAPR_API_TOKEN From 828544a17222cac40ed9a7900e2432994bb52be4 Mon Sep 17 00:00:00 2001 From: Filinto Duran <1373693+filintod@users.noreply.github.com> Date: Mon, 27 Oct 2025 09:22:23 -0500 Subject: [PATCH 07/14] incorporate feedback Signed-off-by: Filinto Duran <1373693+filintod@users.noreply.github.com> --- dapr/aio/clients/grpc/client.py | 14 ++++++-------- dapr/clients/grpc/client.py | 14 ++++++-------- dapr/clients/health.py | 2 ++ dapr/clients/http/client.py | 13 ++++++------- .../dapr/ext/workflow/dapr_workflow_client.py | 7 +++---- .../dapr/ext/workflow/workflow_runtime.py | 7 +++---- .../clients/test_http_service_invocation_client.py | 3 +++ 7 files changed, 29 insertions(+), 31 deletions(-) diff --git a/dapr/aio/clients/grpc/client.py b/dapr/aio/clients/grpc/client.py index 58b64ffb6..bd3bd8959 100644 --- a/dapr/aio/clients/grpc/client.py +++ b/dapr/aio/clients/grpc/client.py @@ -156,10 +156,10 @@ def __init__( api_token (str, optional): Dapr API token for authentication. If not provided, falls back to DAPR_API_TOKEN environment variable. """ - self._api_token = api_token - # For health check, use explicit token or fall back to global setting - health_token = api_token if api_token is not None else settings.DAPR_API_TOKEN - DaprHealth.wait_until_ready(api_token=health_token) + # Use explicit token if provided, otherwise fall back to global setting + api_token = api_token if api_token is not None else settings.DAPR_API_TOKEN + + DaprHealth.wait_until_ready(api_token=api_token) self.retry_policy = retry_policy or RetryPolicy() useragent = f'dapr-sdk-python/{__version__}' @@ -190,12 +190,10 @@ def __init__( else: interceptors.append(DaprClientTimeoutInterceptorAsync()) - # Use explicit token if provided, otherwise fall back to global setting - token = self._api_token if self._api_token is not None else settings.DAPR_API_TOKEN - if token: + if api_token: api_token_interceptor = DaprClientInterceptorAsync( [ - ('dapr-api-token', token), + ('dapr-api-token', api_token), ] ) interceptors.append(api_token_interceptor) diff --git a/dapr/clients/grpc/client.py b/dapr/clients/grpc/client.py index 0f5b064e1..ce83d0a75 100644 --- a/dapr/clients/grpc/client.py +++ b/dapr/clients/grpc/client.py @@ -148,10 +148,10 @@ def __init__( api_token (str, optional): Dapr API token for authentication. If not provided, falls back to DAPR_API_TOKEN environment variable. """ - self._api_token = api_token - # For health check, use explicit token or fall back to global setting - health_token = api_token if api_token is not None else settings.DAPR_API_TOKEN - DaprHealth.wait_until_ready(api_token=health_token) + # Use explicit token if provided, otherwise fall back to global setting + api_token = api_token if api_token is not None else settings.DAPR_API_TOKEN + + DaprHealth.wait_until_ready(api_token=api_token) self.retry_policy = retry_policy or RetryPolicy() useragent = f'dapr-sdk-python/{__version__}' @@ -190,12 +190,10 @@ def __init__( self._channel = grpc.intercept_channel(self._channel, DaprClientTimeoutInterceptor()) # type: ignore - # Use explicit token if provided, otherwise fall back to global setting - token = self._api_token if self._api_token is not None else settings.DAPR_API_TOKEN - if token: + if api_token: api_token_interceptor = DaprClientInterceptor( [ - ('dapr-api-token', token), + ('dapr-api-token', api_token), ] ) self._channel = grpc.intercept_channel( # type: ignore diff --git a/dapr/clients/health.py b/dapr/clients/health.py index 7f694e3ac..e3cb42726 100644 --- a/dapr/clients/health.py +++ b/dapr/clients/health.py @@ -16,6 +16,8 @@ import urllib.error import urllib.request +from typing import Optional + from dapr.clients.http.conf import DAPR_API_TOKEN_HEADER, DAPR_USER_AGENT, USER_AGENT_HEADER from dapr.clients.http.helpers import get_api_url from dapr.conf import settings diff --git a/dapr/clients/http/client.py b/dapr/clients/http/client.py index 4769b932e..fe287b600 100644 --- a/dapr/clients/http/client.py +++ b/dapr/clients/http/client.py @@ -54,10 +54,10 @@ def __init__( api_token (str, optional): Dapr API token for authentication. If not provided, falls back to DAPR_API_TOKEN environment variable. """ - self._api_token = api_token - # For health check, use explicit token or fall back to global setting - health_token = api_token if api_token is not None else settings.DAPR_API_TOKEN - DaprHealth.wait_until_ready(api_token=health_token) + # use provided api_token if any or fallback to env var DAPR_API_TOKEN + self._api_token = api_token if api_token is not None else settings.DAPR_API_TOKEN + + DaprHealth.wait_until_ready(api_token=self._api_token) self._timeout = aiohttp.ClientTimeout(total=timeout) self._serializer = message_serializer @@ -78,9 +78,8 @@ async def send_bytes( headers_map[CONTENT_TYPE_HEADER] = DEFAULT_JSON_CONTENT_TYPE # Use explicit token if provided, otherwise fall back to global setting - token = self._api_token if self._api_token is not None else settings.DAPR_API_TOKEN - if token is not None: - headers_map[DAPR_API_TOKEN_HEADER] = token + if self._api_token is not None: + headers_map[DAPR_API_TOKEN_HEADER] = self._api_token if self._headers_callback is not None: trace_headers = self._headers_callback() diff --git a/ext/dapr-ext-workflow/dapr/ext/workflow/dapr_workflow_client.py b/ext/dapr-ext-workflow/dapr/ext/workflow/dapr_workflow_client.py index 31e36ac2c..24284c5eb 100644 --- a/ext/dapr-ext-workflow/dapr/ext/workflow/dapr_workflow_client.py +++ b/ext/dapr-ext-workflow/dapr/ext/workflow/dapr_workflow_client.py @@ -62,12 +62,11 @@ def __init__( raise DaprInternalError(f'{error}') from error self._logger = Logger('DaprWorkflowClient', logger_options) - self._api_token = api_token metadata = tuple() - token = self._api_token if self._api_token is not None else settings.DAPR_API_TOKEN - if token: - metadata = ((DAPR_API_TOKEN_HEADER, token),) + api_token = api_token if api_token is not None else settings.DAPR_API_TOKEN + if api_token: + metadata = ((DAPR_API_TOKEN_HEADER, api_token),) options = self._logger.get_options() self.__obj = client.TaskHubGrpcClient( host_address=uri.endpoint, diff --git a/ext/dapr-ext-workflow/dapr/ext/workflow/workflow_runtime.py b/ext/dapr-ext-workflow/dapr/ext/workflow/workflow_runtime.py index 5ada9c2af..c0e629243 100644 --- a/ext/dapr-ext-workflow/dapr/ext/workflow/workflow_runtime.py +++ b/ext/dapr-ext-workflow/dapr/ext/workflow/workflow_runtime.py @@ -46,11 +46,10 @@ def __init__( api_token: Optional[str] = None, ): self._logger = Logger('WorkflowRuntime', logger_options) - self._api_token = api_token metadata = tuple() - token = self._api_token if self._api_token is not None else settings.DAPR_API_TOKEN - if token: - metadata = ((DAPR_API_TOKEN_HEADER, token),) + api_token = api_token if api_token is not None else settings.DAPR_API_TOKEN + if api_token: + metadata = ((DAPR_API_TOKEN_HEADER, api_token),) address = getAddress(host, port) try: diff --git a/tests/clients/test_http_service_invocation_client.py b/tests/clients/test_http_service_invocation_client.py index c0b43a863..8fa86a561 100644 --- a/tests/clients/test_http_service_invocation_client.py +++ b/tests/clients/test_http_service_invocation_client.py @@ -256,7 +256,10 @@ def test_generic_client_unknown_protocol(self): def test_invoke_method_with_api_token(self): self.server.set_response(b'FOO') + + # settings (env vars) should be set before creating client settings.DAPR_API_TOKEN = 'c29saSBkZW8gZ2xvcmlhCg==' + self.client = DaprClient() req = common_v1.StateItem(key='test') resp = self.client.invoke_method( From f7547a62334be7739d596da704b270e077a840b9 Mon Sep 17 00:00:00 2001 From: Filinto Duran <1373693+filintod@users.noreply.github.com> Date: Mon, 27 Oct 2025 09:58:13 -0500 Subject: [PATCH 08/14] Update dapr/aio/clients/__init__.py Co-authored-by: Albert Callarisa Signed-off-by: Filinto Duran <1373693+filintod@users.noreply.github.com> --- dapr/aio/clients/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dapr/aio/clients/__init__.py b/dapr/aio/clients/__init__.py index a07dc4718..d4ceb7008 100644 --- a/dapr/aio/clients/__init__.py +++ b/dapr/aio/clients/__init__.py @@ -83,7 +83,7 @@ def __init__( api_token (str, optional): Dapr API token for authentication. If not provided, falls back to DAPR_API_TOKEN environment variable. """ - super().__init__(address, interceptors, max_grpc_message_length, api_token=api_token) + super().__init__(address, interceptors, max_grpc_message_length, api_token) self.invocation_client = None invocation_protocol = settings.DAPR_API_METHOD_INVOCATION_PROTOCOL.upper() From 9b78558d78aa7b8c55b7ce3fd65144eec173697e Mon Sep 17 00:00:00 2001 From: Filinto Duran <1373693+filintod@users.noreply.github.com> Date: Mon, 27 Oct 2025 09:58:32 -0500 Subject: [PATCH 09/14] Update dapr/aio/clients/__init__.py Co-authored-by: Albert Callarisa Signed-off-by: Filinto Duran <1373693+filintod@users.noreply.github.com> --- dapr/aio/clients/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dapr/aio/clients/__init__.py b/dapr/aio/clients/__init__.py index d4ceb7008..a83f14aed 100644 --- a/dapr/aio/clients/__init__.py +++ b/dapr/aio/clients/__init__.py @@ -92,7 +92,7 @@ def __init__( if http_timeout_seconds is None: http_timeout_seconds = settings.DAPR_HTTP_TIMEOUT_SECONDS self.invocation_client = DaprInvocationHttpClient( - headers_callback=headers_callback, timeout=http_timeout_seconds, api_token=api_token + headers_callback=headers_callback, timeout=http_timeout_seconds, api_token ) elif invocation_protocol == 'GRPC': pass From 2fa1ff2921bac01fe5d4673c1dbfe8691055eb71 Mon Sep 17 00:00:00 2001 From: Filinto Duran <1373693+filintod@users.noreply.github.com> Date: Mon, 27 Oct 2025 09:59:34 -0500 Subject: [PATCH 10/14] Update dapr/aio/clients/grpc/client.py Co-authored-by: Albert Callarisa Signed-off-by: Filinto Duran <1373693+filintod@users.noreply.github.com> --- dapr/aio/clients/grpc/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dapr/aio/clients/grpc/client.py b/dapr/aio/clients/grpc/client.py index bd3bd8959..f0099cec3 100644 --- a/dapr/aio/clients/grpc/client.py +++ b/dapr/aio/clients/grpc/client.py @@ -190,7 +190,7 @@ def __init__( else: interceptors.append(DaprClientTimeoutInterceptorAsync()) - if api_token: + if api_token is not None: api_token_interceptor = DaprClientInterceptorAsync( [ ('dapr-api-token', api_token), From 465209c5ac3cdeaef3d3424324a164b50752f169 Mon Sep 17 00:00:00 2001 From: Filinto Duran <1373693+filintod@users.noreply.github.com> Date: Mon, 27 Oct 2025 09:59:44 -0500 Subject: [PATCH 11/14] Update dapr/clients/grpc/client.py Co-authored-by: Albert Callarisa Signed-off-by: Filinto Duran <1373693+filintod@users.noreply.github.com> --- dapr/clients/grpc/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dapr/clients/grpc/client.py b/dapr/clients/grpc/client.py index ce83d0a75..6cf880567 100644 --- a/dapr/clients/grpc/client.py +++ b/dapr/clients/grpc/client.py @@ -190,7 +190,7 @@ def __init__( self._channel = grpc.intercept_channel(self._channel, DaprClientTimeoutInterceptor()) # type: ignore - if api_token: + if api_token is not None: api_token_interceptor = DaprClientInterceptor( [ ('dapr-api-token', api_token), From bca1f9c7d0d9e7e2df90310dc2db922dbff13baf Mon Sep 17 00:00:00 2001 From: Filinto Duran <1373693+filintod@users.noreply.github.com> Date: Mon, 27 Oct 2025 10:00:06 -0500 Subject: [PATCH 12/14] Update ext/dapr-ext-workflow/dapr/ext/workflow/dapr_workflow_client.py Co-authored-by: Albert Callarisa Signed-off-by: Filinto Duran <1373693+filintod@users.noreply.github.com> --- ext/dapr-ext-workflow/dapr/ext/workflow/dapr_workflow_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ext/dapr-ext-workflow/dapr/ext/workflow/dapr_workflow_client.py b/ext/dapr-ext-workflow/dapr/ext/workflow/dapr_workflow_client.py index 24284c5eb..aac0538c8 100644 --- a/ext/dapr-ext-workflow/dapr/ext/workflow/dapr_workflow_client.py +++ b/ext/dapr-ext-workflow/dapr/ext/workflow/dapr_workflow_client.py @@ -65,7 +65,7 @@ def __init__( metadata = tuple() api_token = api_token if api_token is not None else settings.DAPR_API_TOKEN - if api_token: + if api_token is not None: metadata = ((DAPR_API_TOKEN_HEADER, api_token),) options = self._logger.get_options() self.__obj = client.TaskHubGrpcClient( From cd7f34c89a3ff5276fceaf10c87639079b31ca40 Mon Sep 17 00:00:00 2001 From: Filinto Duran <1373693+filintod@users.noreply.github.com> Date: Mon, 27 Oct 2025 10:00:17 -0500 Subject: [PATCH 13/14] Update ext/dapr-ext-workflow/dapr/ext/workflow/workflow_runtime.py Co-authored-by: Albert Callarisa Signed-off-by: Filinto Duran <1373693+filintod@users.noreply.github.com> --- ext/dapr-ext-workflow/dapr/ext/workflow/workflow_runtime.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ext/dapr-ext-workflow/dapr/ext/workflow/workflow_runtime.py b/ext/dapr-ext-workflow/dapr/ext/workflow/workflow_runtime.py index c0e629243..016b1ad90 100644 --- a/ext/dapr-ext-workflow/dapr/ext/workflow/workflow_runtime.py +++ b/ext/dapr-ext-workflow/dapr/ext/workflow/workflow_runtime.py @@ -48,7 +48,7 @@ def __init__( self._logger = Logger('WorkflowRuntime', logger_options) metadata = tuple() api_token = api_token if api_token is not None else settings.DAPR_API_TOKEN - if api_token: + if api_token is not None: metadata = ((DAPR_API_TOKEN_HEADER, api_token),) address = getAddress(host, port) From ebcdf1b60c5511201a89c1709d8ff66dd1fc0ae0 Mon Sep 17 00:00:00 2001 From: Filinto Duran <1373693+filintod@users.noreply.github.com> Date: Mon, 27 Oct 2025 11:49:51 -0500 Subject: [PATCH 14/14] fix bad positional arg after keyword arg Signed-off-by: Filinto Duran <1373693+filintod@users.noreply.github.com> --- dapr/aio/clients/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/dapr/aio/clients/__init__.py b/dapr/aio/clients/__init__.py index a83f14aed..edc473db0 100644 --- a/dapr/aio/clients/__init__.py +++ b/dapr/aio/clients/__init__.py @@ -92,7 +92,9 @@ def __init__( if http_timeout_seconds is None: http_timeout_seconds = settings.DAPR_HTTP_TIMEOUT_SECONDS self.invocation_client = DaprInvocationHttpClient( - headers_callback=headers_callback, timeout=http_timeout_seconds, api_token + headers_callback=headers_callback, + timeout=http_timeout_seconds, + api_token=api_token, ) elif invocation_protocol == 'GRPC': pass