From 9bc92a2501f781bed700c65bdebd192c6c2b7b37 Mon Sep 17 00:00:00 2001 From: Tausif Rahman Date: Thu, 5 Mar 2026 17:37:34 +0000 Subject: [PATCH 1/2] CRED-2146: Add PAT auth support to Python API client - templates and tests --- .../src/generator/templates/configuration.j2 | 5 +- tests/test_pat_auth.py | 130 ++++++++++++++++++ 2 files changed, 133 insertions(+), 2 deletions(-) create mode 100644 tests/test_pat_auth.py diff --git a/.generator/src/generator/templates/configuration.j2 b/.generator/src/generator/templates/configuration.j2 index b0f3730142..05e490f291 100644 --- a/.generator/src/generator/templates/configuration.j2 +++ b/.generator/src/generator/templates/configuration.j2 @@ -269,6 +269,8 @@ class Configuration: self.api_key["apiKeyAuth"] = os.environ["DD_API_KEY"] if "DD_APP_KEY" in os.environ and not self.api_key.get("appKeyAuth"): self.api_key["appKeyAuth"] = os.environ["DD_APP_KEY"] + if "DD_BEARER_TOKEN" in os.environ and not self.access_token: + self.access_token = os.environ["DD_BEARER_TOKEN"] def __deepcopy__(self, memo): cls = self.__class__ @@ -535,7 +537,7 @@ class Configuration: "key": "Authorization", "value": self.get_basic_auth_token() } -{# {%- elif schema.type == "http" and schema.scheme == "bearer" %} +{%- elif schema.type == "http" and schema.scheme == "bearer" %} if self.access_token is not None: auth["{{name}}"] = { "type": "bearer", @@ -546,7 +548,6 @@ class Configuration: "key": "Authorization", "value": "Bearer " + self.access_token } -#} {%- elif schema.type == "oauth2" %} if self.access_token is not None: auth["AuthZ"] = { diff --git a/tests/test_pat_auth.py b/tests/test_pat_auth.py new file mode 100644 index 0000000000..2e4560bf78 --- /dev/null +++ b/tests/test_pat_auth.py @@ -0,0 +1,130 @@ +"""Tests for Personal Access Token (PAT) authentication support.""" + +import pytest +from datetime import datetime, timedelta +from unittest.mock import patch + +from datadog_api_client.api_client import ApiClient, Endpoint as _Endpoint +from datadog_api_client.configuration import Configuration +from datadog_api_client.delegated_auth import ( + DelegatedTokenCredentials, + DelegatedTokenConfig, + DelegatedTokenProvider, +) + + +class TestBearerTokenConfiguration: + """Test access_token field on Configuration for bearer auth.""" + + def test_bearer_token_stored(self): + config = Configuration(access_token="ddpat_test123") + assert config.access_token == "ddpat_test123" + + def test_bearer_token_default_none(self): + config = Configuration() + assert config.access_token is None + + @patch.dict("os.environ", {"DD_BEARER_TOKEN": "ddpat_from_env"}) + def test_bearer_token_env_var(self): + config = Configuration() + assert config.access_token == "ddpat_from_env" + + @patch.dict("os.environ", {"DD_BEARER_TOKEN": "ddpat_from_env"}) + def test_bearer_token_env_var_no_override(self): + config = Configuration(access_token="ddpat_explicit") + assert config.access_token == "ddpat_explicit" + + +class TestAuthSettingsWithBearerToken: + """Test auth_settings() with access_token configured.""" + + def test_auth_settings_includes_bearer(self): + config = Configuration(access_token="ddpat_test123") + auth = config.auth_settings() + assert "bearerAuth" in auth + assert auth["bearerAuth"]["type"] == "bearer" + assert auth["bearerAuth"]["in"] == "header" + assert auth["bearerAuth"]["key"] == "Authorization" + assert auth["bearerAuth"]["value"] == "Bearer ddpat_test123" + + def test_auth_settings_without_bearer(self): + config = Configuration() + auth = config.auth_settings() + assert "bearerAuth" not in auth + + +class TestUpdateParamsForAuthWithBearerToken: + """Test that update_params_for_auth sends all configured auth headers.""" + + def _make_endpoint(self, config, auth_schemes): + """Helper to create an Endpoint with given auth schemes.""" + api_client = ApiClient(config) + return _Endpoint( + settings={ + "response_type": None, + "auth": auth_schemes, + "endpoint_path": "/api/v2/test", + "operation_id": "test_op", + "http_method": "GET", + "version": "v2", + }, + params_map={}, + headers_map={"accept": ["application/json"]}, + api_client=api_client, + ) + + def test_all_auth_headers_sent_when_all_configured(self): + """When all credentials are set, all auth headers are sent.""" + config = Configuration( + api_key={"apiKeyAuth": "test-api-key", "appKeyAuth": "test-app-key"}, + access_token="ddpat_test_pat", + ) + endpoint = self._make_endpoint(config, ["apiKeyAuth", "appKeyAuth", "bearerAuth"]) + headers = {} + queries = [] + endpoint.update_params_for_auth(headers, queries) + + assert headers["Authorization"] == "Bearer ddpat_test_pat" + assert headers["DD-API-KEY"] == "test-api-key" + assert headers["DD-APPLICATION-KEY"] == "test-app-key" + + def test_bearer_only(self): + """Bearer token works without any API keys configured.""" + config = Configuration(access_token="ddpat_test_pat") + endpoint = self._make_endpoint(config, ["apiKeyAuth", "appKeyAuth", "bearerAuth"]) + headers = {} + queries = [] + endpoint.update_params_for_auth(headers, queries) + + assert headers["Authorization"] == "Bearer ddpat_test_pat" + assert "DD-API-KEY" not in headers + assert "DD-APPLICATION-KEY" not in headers + + def test_api_keys_only(self): + """Regular auth works without bearer token.""" + config = Configuration( + api_key={"apiKeyAuth": "test-api-key", "appKeyAuth": "test-app-key"}, + ) + endpoint = self._make_endpoint(config, ["apiKeyAuth", "appKeyAuth", "bearerAuth"]) + headers = {} + queries = [] + endpoint.update_params_for_auth(headers, queries) + + assert headers["DD-API-KEY"] == "test-api-key" + assert headers["DD-APPLICATION-KEY"] == "test-app-key" + assert "Authorization" not in headers + + def test_bearer_not_sent_when_not_in_endpoint_auth(self): + """access_token set but endpoint doesn't declare bearerAuth.""" + config = Configuration( + api_key={"apiKeyAuth": "test-api-key", "appKeyAuth": "test-app-key"}, + access_token="ddpat_test_pat", + ) + endpoint = self._make_endpoint(config, ["apiKeyAuth", "appKeyAuth"]) + headers = {} + queries = [] + endpoint.update_params_for_auth(headers, queries) + + assert headers["DD-API-KEY"] == "test-api-key" + assert headers["DD-APPLICATION-KEY"] == "test-app-key" + assert "Authorization" not in headers From 3c7c23c8e461dbfeca5f116fc6356c37eee57c89 Mon Sep 17 00:00:00 2001 From: Tausif Rahman Date: Thu, 5 Mar 2026 17:38:30 +0000 Subject: [PATCH 2/2] CRED-2146: Regenerated client code from templates --- src/datadog_api_client/configuration.py | 9 +++++++++ tests/test_pat_auth.py | 7 ------- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/datadog_api_client/configuration.py b/src/datadog_api_client/configuration.py index 8aa9287f21..8d243d852d 100644 --- a/src/datadog_api_client/configuration.py +++ b/src/datadog_api_client/configuration.py @@ -478,6 +478,8 @@ def __init__( self.api_key["apiKeyAuth"] = os.environ["DD_API_KEY"] if "DD_APP_KEY" in os.environ and not self.api_key.get("appKeyAuth"): self.api_key["appKeyAuth"] = os.environ["DD_APP_KEY"] + if "DD_BEARER_TOKEN" in os.environ and not self.access_token: + self.access_token = os.environ["DD_BEARER_TOKEN"] def __deepcopy__(self, memo): cls = self.__class__ @@ -777,4 +779,11 @@ def auth_settings(self): "appKeyAuth", ), } + if self.access_token is not None: + auth["bearerAuth"] = { + "type": "bearer", + "in": "header", + "key": "Authorization", + "value": "Bearer " + self.access_token, + } return auth diff --git a/tests/test_pat_auth.py b/tests/test_pat_auth.py index 2e4560bf78..6614eebad7 100644 --- a/tests/test_pat_auth.py +++ b/tests/test_pat_auth.py @@ -1,16 +1,9 @@ """Tests for Personal Access Token (PAT) authentication support.""" -import pytest -from datetime import datetime, timedelta from unittest.mock import patch from datadog_api_client.api_client import ApiClient, Endpoint as _Endpoint from datadog_api_client.configuration import Configuration -from datadog_api_client.delegated_auth import ( - DelegatedTokenCredentials, - DelegatedTokenConfig, - DelegatedTokenProvider, -) class TestBearerTokenConfiguration: