Skip to content

Commit fe5c4e4

Browse files
committed
CRED-2146: Add PAT auth support to Python API client
1 parent 9f2b67a commit fe5c4e4

File tree

5 files changed

+188
-4
lines changed

5 files changed

+188
-4
lines changed

.generator/src/generator/templates/api_client.j2

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -893,7 +893,16 @@ class Endpoint:
893893
self.api_client.configuration.delegated_auth_org_uuid is not None
894894
)
895895

896-
if has_app_key_auth and has_delegated_auth:
896+
# Check if bearer token (PAT) auth is configured
897+
has_bearer_token = self.api_client.configuration.access_token is not None and "bearerAuth" in self.settings["auth"]
898+
899+
if has_bearer_token:
900+
# Bearer token authentication: send ONLY Authorization: Bearer header.
901+
# This is a separate auth path — no API key or app key headers.
902+
bearer_setting = self.api_client.configuration.auth_settings().get("bearerAuth")
903+
if bearer_setting:
904+
headers[bearer_setting["key"]] = bearer_setting["value"]
905+
elif has_app_key_auth and has_delegated_auth:
897906
# Use delegated token authentication
898907
self.api_client.use_delegated_token_auth(headers)
899908
else:

.generator/src/generator/templates/configuration.j2

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,8 @@ class Configuration:
269269
self.api_key["apiKeyAuth"] = os.environ["DD_API_KEY"]
270270
if "DD_APP_KEY" in os.environ and not self.api_key.get("appKeyAuth"):
271271
self.api_key["appKeyAuth"] = os.environ["DD_APP_KEY"]
272+
if "DD_BEARER_TOKEN" in os.environ and not self.access_token:
273+
self.access_token = os.environ["DD_BEARER_TOKEN"]
272274

273275
def __deepcopy__(self, memo):
274276
cls = self.__class__
@@ -535,7 +537,7 @@ class Configuration:
535537
"key": "Authorization",
536538
"value": self.get_basic_auth_token()
537539
}
538-
{# {%- elif schema.type == "http" and schema.scheme == "bearer" %}
540+
{%- elif schema.type == "http" and schema.scheme == "bearer" %}
539541
if self.access_token is not None:
540542
auth["{{name}}"] = {
541543
"type": "bearer",
@@ -546,7 +548,6 @@ class Configuration:
546548
"key": "Authorization",
547549
"value": "Bearer " + self.access_token
548550
}
549-
#}
550551
{%- elif schema.type == "oauth2" %}
551552
if self.access_token is not None:
552553
auth["AuthZ"] = {

src/datadog_api_client/api_client.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -902,7 +902,16 @@ def update_params_for_auth(self, headers, queries) -> None:
902902
and self.api_client.configuration.delegated_auth_org_uuid is not None
903903
)
904904

905-
if has_app_key_auth and has_delegated_auth:
905+
# Check if bearer token (PAT) auth is configured
906+
has_bearer_token = self.api_client.configuration.access_token is not None and "bearerAuth" in self.settings["auth"]
907+
908+
if has_bearer_token:
909+
# Bearer token authentication: send ONLY Authorization: Bearer header.
910+
# This is a separate auth path — no API key or app key headers.
911+
bearer_setting = self.api_client.configuration.auth_settings().get("bearerAuth")
912+
if bearer_setting:
913+
headers[bearer_setting["key"]] = bearer_setting["value"]
914+
elif has_app_key_auth and has_delegated_auth:
906915
# Use delegated token authentication
907916
self.api_client.use_delegated_token_auth(headers)
908917
else:

src/datadog_api_client/configuration.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -478,6 +478,8 @@ def __init__(
478478
self.api_key["apiKeyAuth"] = os.environ["DD_API_KEY"]
479479
if "DD_APP_KEY" in os.environ and not self.api_key.get("appKeyAuth"):
480480
self.api_key["appKeyAuth"] = os.environ["DD_APP_KEY"]
481+
if "DD_BEARER_TOKEN" in os.environ and not self.access_token:
482+
self.access_token = os.environ["DD_BEARER_TOKEN"]
481483

482484
def __deepcopy__(self, memo):
483485
cls = self.__class__
@@ -777,4 +779,11 @@ def auth_settings(self):
777779
"appKeyAuth",
778780
),
779781
}
782+
if self.access_token is not None:
783+
auth["bearerAuth"] = {
784+
"type": "bearer",
785+
"in": "header",
786+
"key": "Authorization",
787+
"value": "Bearer " + self.access_token,
788+
}
780789
return auth

