Skip to content

Commit 4c42418

Browse files
committed
Add PAT (Personal Access Token) auth support
- Add set_pat() convenience method on Configuration to set a PAT that is used in place of an application key (DD-APPLICATION-KEY header) - Add DD_PAT env var support, taking precedence over DD_APP_KEY when set - Update both configuration.rs and configuration.j2 template in sync - Add unit tests for PAT configuration behavior Ref: CRED-2150 Use Authorization: Bearer header for PAT auth instead of DD-APPLICATION-KEY PAT auth is now a separate auth path from API key + App key: - When PAT is set: sends only Authorization: Bearer <PAT> header - When PAT is not set: uses existing DD-API-KEY + DD-APPLICATION-KEY flow - These are mutually exclusive with no fallback between them Added auth_headers() method to Configuration that all generated API methods now call instead of inline auth key lookups.
1 parent f2e7ab9 commit 4c42418

File tree

134 files changed

+6783
-13086
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

134 files changed

+6783
-13086
lines changed

.generator/src/generator/templates/api.j2

Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -384,19 +384,14 @@ impl {{ structName }} {
384384
};
385385

386386
// build auth
387-
{%- set authMethods = operation.security if "security" in operation else openapi.security %}
388-
{%- if authMethods %}
389-
{%- for authMethod in authMethods %}
390-
{%- for name in authMethod %}
391-
{%- set schema = openapi.components.securitySchemes[name] %}
392-
{%- if schema.type == "apiKey" and schema.in != "cookie" %}
393-
if let Some(local_key) = local_configuration.auth_keys.get("{{ name }}") {
394-
headers.insert("{{schema.name}}", HeaderValue::from_str(local_key.key.as_str()).expect("failed to parse {{schema.name}} header"));
395-
};
396-
{%- endif %}
397-
{%- endfor %}
398-
{%- endfor %}
399-
{%- endif %}
387+
for (key, value) in local_configuration.auth_headers() {
388+
headers.insert(
389+
reqwest::header::HeaderName::from_bytes(key.as_bytes())
390+
.expect("failed to parse auth header name"),
391+
HeaderValue::from_str(value.as_str())
392+
.expect("failed to parse auth header value"),
393+
);
394+
}
400395

