Skip to content

Commit 8013ba4

Browse files
committed
CRED-2150: Add PAT auth support to Rust API client
1 parent f2e7ab9 commit 8013ba4

File tree

134 files changed

+6806
-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

+6806
-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: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ 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+
{%- if "bearerAuth" in openapi.components.securitySchemes %}
48+
pub(crate) pat: Option<String>,
49+
{%- endif %}
4750
pub server_index: usize,
4851
pub server_variables: HashMap<String, String>,
4952
pub server_operation_index: HashMap<String, usize>,
@@ -106,6 +109,45 @@ impl Configuration {
106109
self.auth_keys.insert(operation_str.to_string(), api_key);
107110
}
108111

112+
{%- if "bearerAuth" in openapi.components.securitySchemes %}
113+
114+
/// Set a bearer token for authentication.
115+
/// When a bearer token is configured, the client sends only an `Authorization: Bearer <token>`
116+
/// header and does NOT send DD-API-KEY or DD-APPLICATION-KEY headers.
117+
pub fn set_pat(&mut self, pat: String) {
118+
self.pat = Some(pat);
119+
}
120+
121+
{%- endif %}
122+
123+
/// Build authentication headers for an API request.
124+
{%- if "bearerAuth" in openapi.components.securitySchemes %}
125+
/// If a bearer token is configured, returns a single `Authorization: Bearer` header.
126+
/// Otherwise, returns API key headers.
127+
{%- endif %}
128+
pub fn auth_headers(&self) -> Vec<(String, String)> {
129+
{%- if "bearerAuth" in openapi.components.securitySchemes %}
130+
if let Some(ref pat) = self.pat {
131+
return vec![("Authorization".to_string(), format!("Bearer {}", pat))];
132+
}
133+
{%- endif %}
134+
let mut headers = Vec::new();
135+
{%- set authMethods = openapi.security %}
136+
{%- if authMethods %}
137+
{%- for authMethod in authMethods %}
138+
{%- for name in authMethod %}
139+
{%- set schema = openapi.components.securitySchemes[name] %}
140+
{%- if schema.type == "apiKey" and schema.in != "cookie" %}
141+
if let Some(key) = self.auth_keys.get("{{ name }}") {
142+
headers.push(("{{ schema.name }}".to_string(), key.key.clone()));
143+
}
144+
{%- endif %}
145+
{%- endfor %}
146+
{%- endfor %}
147+
{%- endif %}
148+
headers
149+
}
150+
109151
pub fn set_proxy_url(&mut self, proxy_url: Option<String>) {
110152
self.proxy_url = proxy_url;
111153
}
@@ -149,10 +191,20 @@ impl Default for Configuration {
149191
{%- endfor %}
150192
{%- endif %}
151193

194+
{%- if "bearerAuth" in openapi.components.securitySchemes %}
195+
{%- set bearerEnvName = openapi.components.securitySchemes.bearerAuth["x-env-name"] %}
196+
197+
// {{ bearerEnvName }} env var enables Bearer token auth (mutually exclusive with API key auth)
198+
let pat = env::var("{{ bearerEnvName }}").ok().filter(|p| !p.is_empty());
199+
{%- endif %}
200+
152201
Self {
153202
user_agent: DEFAULT_USER_AGENT.clone(),
154203
unstable_operations,
155204
auth_keys,
205+
{%- if "bearerAuth" in openapi.components.securitySchemes %}
206+
pat,
207+
{%- endif %}
156208
server_index: 0,
157209
server_variables: HashMap::from([(
158210
"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 bearer token for authentication.
114+
/// When a bearer token is configured, the client sends only an `Authorization: Bearer <token>`
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 bearer token is configured, returns a single `Authorization: Bearer` header.
122+
/// Otherwise, returns API 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_BEARER_TOKEN env var enables Bearer token auth (mutually exclusive with API key auth)
392+
let pat = env::var("DD_BEARER_TOKEN").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_BEARER_TOKEN").ok();
1217+
env::remove_var("DD_BEARER_TOKEN");
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_BEARER_TOKEN", v),
1243+
None => env::remove_var("DD_BEARER_TOKEN"),
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_BEARER_TOKEN").ok();
1251+
1252+
env::set_var("DD_BEARER_TOKEN", "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_BEARER_TOKEN", v),
1265+
None => env::remove_var("DD_BEARER_TOKEN"),
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_BEARER_TOKEN").ok();
1273+
let old_app_key = env::var("DD_APP_KEY").ok();
1274+
1275+
env::set_var("DD_BEARER_TOKEN", "");
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_BEARER_TOKEN", v),
1287+
None => env::remove_var("DD_BEARER_TOKEN"),
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)