tests/test_pat_auth.py

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
"""Tests for Personal Access Token (PAT) authentication support."""
2+
3+
import pytest
4+
from datetime import datetime, timedelta
5+
from unittest.mock import patch
6+
7+
from datadog_api_client.api_client import ApiClient, Endpoint as _Endpoint
8+
from datadog_api_client.configuration import Configuration
9+
from datadog_api_client.delegated_auth import (
10+
DelegatedTokenCredentials,
11+
DelegatedTokenConfig,
12+
DelegatedTokenProvider,
13+
)
14+
15+
16+
class TestBearerTokenConfiguration:
17+
"""Test access_token field on Configuration for bearer auth."""
18+
19+
def test_bearer_token_stored(self):
20+
config = Configuration(access_token="ddpat_test123")
21+
assert config.access_token == "ddpat_test123"
22+
23+
def test_bearer_token_default_none(self):
24+
config = Configuration()
25+
assert config.access_token is None
26+
27+
@patch.dict("os.environ", {"DD_BEARER_TOKEN": "ddpat_from_env"})
28+
def test_bearer_token_env_var(self):
29+
config = Configuration()
30+
assert config.access_token == "ddpat_from_env"
31+
32+
@patch.dict("os.environ", {"DD_BEARER_TOKEN": "ddpat_from_env"})
33+
def test_bearer_token_env_var_no_override(self):
34+
config = Configuration(access_token="ddpat_explicit")
35+
assert config.access_token == "ddpat_explicit"
36+
37+
38+
class TestAuthSettingsWithBearerToken:
39+
"""Test auth_settings() with access_token configured."""
40+
41+
def test_auth_settings_includes_bearer(self):
42+
config = Configuration(access_token="ddpat_test123")
43+
auth = config.auth_settings()
44+
assert "bearerAuth" in auth
45+
assert auth["bearerAuth"]["type"] == "bearer"
46+
assert auth["bearerAuth"]["in"] == "header"
47+
assert auth["bearerAuth"]["key"] == "Authorization"
48+
assert auth["bearerAuth"]["value"] == "Bearer ddpat_test123"
49+
50+
def test_auth_settings_without_bearer(self):
51+
config = Configuration()
52+
auth = config.auth_settings()
53+
assert "bearerAuth" not in auth
54+
55+
56+
class TestUpdateParamsForAuthWithBearerToken:
57+
"""Test that update_params_for_auth uses bearer token correctly."""
58+
59+
def _make_endpoint(self, config, auth_schemes):
60+
"""Helper to create an Endpoint with given auth schemes."""
61+
api_client = ApiClient(config)
62+
return _Endpoint(
63+
settings={
64+
"response_type": None,
65+
"auth": auth_schemes,
66+
"endpoint_path": "/api/v2/test",
67+
"operation_id": "test_op",
68+
"http_method": "GET",
69+
"version": "v2",
70+
},
71+
params_map={},
72+
headers_map={"accept": ["application/json"]},
73+
api_client=api_client,
74+
)
75+
76+
def test_bearer_sends_only_authorization_header(self):
77+
"""When access_token is set and bearerAuth is in auth, only Authorization: Bearer is sent."""
78+
config = Configuration(
79+
api_key={"apiKeyAuth": "test-api-key", "appKeyAuth": "test-app-key"},
80+
access_token="ddpat_test_pat",
81+
)
82+
endpoint = self._make_endpoint(config, ["apiKeyAuth", "appKeyAuth", "bearerAuth"])
83+
headers = {}
84+
queries = []
85+
endpoint.update_params_for_auth(headers, queries)
86+
87+
assert headers["Authorization"] == "Bearer ddpat_test_pat"
88+
assert "DD-API-KEY" not in headers
89+
assert "DD-APPLICATION-KEY" not in headers
90+
91+
def test_bearer_without_api_keys(self):
92+
"""Bearer token works even without any API keys configured."""
93+
config = Configuration(access_token="ddpat_test_pat")
94+
endpoint = self._make_endpoint(config, ["apiKeyAuth", "appKeyAuth", "bearerAuth"])
95+
headers = {}
96+
queries = []
97+
endpoint.update_params_for_auth(headers, queries)
98+
99+
assert headers["Authorization"] == "Bearer ddpat_test_pat"
100+
assert "DD-API-KEY" not in headers
101+
assert "DD-APPLICATION-KEY" not in headers
102+
103+
def test_no_bearer_when_not_in_endpoint_auth(self):
104+
"""access_token set but endpoint doesn't declare bearerAuth — uses regular auth."""
105+
config = Configuration(
106+
api_key={"apiKeyAuth": "test-api-key", "appKeyAuth": "test-app-key"},
107+
access_token="ddpat_test_pat",
108+
)
109+
endpoint = self._make_endpoint(config, ["apiKeyAuth", "appKeyAuth"])
110+
headers = {}
111+
queries = []
112+
endpoint.update_params_for_auth(headers, queries)
113+
114+
assert headers["DD-API-KEY"] == "test-api-key"
115+
assert headers["DD-APPLICATION-KEY"] == "test-app-key"
116+
117+
def test_regular_auth_without_bearer(self):
118+
config = Configuration(
119+
api_key={"apiKeyAuth": "test-api-key", "appKeyAuth": "test-app-key"},
120+
)
121+
endpoint = self._make_endpoint(config, ["apiKeyAuth", "appKeyAuth", "bearerAuth"])
122+
headers = {}
123+
queries = []
124+
endpoint.update_params_for_auth(headers, queries)
125+
126+
assert headers["DD-API-KEY"] == "test-api-key"
127+
assert headers["DD-APPLICATION-KEY"] == "test-app-key"
128+
assert "Authorization" not in headers
129+
130+
def test_bearer_takes_priority_over_delegated_auth(self):
131+
"""When both bearer token and delegated auth are configured, bearer wins."""
132+
133+
class MockProvider(DelegatedTokenProvider):
134+
def authenticate(self, config, api_config):
135+
return DelegatedTokenCredentials(
136+
org_uuid="test-org",
137+
delegated_token="delegated-token-123",
138+
delegated_proof="proof",
139+
expiration=datetime.now() + timedelta(minutes=10),
140+
)
141+
142+
config = Configuration(
143+
api_key={"apiKeyAuth": "test-api-key", "appKeyAuth": "test-app-key"},
144+
access_token="ddpat_test_pat",
145+
delegated_auth_provider=MockProvider(),
146+
delegated_auth_org_uuid="test-org",
147+
)
148+
endpoint = self._make_endpoint(config, ["apiKeyAuth", "appKeyAuth", "bearerAuth"])
149+
headers = {}
150+
queries = []
151+
endpoint.update_params_for_auth(headers, queries)
152+
153+
# Bearer token takes priority
154+
assert headers["Authorization"] == "Bearer ddpat_test_pat"
155+
assert "DD-API-KEY" not in headers
156+
assert "DD-APPLICATION-KEY" not in headers

0 commit comments

Comments
 (0)