Skip to content
14 changes: 6 additions & 8 deletions src/commands/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -872,13 +872,11 @@ mod tests {
cleanup_env();
}

/// OAuth-excluded endpoints (e.g. GET /api/v2/fleet/agents) must use API-key
/// OAuth-excluded endpoints (e.g. GET /api/unstable/fleet/some-id) must use API-key
/// auth even when a bearer token is present. This exercises the reuse of
/// raw_client::apply_auth's per-endpoint fallback table.
///
/// Fleet Automation is just today's example of a still-excluded endpoint,
/// not a claim it's meant to stay that way -- update this test if/when
/// Fleet gets OAuth support too.
/// Uses the still-excluded unstable Fleet entry as the example.
#[tokio::test]
async fn test_api_oauth_excluded_uses_api_keys() {
let _lock = lock_env().await;
Expand All @@ -888,7 +886,7 @@ mod tests {
// must prefer the API keys.
cfg.access_token = Some("bearer-token".into());
let _mock = server
.mock("GET", "/api/v2/fleet/agents")
.mock("GET", "/api/unstable/fleet/some-id")
.match_query(mockito::Matcher::Any)
.match_header("DD-API-KEY", "test-api-key")
.match_header("DD-APPLICATION-KEY", "test-app-key")
Expand All @@ -901,7 +899,7 @@ mod tests {

let result = super::run(
&cfg,
"v2/fleet/agents",
"unstable/fleet/some-id",
"GET",
&[],
&[],
Expand Down Expand Up @@ -929,7 +927,7 @@ mod tests {
let mut cfg = test_config(&server.url());
cfg.access_token = Some("bearer-token".into());
let _mock = server
.mock("GET", "/api/v2/fleet/agents")
.mock("GET", "/api/unstable/fleet/some-id")
.match_query(mockito::Matcher::Any)
.match_header("DD-API-KEY", "test-api-key")
.match_header("authorization", mockito::Matcher::Missing)
Expand All @@ -940,7 +938,7 @@ mod tests {
.await;

// Pass the fully-qualified URL, not a relative path.
let absolute = format!("{}/api/v2/fleet/agents", server.url());
let absolute = format!("{}/api/unstable/fleet/some-id", server.url());
let result = super::run(
&cfg,
&absolute,
Expand Down
232 changes: 89 additions & 143 deletions src/raw_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,13 @@ fn find_endpoint_requirement(method: &str, path: &str) -> Option<&'static Endpoi
/// Endpoints that don't support OAuth.
/// Trailing "/" means prefix match for ID-parameterized paths.
static OAUTH_EXCLUDED_ENDPOINTS: &[EndpointRequirement] = &[
// Fleet Automation unstable surface — doesn't support OAuth server-side
// yet. Current status, not a permanent contract; delete this entry (and
// the tests referencing it) once it does, rather than patching forward.
EndpointRequirement {
path: "/api/unstable/fleet/",
method: "GET",
},
// DDSQL editor tools (3)
EndpointRequirement {
path: "/api/unstable/ddsql-editor/tools/ddsql-docs",
Expand All @@ -157,137 +164,6 @@ static OAUTH_EXCLUDED_ENDPOINTS: &[EndpointRequirement] = &[
path: "/api/unstable/ddsql-editor/tools/table-data",
method: "POST",
},
// Fleet Automation (15)
EndpointRequirement {
path: "/api/v2/fleet/agents",
method: "GET",
},
EndpointRequirement {
path: "/api/v2/fleet/agents/",
method: "GET",
},
EndpointRequirement {
path: "/api/v2/fleet/agents/versions",
method: "GET",
},
EndpointRequirement {
path: "/api/v2/fleet/deployments",
method: "GET",
},
EndpointRequirement {
path: "/api/v2/fleet/deployments/",
method: "GET",
},
EndpointRequirement {
path: "/api/v2/fleet/deployments/configure",
method: "POST",
},
EndpointRequirement {
path: "/api/v2/fleet/deployments/upgrade",
method: "POST",
},
EndpointRequirement {
path: "/api/v2/fleet/deployments/",
method: "POST",
},
EndpointRequirement {
path: "/api/v2/fleet/deployments/",
method: "DELETE",
},
EndpointRequirement {
path: "/api/v2/fleet/schedules",
method: "GET",
},
EndpointRequirement {
path: "/api/v2/fleet/schedules/",
method: "GET",
},
EndpointRequirement {
path: "/api/v2/fleet/schedules",
method: "POST",
},
EndpointRequirement {
path: "/api/v2/fleet/schedules/",
method: "PATCH",
},
EndpointRequirement {
path: "/api/v2/fleet/schedules/",
method: "DELETE",
},
EndpointRequirement {
path: "/api/v2/fleet/schedules/",
method: "POST",
},
// Cost / Billing (11) — API key only, no OAuth support
EndpointRequirement {
path: "/api/v2/usage/projected_cost",
method: "GET",
},
EndpointRequirement {
path: "/api/v2/usage/cost_by_org",
method: "GET",
},
EndpointRequirement {
path: "/api/v2/cost_by_tag/monthly_cost_attribution",
method: "GET",
},
// Cloud Cost Management config (12)
EndpointRequirement {
path: "/api/v2/cost/aws_cur_config",
method: "GET",
},
EndpointRequirement {
path: "/api/v2/cost/aws_cur_config",
method: "POST",
},
EndpointRequirement {
path: "/api/v2/cost/aws_cur_config/",
method: "GET",
},
EndpointRequirement {
path: "/api/v2/cost/aws_cur_config/",
method: "DELETE",
},
EndpointRequirement {
path: "/api/v2/cost/azure_uc_config",
method: "GET",
},
EndpointRequirement {
path: "/api/v2/cost/azure_uc_config",
method: "POST",
},
EndpointRequirement {
path: "/api/v2/cost/azure_uc_config/",
method: "GET",
},
EndpointRequirement {
path: "/api/v2/cost/azure_uc_config/",
method: "DELETE",
},
EndpointRequirement {
path: "/api/v2/cost/gcp_uc_config",
method: "GET",
},
EndpointRequirement {
path: "/api/v2/cost/gcp_uc_config",
method: "POST",
},
EndpointRequirement {
path: "/api/v2/cost/gcp_uc_config/",
method: "GET",
},
EndpointRequirement {
path: "/api/v2/cost/gcp_uc_config/",
method: "DELETE",
},
EndpointRequirement {
path: "/api/v2/cost/oci_config",
method: "GET",
},
EndpointRequirement {
path: "/api/v2/cost/anomalies",
method: "GET",
},
// Profiling (4)
Comment thread
srosenthal-dd marked this conversation as resolved.
Comment thread
srosenthal-dd marked this conversation as resolved.
// No OAuth scope is declared for Continuous Profiler endpoints; force API-key auth.
EndpointRequirement {
Expand Down Expand Up @@ -792,10 +668,11 @@ mod tests {

#[test]
fn test_prefix_matching_with_id() {
// Trailing "/" in the pattern should match paths with IDs
// Trailing "/" in the pattern should match paths with IDs.
// Uses the still-excluded unstable Fleet entry as the example.
assert!(requires_api_key_fallback(
"GET",
"/api/v2/fleet/agents/agent-123"
"/api/unstable/fleet/some-id"
));
}

Expand Down Expand Up @@ -880,12 +757,83 @@ mod tests {
}

#[test]
fn test_requires_api_key_fallback_fleet() {
assert!(requires_api_key_fallback("GET", "/api/v2/fleet/agents"));
assert!(requires_api_key_fallback(
fn test_no_fallback_for_fleet() {
// Fleet Automation v2 routes already accept OAuth server-side;
// the raw/generic `pup api` passthrough should use the OAuth bearer
// like the typed fleet commands do.
assert!(!requires_api_key_fallback("GET", "/api/v2/fleet/agents"));
assert!(!requires_api_key_fallback(
"GET",
"/api/v2/fleet/agents/agent-123"
));
assert!(!requires_api_key_fallback(
"GET",
"/api/v2/fleet/deployments"
));
assert!(!requires_api_key_fallback(
"POST",
"/api/v2/fleet/deployments/configure"
));
assert!(!requires_api_key_fallback(
"POST",
"/api/v2/fleet/schedules/sched-123/trigger"
));
}

#[test]
fn test_no_fallback_for_cost_billing() {
// Cost/Billing routes already accept OAuth server-side (DAL-959); the
// raw/generic `pup api` passthrough should use the OAuth bearer
// instead of forcing API-key fallback.
assert!(!requires_api_key_fallback(
"GET",
"/api/v2/usage/projected_cost"
));
assert!(!requires_api_key_fallback(
"GET",
"/api/v2/usage/cost_by_org"
));
assert!(!requires_api_key_fallback(
"GET",
"/api/v2/cost_by_tag/monthly_cost_attribution"
));
}

#[test]
fn test_no_fallback_for_ccm() {
// Cloud Cost Management config routes already accept OAuth
// server-side (DAL-959); the raw/generic `pup api` passthrough
// should use the OAuth bearer instead of forcing API-key fallback.
assert!(!requires_api_key_fallback(
"GET",
"/api/v2/cost/aws_cur_config"
));
assert!(!requires_api_key_fallback(
"POST",
"/api/v2/cost/aws_cur_config"
));
assert!(!requires_api_key_fallback(
"DELETE",
"/api/v2/cost/aws_cur_config/config-123"
));
assert!(!requires_api_key_fallback(
"GET",
"/api/v2/cost/azure_uc_config"
));
assert!(!requires_api_key_fallback(
"DELETE",
"/api/v2/cost/azure_uc_config/config-123"
));
assert!(!requires_api_key_fallback(
"GET",
"/api/v2/cost/gcp_uc_config"
));
assert!(!requires_api_key_fallback(
"DELETE",
"/api/v2/cost/gcp_uc_config/config-123"
));
assert!(!requires_api_key_fallback("GET", "/api/v2/cost/oci_config"));
assert!(!requires_api_key_fallback("GET", "/api/v2/cost/anomalies"));
}

#[test]
Expand Down Expand Up @@ -1049,16 +997,14 @@ mod tests {

#[test]
fn test_other_oauth_excluded_endpoints_still_require_both_keys() {
// Uses Fleet Automation as a currently-still-excluded example. This is
// just today's state of OAUTH_EXCLUDED_ENDPOINTS, not a claim that Fleet
// (or anything else in the table) is meant to stay that way -- update
// this example if/when its entries get OAuth support and are removed.
// Uses the still-excluded unstable Fleet entry as the example.
let mut cfg = test_cfg();
cfg.app_key = None;
let req = reqwest::Client::new().get("https://api.datadoghq.com/api/v2/fleet/agents");
let req =
reqwest::Client::new().get("https://api.datadoghq.com/api/unstable/fleet/some-id");

let err = match apply_auth(req, &cfg, "GET", "/api/v2/fleet/agents") {
Ok(_) => panic!("Fleet Automation should require both keys"),
let err = match apply_auth(req, &cfg, "GET", "/api/unstable/fleet/some-id") {
Ok(_) => panic!("excluded endpoint should require both keys"),
Err(err) => err,
};
assert!(err.to_string().contains("DD_API_KEY and DD_APP_KEY"));
Expand Down