401396
{% if formParameter %}
402397
// build form parameters

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

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ pub struct Configuration {
4444
pub(crate) user_agent: String,
4545
pub(crate) unstable_operations: HashMap<String, bool>,
4646
pub(crate) auth_keys: HashMap<String, APIKey>,
47+
pub(crate) pat: Option<String>,
4748
pub server_index: usize,
4849
pub server_variables: HashMap<String, String>,
4950
pub server_operation_index: HashMap<String, usize>,
@@ -106,6 +107,30 @@ impl Configuration {
106107
self.auth_keys.insert(operation_str.to_string(), api_key);
107108
}
108109

110+
/// Set a Personal Access Token (PAT) for authentication.
111+
/// When a PAT is configured, the client sends only an `Authorization: Bearer <PAT>`
112+
/// header and does NOT send DD-API-KEY or DD-APPLICATION-KEY headers.
113+
pub fn set_pat(&mut self, pat: String) {
114+
self.pat = Some(pat);
115+
}
116+
117+
/// Build authentication headers for an API request.
118+
/// If a PAT is configured, returns a single `Authorization: Bearer` header.
119+
/// Otherwise, returns `DD-API-KEY` and `DD-APPLICATION-KEY` headers.
120+
pub fn auth_headers(&self) -> Vec<(String, String)> {
121+
if let Some(ref pat) = self.pat {
122+
return vec![("Authorization".to_string(), format!("Bearer {}", pat))];
123+
}
124+
let mut headers = Vec::new();
125+
if let Some(key) = self.auth_keys.get("apiKeyAuth") {
126+
headers.push(("DD-API-KEY".to_string(), key.key.clone()));
127+
}
128+
if let Some(key) = self.auth_keys.get("appKeyAuth") {
129+
headers.push(("DD-APPLICATION-KEY".to_string(), key.key.clone()));
130+
}
131+
headers
132+
}
133+
109134
pub fn set_proxy_url(&mut self, proxy_url: Option<String>) {
110135
self.proxy_url = proxy_url;
111136
}
@@ -149,10 +174,14 @@ impl Default for Configuration {
149174
{%- endfor %}
150175
{%- endif %}
151176

177+
// DD_PAT env var enables Bearer token auth (mutually exclusive with API key auth)
178+
let pat = env::var("DD_PAT").ok().filter(|p| !p.is_empty());
179+
152180
Self {
153181
user_agent: DEFAULT_USER_AGENT.clone(),
154182
unstable_operations,
155183
auth_keys,
184+
pat,
156185
server_index: 0,
157186
server_variables: HashMap::from([(
158187
"site".into(),

src/datadog/configuration.rs

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ pub struct Configuration {
4646
pub(crate) user_agent: String,
4747
pub(crate) unstable_operations: HashMap<String, bool>,
4848
pub(crate) auth_keys: HashMap<String, APIKey>,
49+
pub(crate) pat: Option<String>,
4950
pub server_index: usize,
5051
pub server_variables: HashMap<String, String>,
5152
pub server_operation_index: HashMap<String, usize>,
@@ -109,6 +110,30 @@ impl Configuration {
109110
self.auth_keys.insert(operation_str.to_string(), api_key);
110111
}
111112

113+
/// Set a Personal Access Token (PAT) for authentication.
114+
/// When a PAT is configured, the client sends only an `Authorization: Bearer <PAT>`
115+
/// header and does NOT send DD-API-KEY or DD-APPLICATION-KEY headers.
116+
pub fn set_pat(&mut self, pat: String) {
117+
self.pat = Some(pat);
118+
}
119+
120+
/// Build authentication headers for an API request.
121+
/// If a PAT is configured, returns a single `Authorization: Bearer` header.
122+
/// Otherwise, returns `DD-API-KEY` and `DD-APPLICATION-KEY` headers.
123+
pub fn auth_headers(&self) -> Vec<(String, String)> {
124+
if let Some(ref pat) = self.pat {
125+
return vec![("Authorization".to_string(), format!("Bearer {}", pat))];
126+
}
127+
let mut headers = Vec::new();
128+
if let Some(key) = self.auth_keys.get("apiKeyAuth") {
129+
headers.push(("DD-API-KEY".to_string(), key.key.clone()));
130+
}
131+
if let Some(key) = self.auth_keys.get("appKeyAuth") {
132+
headers.push(("DD-APPLICATION-KEY".to_string(), key.key.clone()));
133+
}
134+
headers
135+
}
136+
112137
pub fn set_proxy_url(&mut self, proxy_url: Option<String>) {
113138
self.proxy_url = proxy_url;
114139
}
@@ -363,10 +388,14 @@ impl Default for Configuration {
363388
},
364389
);
365390

391+
// DD_PAT env var enables Bearer token auth (mutually exclusive with API key auth)
392+
let pat = env::var("DD_PAT").ok().filter(|p| !p.is_empty());
393+
366394
Self {
367395
user_agent: DEFAULT_USER_AGENT.clone(),
368396
unstable_operations,
369397
auth_keys,
398+
pat,
370399
server_index: 0,
371400
server_variables: HashMap::from([(
372401
"site".into(),
@@ -1128,3 +1157,138 @@ lazy_static! {
11281157
])
11291158
};
11301159
}
1160+
1161+
#[cfg(test)]
1162+
mod tests {
1163+
use super::*;
1164+
use std::sync::Mutex;
1165+
1166+
// Mutex to prevent env var tests from interfering with each other
1167+
static ENV_MUTEX: Mutex<()> = Mutex::new(());
1168+
1169+
#[test]
1170+
fn test_set_pat_stores_pat() {
1171+
let mut config = Configuration::new();
1172+
config.set_pat("my-pat-token".to_string());
1173+
assert_eq!(config.pat.as_deref(), Some("my-pat-token"));
1174+
}
1175+
1176+
#[test]
1177+
fn test_auth_headers_with_pat_returns_bearer() {
1178+
let mut config = Configuration::new();
1179+
config.set_pat("my-pat-token".to_string());
1180+
1181+
let headers = config.auth_headers();
1182+
assert_eq!(headers.len(), 1);
1183+
assert_eq!(headers[0].0, "Authorization");
1184+
assert_eq!(headers[0].1, "Bearer my-pat-token");
1185+
}
1186+
1187+
#[test]
1188+
fn test_auth_headers_with_pat_excludes_api_keys() {
1189+
let mut config = Configuration::new();
1190+
config.set_auth_key(
1191+
"apiKeyAuth",
1192+
APIKey {
1193+
key: "my-api-key".to_string(),
1194+
prefix: "".to_string(),
1195+
},
1196+
);
1197+
config.set_auth_key(
1198+
"appKeyAuth",
1199+
APIKey {
1200+
key: "my-app-key".to_string(),
1201+
prefix: "".to_string(),
1202+
},
1203+
);
1204+
// PAT overrides all key-based auth
1205+
config.set_pat("my-pat-token".to_string());
1206+
1207+
let headers = config.auth_headers();
1208+
assert_eq!(headers.len(), 1);
1209+
assert_eq!(headers[0].0, "Authorization");
1210+
assert_eq!(headers[0].1, "Bearer my-pat-token");
1211+
}
1212+
1213+
#[test]
1214+
fn test_auth_headers_without_pat_returns_api_keys() {
1215+
let _lock = ENV_MUTEX.lock().unwrap();
1216+
let old_pat = env::var("DD_PAT").ok();
1217+
env::remove_var("DD_PAT");
1218+
1219+
let mut config = Configuration::new();
1220+
config.set_auth_key(
1221+
"apiKeyAuth",
1222+
APIKey {
1223+
key: "my-api-key".to_string(),
1224+
prefix: "".to_string(),
1225+
},
1226+
);
1227+
config.set_auth_key(
1228+
"appKeyAuth",
1229+
APIKey {
1230+
key: "my-app-key".to_string(),
1231+
prefix: "".to_string(),
1232+
},
1233+
);
1234+
1235+
let headers = config.auth_headers();
1236+
assert!(headers.iter().any(|(k, v)| k == "DD-API-KEY" && v == "my-api-key"));
1237+
assert!(headers.iter().any(|(k, v)| k == "DD-APPLICATION-KEY" && v == "my-app-key"));
1238+
// No Authorization header should be present
1239+
assert!(!headers.iter().any(|(k, _)| k == "Authorization"));
1240+
1241+
match old_pat {
1242+
Some(v) => env::set_var("DD_PAT", v),
1243+
None => env::remove_var("DD_PAT"),
1244+
}
1245+
}
1246+
1247+
#[test]
1248+
fn test_dd_pat_env_var() {
1249+
let _lock = ENV_MUTEX.lock().unwrap();
1250+
let old_pat = env::var("DD_PAT").ok();
1251+
1252+
env::set_var("DD_PAT", "env-pat-token");
1253+
1254+
let config = Configuration::default();
1255+
assert_eq!(config.pat.as_deref(), Some("env-pat-token"));
1256+
1257+
let headers = config.auth_headers();
1258+
assert_eq!(headers.len(), 1);
1259+
assert_eq!(headers[0].0, "Authorization");
1260+
assert_eq!(headers[0].1, "Bearer env-pat-token");
1261+
1262+
// Restore env
1263+
match old_pat {
1264+
Some(v) => env::set_var("DD_PAT", v),
1265+
None => env::remove_var("DD_PAT"),
1266+
}
1267+
}
1268+
1269+
#[test]
1270+
fn test_empty_dd_pat_does_not_set_pat() {
1271+
let _lock = ENV_MUTEX.lock().unwrap();
1272+
let old_pat = env::var("DD_PAT").ok();
1273+
let old_app_key = env::var("DD_APP_KEY").ok();
1274+
1275+
env::set_var("DD_PAT", "");
1276+
env::set_var("DD_APP_KEY", "my-app-key");
1277+
1278+
let config = Configuration::default();
1279+
assert!(config.pat.is_none());
1280+
1281+
let headers = config.auth_headers();
1282+
assert!(headers.iter().any(|(k, v)| k == "DD-APPLICATION-KEY" && v == "my-app-key"));
1283+
1284+
// Restore env
1285+
match old_pat {
1286+
Some(v) => env::set_var("DD_PAT", v),
1287+
None => env::remove_var("DD_PAT"),
1288+
}
1289+
match old_app_key {
1290+
Some(v) => env::set_var("DD_APP_KEY", v),
1291+
None => env::remove_var("DD_APP_KEY"),
1292+
}
1293+
}
1294+
}

src/datadogV1/api/api_authentication.rs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -143,13 +143,14 @@ impl AuthenticationAPI {
143143
};
144144

145145
// build auth
146-
if let Some(local_key) = local_configuration.auth_keys.get("apiKeyAuth") {
146+
for (key, value) in local_configuration.auth_headers() {
147147
headers.insert(
148-
"DD-API-KEY",
149-
HeaderValue::from_str(local_key.key.as_str())
150-
.expect("failed to parse DD-API-KEY header"),
148+
reqwest::header::HeaderName::from_bytes(key.as_bytes())
149+
.expect("failed to parse auth header name"),
150+
HeaderValue::from_str(value.as_str())
151+
.expect("failed to parse auth header value"),
151152
);
152-
};
153+
}
153154

154155
local_req_builder = local_req_builder.headers(headers);
155156
let local_req = local_req_builder.build()?;

0 commit comments

Comments
 (0)