diff --git a/src/commands/api.rs b/src/commands/api.rs index 581599c0..0b1603f5 100644 --- a/src/commands/api.rs +++ b/src/commands/api.rs @@ -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; @@ -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") @@ -901,7 +899,7 @@ mod tests { let result = super::run( &cfg, - "v2/fleet/agents", + "unstable/fleet/some-id", "GET", &[], &[], @@ -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) @@ -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, diff --git a/src/raw_client.rs b/src/raw_client.rs index ec50ef1f..f09b19b0 100644 --- a/src/raw_client.rs +++ b/src/raw_client.rs @@ -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", @@ -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) // No OAuth scope is declared for Continuous Profiler endpoints; force API-key auth. EndpointRequirement { @@ -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" )); } @@ -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] @@ -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"));