Skip to content

Commit fd911c2

Browse files
committed
Add PAT (Personal Access Token) auth support via Bearer header
PAT auth is a separate auth path from API key + App key. When a bearer token is configured, the client sends ONLY the Authorization: Bearer header — no DD-API-KEY or DD-APPLICATION-KEY headers are sent. Priority order: PAT (bearer_token) > Delegated Auth > API key + App key.
1 parent 9f2b67a commit fd911c2

File tree

5 files changed

+198
-2
lines changed

5 files changed

+198
-2
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.bearer_token is not None
898+
899+
if has_bearer_token:
900+
# PAT 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: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,7 @@ class Configuration:
179179
retry_policy=None,
180180
delegated_auth_provider=None,
181181
delegated_auth_org_uuid=None,
182+
bearer_token=None,
182183
):
183184
"""Constructor."""
184185
self._base_path = "https://api.datadoghq.com" if host is None else host
@@ -190,6 +191,7 @@ class Configuration:
190191

191192
# Authentication Settings
192193
self.access_token = access_token
194+
self.bearer_token = bearer_token
193195
self.api_key = {}
194196
if api_key:
195197
self.api_key = api_key
@@ -269,6 +271,8 @@ class Configuration:
269271
self.api_key["apiKeyAuth"] = os.environ["DD_API_KEY"]
270272
if "DD_APP_KEY" in os.environ and not self.api_key.get("appKeyAuth"):
271273
self.api_key["appKeyAuth"] = os.environ["DD_APP_KEY"]
274+
if "DD_BEARER_TOKEN" in os.environ and not self.bearer_token:
275+
self.bearer_token = os.environ["DD_BEARER_TOKEN"]
272276

273277
def __deepcopy__(self, memo):
274278
cls = self.__class__
@@ -557,5 +561,12 @@ class Configuration:
557561
}
558562
{%- endif %}
559563
{%- endfor %}
564+
if self.bearer_token is not None:
565+
auth["bearerAuth"] = {
566+
"type": "bearer",
567+
"in": "header",
568+
"key": "Authorization",
569+
"value": "Bearer " + self.bearer_token,
570+
}
560571
return auth
561572
{# keep new line #}

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.bearer_token is not None
907+
908+
if has_bearer_token:
909+
# PAT 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: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,7 @@ def __init__(
180180
retry_policy=None,
181181
delegated_auth_provider=None,
182182
delegated_auth_org_uuid=None,
183+
bearer_token=None,
183184
):
184185
"""Constructor."""
185186
self._base_path = "https://api.datadoghq.com" if host is None else host
@@ -191,6 +192,7 @@ def __init__(
191192

192193
# Authentication Settings
193194
self.access_token = access_token
195+
self.bearer_token = bearer_token
194196
self.api_key = {}
195197
if api_key:
196198
self.api_key = api_key
@@ -478,6 +480,8 @@ def __init__(
478480
self.api_key["apiKeyAuth"] = os.environ["DD_API_KEY"]
479481
if "DD_APP_KEY" in os.environ and not self.api_key.get("appKeyAuth"):
480482
self.api_key["appKeyAuth"] = os.environ["DD_APP_KEY"]
483+
if "DD_BEARER_TOKEN" in os.environ and not self.bearer_token:
484+
self.bearer_token = os.environ["DD_BEARER_TOKEN"]
481485

482486
def __deepcopy__(self, memo):
483487
cls = self.__class__
@@ -777,4 +781,11 @@ def auth_settings(self):
777781
"appKeyAuth",
778782
),
779783
}
784+
if self.bearer_token is not None:
785+
auth["bearerAuth"] = {
786+
"type": "bearer",
787+
"in": "header",
788+
"key": "Authorization",
789+
"value": "Bearer " + self.bearer_token,
790+
}
780791
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 bearer_token field on Configuration."""
18+
19+
def test_bearer_token_stored(self):
20+
config = Configuration(bearer_token="ddpat_test123")
21+
assert config.bearer_token == "ddpat_test123"
22+
23+
def test_bearer_token_default_none(self):
24+
config = Configuration()
25+
assert config.bearer_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.bearer_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(bearer_token="ddpat_explicit")
35+
assert config.bearer_token == "ddpat_explicit"
36+
37+
38+
class TestAuthSettingsWithBearerToken:
39+
"""Test auth_settings() with bearer_token configured."""
40+
41+
def test_auth_settings_includes_bearer(self):
42+
config = Configuration(bearer_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 bearer_token is set, only Authorization: Bearer is sent."""
78+
config = Configuration(
79+
api_key={"apiKeyAuth": "test-api-key", "appKeyAuth": "test-app-key"},
80+
bearer_token="ddpat_test_pat",
81+
)
82+
endpoint = self._make_endpoint(config, ["apiKeyAuth", "appKeyAuth"])
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(bearer_token="ddpat_test_pat")
94+
endpoint = self._make_endpoint(config, ["apiKeyAuth", "appKeyAuth"])
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_bearer_on_apikey_only_endpoint(self):
104+
"""Bearer token is used even if endpoint only declares apiKeyAuth."""
105+
config = Configuration(
106+
api_key={"apiKeyAuth": "test-api-key"},
107+
bearer_token="ddpat_test_pat",
108+
)
109+
endpoint = self._make_endpoint(config, ["apiKeyAuth"])
110+
headers = {}
111+
queries = []
112+
endpoint.update_params_for_auth(headers, queries)
113+
114+
assert headers["Authorization"] == "Bearer ddpat_test_pat"
115+
assert "DD-API-KEY" not in headers
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"])
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+
bearer_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"])
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)