Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 7 additions & 8 deletions src/commands/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -870,13 +870,12 @@ 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/v2/test-oauth-excluded/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 a test fixture entry (permanently excluded) so this test doesn't
/// churn when real endpoints gain OAuth support.
#[tokio::test]
async fn test_api_oauth_excluded_uses_api_keys() {
let _lock = lock_env().await;
Expand All @@ -886,7 +885,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/v2/test-oauth-excluded/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 @@ -899,7 +898,7 @@ mod tests {

let result = super::run(
&cfg,
"v2/fleet/agents",
"v2/test-oauth-excluded/some-id",
"GET",
&[],
&[],
Expand Down Expand Up @@ -927,7 +926,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/v2/test-oauth-excluded/some-id")
.match_query(mockito::Matcher::Any)
.match_header("DD-API-KEY", "test-api-key")
.match_header("authorization", mockito::Matcher::Missing)
Expand All @@ -938,7 +937,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/v2/test-oauth-excluded/some-id", server.url());
let result = super::run(
&cfg,
&absolute,
Expand Down
206 changes: 33 additions & 173 deletions src/raw_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,12 @@ 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] = &[
// Test fixture (1) — not a real API; exists only so tests that need a
// "still excluded" example don't churn when real endpoints gain OAuth.
EndpointRequirement {
path: "/api/v2/test-oauth-excluded/",
method: "GET",
},
// DDSQL editor tools (3)
EndpointRequirement {
path: "/api/unstable/ddsql-editor/tools/ddsql-docs",
Expand All @@ -133,162 +139,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",
},
// Observability Pipelines (6) — API key only, no OAuth support
EndpointRequirement {
path: "/api/v2/obs-pipelines/pipelines",
method: "GET",
},
EndpointRequirement {
path: "/api/v2/obs-pipelines/pipelines",
method: "POST",
},
EndpointRequirement {
path: "/api/v2/obs-pipelines/pipelines/",
method: "GET",
},
EndpointRequirement {
path: "/api/v2/obs-pipelines/pipelines/",
method: "PUT",
},
EndpointRequirement {
path: "/api/v2/obs-pipelines/pipelines/",
method: "DELETE",
},
EndpointRequirement {
path: "/api/v2/obs-pipelines/pipelines/validate",
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)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Restore the Observability Pipelines exclusions

When an OAuth-authenticated user with API/application keys runs pup obs-pipelines diff, src/commands/obs_pipelines.rs calls raw_get for /api/v2/obs-pipelines/pipelines/{id}; removing this endpoint group makes apply_auth send the bearer token instead of the required keys, causing the request to be rejected. The documented rollout covers Fleet, Cost/Billing, and CCM—not Observability Pipelines, whose six entries were explicitly marked API-key-only—so restore those exclusions and their behavioral coverage. This unrelated deletion also exceeds the stated scope of the change.

AGENTS.md reference: AGENTS.md:L21-L27

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Cover the cost and CCM auth-routing changes

The commit removes the Cost/Billing and Cloud Cost Management entries, but the only new no-fallback assertions exercise Fleet; a repo-wide search finds no test proving that representative /api/v2/usage/* and /api/v2/cost/* requests now attach an OAuth bearer rather than falling back to API/application keys. Add header-level happy-path coverage for these endpoint groups, along with the applicable missing-auth failure case, so method/path mistakes in this security-sensitive table cannot pass unnoticed.

AGENTS.md reference: AGENTS.md:L21-L22

Useful? React with 👍 / 👎.

// No OAuth scope is declared for Continuous Profiler endpoints; force API-key auth.
EndpointRequirement {
Expand Down Expand Up @@ -818,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 test fixture entry (permanently excluded) as the example.
assert!(requires_api_key_fallback(
"GET",
"/api/v2/fleet/agents/agent-123"
"/api/v2/test-oauth-excluded/some-id"
));
}

Expand All @@ -834,11 +685,6 @@ mod tests {
));
}

#[test]
fn test_oauth_excluded_count() {
assert_eq!(OAUTH_EXCLUDED_ENDPOINTS.len(), 46);
}

#[test]
fn test_no_fallback_for_notebooks() {
assert!(!requires_api_key_fallback("GET", "/api/v1/notebooks"));
Expand All @@ -847,12 +693,27 @@ 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]
Expand Down Expand Up @@ -1016,16 +877,15 @@ 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 test fixture entry (permanently excluded) so this test
// doesn't churn when real endpoints gain OAuth support.
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/v2/test-oauth-excluded/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/v2/test-oauth-excluded/some-id") {
Ok(_) => panic!("test fixture should require both keys"),
Err(err) => err,
};
assert!(err.to_string().contains("DD_API_KEY and DD_APP_KEY"));
Expand Down