From 57403fcb5c4c6d1e9863779e9dcad9d002a7f4ae Mon Sep 17 00:00:00 2001 From: Stephen Rosenthal Date: Tue, 3 Mar 2026 14:46:02 -0800 Subject: [PATCH 1/6] fix(auth): add OAuth scopes for integrations, organizations, security, events, HAMR Add missing OAuth scopes so these command domains work with bearer token auth instead of requiring API key fallback. New scopes: integrations_read, manage_integrations, org_management, security_monitoring_filters_write, security_monitoring_rules_write, logs_modify_indexes, disaster_recovery_status_read, disaster_recovery_status_write. Also enables events search for OAuth by removing the forced API key requirement and the OAUTH_EXCLUDED_ENDPOINTS entry. Co-Authored-By: Claude Opus 4.6 --- src/auth/types.rs | 13 +++++++++++++ src/client.rs | 11 +++-------- src/commands/events.rs | 15 +++++---------- 3 files changed, 21 insertions(+), 18 deletions(-) diff --git a/src/auth/types.rs b/src/auth/types.rs index bed5ad27..f4f8b38d 100644 --- a/src/auth/types.rs +++ b/src/auth/types.rs @@ -91,6 +91,19 @@ pub fn default_scopes() -> Vec<&'static str> { "oci_configurations_manage", "timeseries_query", "usage_read", + // Integrations (Jira, ServiceNow, Slack, Webhooks) + "integrations_read", + "manage_integrations", + // Organizations + "org_management", + // Security Monitoring (filters + rules write) + "security_monitoring_filters_write", + "security_monitoring_rules_write", + // Logs index management + "logs_modify_indexes", + // HAMR (disaster recovery) + "disaster_recovery_status_read", + "disaster_recovery_status_write", ] } diff --git a/src/client.rs b/src/client.rs index e48d72b9..069badff 100644 --- a/src/client.rs +++ b/src/client.rs @@ -342,11 +342,6 @@ static OAUTH_EXCLUDED_ENDPOINTS: &[EndpointRequirement] = &[ path: "/api/v2/application_keys/", method: "DELETE", }, - // Events (1) - EndpointRequirement { - path: "/api/v2/events/search", - method: "POST", - }, // Fleet Automation (15) EndpointRequirement { path: "/api/v2/fleet/agents", @@ -566,8 +561,8 @@ mod tests { } #[test] - fn test_requires_api_key_fallback_events() { - assert!(requires_api_key_fallback("POST", "/api/v2/events/search")); + fn test_no_fallback_for_events_search() { + assert!(!requires_api_key_fallback("POST", "/api/v2/events/search")); } #[test] @@ -606,7 +601,7 @@ mod tests { #[test] fn test_oauth_excluded_count() { - assert_eq!(OAUTH_EXCLUDED_ENDPOINTS.len(), 40); + assert_eq!(OAUTH_EXCLUDED_ENDPOINTS.len(), 39); } #[test] diff --git a/src/commands/events.rs b/src/commands/events.rs index 52a1b192..ec9ef2ab 100644 --- a/src/commands/events.rs +++ b/src/commands/events.rs @@ -1,4 +1,4 @@ -use anyhow::{bail, Result}; +use anyhow::Result; #[cfg(not(target_arch = "wasm32"))] use datadog_api_client::datadogV1::api_events::{ EventsAPI as EventsV1API, ListEventsOptionalParams, @@ -65,16 +65,11 @@ pub async fn search( to: String, limit: i32, ) -> Result<()> { - // Events search is OAuth-excluded — require API keys - if !cfg.has_api_keys() { - bail!( - "events search requires API key authentication (DD_API_KEY + DD_APP_KEY).\n\ - This endpoint does not support bearer token auth." - ); - } - let dd_cfg = client::make_dd_config(cfg); - let api = EventsV2API::with_config(dd_cfg); + let api = match client::make_bearer_client(cfg) { + Some(c) => EventsV2API::with_client_and_config(dd_cfg, c), + None => EventsV2API::with_config(dd_cfg), + }; let from_ms = util::parse_time_to_unix_millis(&from)?; let to_ms = util::parse_time_to_unix_millis(&to)?; From ec78d5e9ba0b731afff85ef250c6b301b6ddd9e3 Mon Sep 17 00:00:00 2001 From: Stephen Rosenthal Date: Tue, 3 Mar 2026 14:52:03 -0800 Subject: [PATCH 2/6] fix(auth): enable OAuth for RUM and Notebooks commands Remove RUM and Notebooks from OAUTH_EXCLUDED_ENDPOINTS and replace all forced API key bail blocks with bearer client middleware in rum.rs. New scopes: notebooks_read, notebooks_write, rum_generate_metrics, rum_session_replay_read (rum_apps_read/write and rum_retention_filters_read/write were already present). Co-Authored-By: Claude Opus 4.6 --- src/auth/types.rs | 6 ++ src/client.rs | 86 ++++---------------------- src/commands/rum.rs | 147 ++++++++++++++++++++++---------------------- 3 files changed, 91 insertions(+), 148 deletions(-) diff --git a/src/auth/types.rs b/src/auth/types.rs index f4f8b38d..bca76cef 100644 --- a/src/auth/types.rs +++ b/src/auth/types.rs @@ -104,6 +104,12 @@ pub fn default_scopes() -> Vec<&'static str> { // HAMR (disaster recovery) "disaster_recovery_status_read", "disaster_recovery_status_write", + // Notebooks + "notebooks_read", + "notebooks_write", + // RUM + "rum_generate_metrics", + "rum_session_replay_read", ] } diff --git a/src/client.rs b/src/client.rs index 069badff..56befd65 100644 --- a/src/client.rs +++ b/src/client.rs @@ -264,47 +264,6 @@ fn find_endpoint_requirement(method: &str, path: &str) -> Option<&'static Endpoi /// Trailing "/" means prefix match for ID-parameterized paths. #[cfg(not(target_arch = "wasm32"))] static OAUTH_EXCLUDED_ENDPOINTS: &[EndpointRequirement] = &[ - // RUM API (10) - EndpointRequirement { - path: "/api/v2/rum/applications", - method: "GET", - }, - EndpointRequirement { - path: "/api/v2/rum/applications/", - method: "GET", - }, - EndpointRequirement { - path: "/api/v2/rum/applications", - method: "POST", - }, - EndpointRequirement { - path: "/api/v2/rum/applications/", - method: "PATCH", - }, - EndpointRequirement { - path: "/api/v2/rum/applications/", - method: "DELETE", - }, - EndpointRequirement { - path: "/api/v2/rum/metrics", - method: "GET", - }, - EndpointRequirement { - path: "/api/v2/rum/metrics/", - method: "GET", - }, - EndpointRequirement { - path: "/api/v2/rum/retention_filters", - method: "GET", - }, - EndpointRequirement { - path: "/api/v2/rum/retention_filters/", - method: "GET", - }, - EndpointRequirement { - path: "/api/v2/rum/events/search", - method: "POST", - }, // API/App Keys (8) EndpointRequirement { path: "/api/v2/api_keys", @@ -403,27 +362,6 @@ static OAUTH_EXCLUDED_ENDPOINTS: &[EndpointRequirement] = &[ path: "/api/v2/fleet/schedules/", method: "POST", }, - // Notebooks (5) - EndpointRequirement { - path: "/api/v1/notebooks", - method: "GET", - }, - EndpointRequirement { - path: "/api/v1/notebooks", - method: "POST", - }, - EndpointRequirement { - path: "/api/v1/notebooks/", - method: "GET", - }, - EndpointRequirement { - path: "/api/v1/notebooks/", - method: "PUT", - }, - EndpointRequirement { - path: "/api/v1/notebooks/", - method: "DELETE", - }, ]; // --------------------------------------------------------------------------- @@ -552,9 +490,9 @@ mod tests { } #[test] - fn test_requires_api_key_fallback_rum() { - assert!(requires_api_key_fallback("GET", "/api/v2/rum/applications")); - assert!(requires_api_key_fallback( + fn test_no_fallback_for_rum() { + assert!(!requires_api_key_fallback("GET", "/api/v2/rum/applications")); + assert!(!requires_api_key_fallback( "GET", "/api/v2/rum/applications/abc-123" )); @@ -575,14 +513,14 @@ mod tests { #[test] fn test_prefix_matching_with_id() { // Trailing "/" in the pattern should match paths with IDs - assert!(requires_api_key_fallback( - "GET", - "/api/v2/rum/applications/some-uuid-here" - )); assert!(requires_api_key_fallback( "DELETE", "/api/v2/api_keys/key-123" )); + assert!(requires_api_key_fallback( + "GET", + "/api/v2/fleet/agents/agent-123" + )); } #[test] @@ -601,7 +539,7 @@ mod tests { #[test] fn test_oauth_excluded_count() { - assert_eq!(OAUTH_EXCLUDED_ENDPOINTS.len(), 39); + assert_eq!(OAUTH_EXCLUDED_ENDPOINTS.len(), 24); } #[test] @@ -670,10 +608,10 @@ mod tests { } #[test] - fn test_requires_api_key_fallback_notebooks() { - assert!(requires_api_key_fallback("GET", "/api/v1/notebooks")); - assert!(requires_api_key_fallback("GET", "/api/v1/notebooks/12345")); - assert!(requires_api_key_fallback("POST", "/api/v1/notebooks")); + fn test_no_fallback_for_notebooks() { + assert!(!requires_api_key_fallback("GET", "/api/v1/notebooks")); + assert!(!requires_api_key_fallback("GET", "/api/v1/notebooks/12345")); + assert!(!requires_api_key_fallback("POST", "/api/v1/notebooks")); } #[test] diff --git a/src/commands/rum.rs b/src/commands/rum.rs index bb4186a7..1535c310 100644 --- a/src/commands/rum.rs +++ b/src/commands/rum.rs @@ -1,4 +1,4 @@ -use anyhow::{bail, Result}; +use anyhow::Result; #[cfg(not(target_arch = "wasm32"))] use datadog_api_client::datadogV2::api_rum::{ListRUMEventsOptionalParams, RUMAPI}; #[cfg(not(target_arch = "wasm32"))] @@ -30,12 +30,11 @@ use crate::util; #[cfg(not(target_arch = "wasm32"))] pub async fn apps_list(cfg: &Config) -> Result<()> { - // RUM apps is OAuth-excluded — require API keys - if !cfg.has_api_keys() { - bail!("RUM apps requires API key authentication (DD_API_KEY + DD_APP_KEY)"); - } let dd_cfg = client::make_dd_config(cfg); - let api = RUMAPI::with_config(dd_cfg); + let api = match client::make_bearer_client(cfg) { + Some(c) => RUMAPI::with_client_and_config(dd_cfg, c), + None => RUMAPI::with_config(dd_cfg), + }; let resp = api .get_rum_applications() .await @@ -51,11 +50,11 @@ pub async fn apps_list(cfg: &Config) -> Result<()> { #[cfg(not(target_arch = "wasm32"))] pub async fn apps_get(cfg: &Config, app_id: &str) -> Result<()> { - if !cfg.has_api_keys() { - bail!("RUM apps requires API key authentication (DD_API_KEY + DD_APP_KEY)"); - } let dd_cfg = client::make_dd_config(cfg); - let api = RUMAPI::with_config(dd_cfg); + let api = match client::make_bearer_client(cfg) { + Some(c) => RUMAPI::with_client_and_config(dd_cfg, c), + None => RUMAPI::with_config(dd_cfg), + }; let resp = api .get_rum_application(app_id.to_string()) .await @@ -72,11 +71,11 @@ pub async fn apps_get(cfg: &Config, app_id: &str) -> Result<()> { #[cfg(not(target_arch = "wasm32"))] pub async fn apps_create(cfg: &Config, name: &str, app_type: Option) -> Result<()> { - if !cfg.has_api_keys() { - bail!("RUM apps requires API key authentication (DD_API_KEY + DD_APP_KEY)"); - } let dd_cfg = client::make_dd_config(cfg); - let api = RUMAPI::with_config(dd_cfg); + let api = match client::make_bearer_client(cfg) { + Some(c) => RUMAPI::with_client_and_config(dd_cfg, c), + None => RUMAPI::with_config(dd_cfg), + }; let mut attrs = RUMApplicationCreateAttributes::new(name.to_string()); if let Some(t) = app_type { attrs = attrs.type_(t); @@ -108,11 +107,11 @@ pub async fn apps_create(cfg: &Config, name: &str, app_type: Option) -> #[cfg(not(target_arch = "wasm32"))] pub async fn apps_delete(cfg: &Config, app_id: &str) -> Result<()> { - if !cfg.has_api_keys() { - bail!("RUM apps requires API key authentication (DD_API_KEY + DD_APP_KEY)"); - } let dd_cfg = client::make_dd_config(cfg); - let api = RUMAPI::with_config(dd_cfg); + let api = match client::make_bearer_client(cfg) { + Some(c) => RUMAPI::with_client_and_config(dd_cfg, c), + None => RUMAPI::with_config(dd_cfg), + }; api.delete_rum_application(app_id.to_string()) .await .map_err(|e| anyhow::anyhow!("failed to delete RUM app: {e:?}"))?; @@ -239,11 +238,11 @@ pub async fn sessions_search( #[cfg(not(target_arch = "wasm32"))] pub async fn apps_update(cfg: &Config, app_id: &str, file: &str) -> Result<()> { - if !cfg.has_api_keys() { - bail!("RUM apps requires API key authentication (DD_API_KEY + DD_APP_KEY)"); - } let dd_cfg = client::make_dd_config(cfg); - let api = RUMAPI::with_config(dd_cfg); + let api = match client::make_bearer_client(cfg) { + Some(c) => RUMAPI::with_client_and_config(dd_cfg, c), + None => RUMAPI::with_config(dd_cfg), + }; let body: RUMApplicationUpdateRequest = crate::util::read_json_file(file)?; let resp = api .update_rum_application(app_id.to_string(), body) @@ -264,11 +263,11 @@ pub async fn apps_update(cfg: &Config, app_id: &str, file: &str) -> Result<()> { #[cfg(not(target_arch = "wasm32"))] pub async fn metrics_list(cfg: &Config) -> Result<()> { - if !cfg.has_api_keys() { - bail!("RUM metrics requires API key authentication (DD_API_KEY + DD_APP_KEY)"); - } let dd_cfg = client::make_dd_config(cfg); - let api = RumMetricsAPI::with_config(dd_cfg); + let api = match client::make_bearer_client(cfg) { + Some(c) => RumMetricsAPI::with_client_and_config(dd_cfg, c), + None => RumMetricsAPI::with_config(dd_cfg), + }; let resp = api .list_rum_metrics() .await @@ -284,11 +283,11 @@ pub async fn metrics_list(cfg: &Config) -> Result<()> { #[cfg(not(target_arch = "wasm32"))] pub async fn metrics_get(cfg: &Config, metric_id: &str) -> Result<()> { - if !cfg.has_api_keys() { - bail!("RUM metrics requires API key authentication (DD_API_KEY + DD_APP_KEY)"); - } let dd_cfg = client::make_dd_config(cfg); - let api = RumMetricsAPI::with_config(dd_cfg); + let api = match client::make_bearer_client(cfg) { + Some(c) => RumMetricsAPI::with_client_and_config(dd_cfg, c), + None => RumMetricsAPI::with_config(dd_cfg), + }; let resp = api .get_rum_metric(metric_id.to_string()) .await @@ -305,11 +304,11 @@ pub async fn metrics_get(cfg: &Config, metric_id: &str) -> Result<()> { #[cfg(not(target_arch = "wasm32"))] pub async fn metrics_create(cfg: &Config, file: &str) -> Result<()> { - if !cfg.has_api_keys() { - bail!("RUM metrics requires API key authentication (DD_API_KEY + DD_APP_KEY)"); - } let dd_cfg = client::make_dd_config(cfg); - let api = RumMetricsAPI::with_config(dd_cfg); + let api = match client::make_bearer_client(cfg) { + Some(c) => RumMetricsAPI::with_client_and_config(dd_cfg, c), + None => RumMetricsAPI::with_config(dd_cfg), + }; let body: RumMetricCreateRequest = crate::util::read_json_file(file)?; let resp = api .create_rum_metric(body) @@ -327,11 +326,11 @@ pub async fn metrics_create(cfg: &Config, file: &str) -> Result<()> { #[cfg(not(target_arch = "wasm32"))] pub async fn metrics_update(cfg: &Config, metric_id: &str, file: &str) -> Result<()> { - if !cfg.has_api_keys() { - bail!("RUM metrics requires API key authentication (DD_API_KEY + DD_APP_KEY)"); - } let dd_cfg = client::make_dd_config(cfg); - let api = RumMetricsAPI::with_config(dd_cfg); + let api = match client::make_bearer_client(cfg) { + Some(c) => RumMetricsAPI::with_client_and_config(dd_cfg, c), + None => RumMetricsAPI::with_config(dd_cfg), + }; let body: RumMetricUpdateRequest = crate::util::read_json_file(file)?; let resp = api .update_rum_metric(metric_id.to_string(), body) @@ -350,11 +349,11 @@ pub async fn metrics_update(cfg: &Config, metric_id: &str, file: &str) -> Result #[cfg(not(target_arch = "wasm32"))] pub async fn metrics_delete(cfg: &Config, metric_id: &str) -> Result<()> { - if !cfg.has_api_keys() { - bail!("RUM metrics requires API key authentication (DD_API_KEY + DD_APP_KEY)"); - } let dd_cfg = client::make_dd_config(cfg); - let api = RumMetricsAPI::with_config(dd_cfg); + let api = match client::make_bearer_client(cfg) { + Some(c) => RumMetricsAPI::with_client_and_config(dd_cfg, c), + None => RumMetricsAPI::with_config(dd_cfg), + }; api.delete_rum_metric(metric_id.to_string()) .await .map_err(|e| anyhow::anyhow!("failed to delete RUM metric: {e:?}"))?; @@ -374,11 +373,11 @@ pub async fn metrics_delete(cfg: &Config, metric_id: &str) -> Result<()> { #[cfg(not(target_arch = "wasm32"))] pub async fn retention_filters_list(cfg: &Config, app_id: &str) -> Result<()> { - if !cfg.has_api_keys() { - bail!("RUM retention filters requires API key authentication (DD_API_KEY + DD_APP_KEY)"); - } let dd_cfg = client::make_dd_config(cfg); - let api = RumRetentionFiltersAPI::with_config(dd_cfg); + let api = match client::make_bearer_client(cfg) { + Some(c) => RumRetentionFiltersAPI::with_client_and_config(dd_cfg, c), + None => RumRetentionFiltersAPI::with_config(dd_cfg), + }; let resp = api .list_retention_filters(app_id.to_string()) .await @@ -395,11 +394,11 @@ pub async fn retention_filters_list(cfg: &Config, app_id: &str) -> Result<()> { #[cfg(not(target_arch = "wasm32"))] pub async fn retention_filters_get(cfg: &Config, app_id: &str, filter_id: &str) -> Result<()> { - if !cfg.has_api_keys() { - bail!("RUM retention filters requires API key authentication (DD_API_KEY + DD_APP_KEY)"); - } let dd_cfg = client::make_dd_config(cfg); - let api = RumRetentionFiltersAPI::with_config(dd_cfg); + let api = match client::make_bearer_client(cfg) { + Some(c) => RumRetentionFiltersAPI::with_client_and_config(dd_cfg, c), + None => RumRetentionFiltersAPI::with_config(dd_cfg), + }; let resp = api .get_retention_filter(app_id.to_string(), filter_id.to_string()) .await @@ -416,11 +415,11 @@ pub async fn retention_filters_get(cfg: &Config, app_id: &str, filter_id: &str) #[cfg(not(target_arch = "wasm32"))] pub async fn retention_filters_create(cfg: &Config, app_id: &str, file: &str) -> Result<()> { - if !cfg.has_api_keys() { - bail!("RUM retention filters requires API key authentication (DD_API_KEY + DD_APP_KEY)"); - } let dd_cfg = client::make_dd_config(cfg); - let api = RumRetentionFiltersAPI::with_config(dd_cfg); + let api = match client::make_bearer_client(cfg) { + Some(c) => RumRetentionFiltersAPI::with_client_and_config(dd_cfg, c), + None => RumRetentionFiltersAPI::with_config(dd_cfg), + }; let body: RumRetentionFilterCreateRequest = crate::util::read_json_file(file)?; let resp = api .create_retention_filter(app_id.to_string(), body) @@ -444,11 +443,11 @@ pub async fn retention_filters_update( filter_id: &str, file: &str, ) -> Result<()> { - if !cfg.has_api_keys() { - bail!("RUM retention filters requires API key authentication (DD_API_KEY + DD_APP_KEY)"); - } let dd_cfg = client::make_dd_config(cfg); - let api = RumRetentionFiltersAPI::with_config(dd_cfg); + let api = match client::make_bearer_client(cfg) { + Some(c) => RumRetentionFiltersAPI::with_client_and_config(dd_cfg, c), + None => RumRetentionFiltersAPI::with_config(dd_cfg), + }; let body: RumRetentionFilterUpdateRequest = crate::util::read_json_file(file)?; let resp = api .update_retention_filter(app_id.to_string(), filter_id.to_string(), body) @@ -472,11 +471,11 @@ pub async fn retention_filters_update( #[cfg(not(target_arch = "wasm32"))] pub async fn retention_filters_delete(cfg: &Config, app_id: &str, filter_id: &str) -> Result<()> { - if !cfg.has_api_keys() { - bail!("RUM retention filters requires API key authentication (DD_API_KEY + DD_APP_KEY)"); - } let dd_cfg = client::make_dd_config(cfg); - let api = RumRetentionFiltersAPI::with_config(dd_cfg); + let api = match client::make_bearer_client(cfg) { + Some(c) => RumRetentionFiltersAPI::with_client_and_config(dd_cfg, c), + None => RumRetentionFiltersAPI::with_config(dd_cfg), + }; api.delete_retention_filter(app_id.to_string(), filter_id.to_string()) .await .map_err(|e| anyhow::anyhow!("failed to delete RUM retention filter: {e:?}"))?; @@ -555,11 +554,11 @@ pub async fn sessions_list(cfg: &Config, from: String, to: String, limit: i32) - #[cfg(not(target_arch = "wasm32"))] pub async fn playlists_list(cfg: &Config) -> Result<()> { - if !cfg.has_api_keys() { - bail!("RUM playlists requires API key authentication (DD_API_KEY + DD_APP_KEY)"); - } let dd_cfg = client::make_dd_config(cfg); - let api = RumReplayPlaylistsAPI::with_config(dd_cfg); + let api = match client::make_bearer_client(cfg) { + Some(c) => RumReplayPlaylistsAPI::with_client_and_config(dd_cfg, c), + None => RumReplayPlaylistsAPI::with_config(dd_cfg), + }; let resp = api .list_rum_replay_playlists(ListRumReplayPlaylistsOptionalParams::default()) .await @@ -575,11 +574,11 @@ pub async fn playlists_list(cfg: &Config) -> Result<()> { #[cfg(not(target_arch = "wasm32"))] pub async fn playlists_get(cfg: &Config, playlist_id: i32) -> Result<()> { - if !cfg.has_api_keys() { - bail!("RUM playlists requires API key authentication (DD_API_KEY + DD_APP_KEY)"); - } let dd_cfg = client::make_dd_config(cfg); - let api = RumReplayPlaylistsAPI::with_config(dd_cfg); + let api = match client::make_bearer_client(cfg) { + Some(c) => RumReplayPlaylistsAPI::with_client_and_config(dd_cfg, c), + None => RumReplayPlaylistsAPI::with_config(dd_cfg), + }; let resp = api .get_rum_replay_playlist(playlist_id) .await @@ -598,11 +597,11 @@ pub async fn playlists_get(cfg: &Config, playlist_id: i32) -> Result<()> { #[cfg(not(target_arch = "wasm32"))] pub async fn heatmaps_query(cfg: &Config, view_name: &str) -> Result<()> { - if !cfg.has_api_keys() { - bail!("RUM heatmaps requires API key authentication (DD_API_KEY + DD_APP_KEY)"); - } let dd_cfg = client::make_dd_config(cfg); - let api = RumReplayHeatmapsAPI::with_config(dd_cfg); + let api = match client::make_bearer_client(cfg) { + Some(c) => RumReplayHeatmapsAPI::with_client_and_config(dd_cfg, c), + None => RumReplayHeatmapsAPI::with_config(dd_cfg), + }; let resp = api .list_replay_heatmap_snapshots( view_name.to_string(), From b27d7bff244a052e6f9b2652c1b60d3dd6af7ea3 Mon Sep 17 00:00:00 2001 From: Stephen Rosenthal Date: Tue, 3 Mar 2026 15:00:05 -0800 Subject: [PATCH 3/6] fix(auth): align OAuth scopes with full expected set (68 scopes) Reorganize default_scopes() alphabetically by domain and align with the complete expected scope list. Adds 13 missing scopes (CI Visibility, Service Catalog, Teams, BITS, Data Scanner, Status Pages, host_tags_write) and removes 6 unnecessary scopes (slos_corrections, synthetics_global_variable_*, synthetics_private_location_write, security_monitoring_suppressions_read, user_self_profile_read). Co-Authored-By: Claude Opus 4.6 --- src/auth/types.rs | 130 ++++++++++++++++++++++++++++++---------------- 1 file changed, 84 insertions(+), 46 deletions(-) diff --git a/src/auth/types.rs b/src/auth/types.rs index bca76cef..71de868d 100644 --- a/src/auth/types.rs +++ b/src/auth/types.rs @@ -42,74 +42,102 @@ pub struct ClientCredentials { /// Default OAuth scopes requested during login. pub fn default_scopes() -> Vec<&'static str> { vec![ + // APM + "apm_read", + "apm_service_catalog_read", + // Audit + "audit_logs_read", + // BITS + "bits_investigations_read", + "bits_investigations_write", + // Cases + "cases_read", + "cases_write", + // CI Visibility + "ci_visibility_read", + "code_coverage_read", + "dora_metrics_write", + "test_optimization_read", + "test_optimization_write", + // Dashboards "dashboards_read", "dashboards_write", + // Data Scanner + "data_scanner_read", + // Error Tracking "error_tracking_read", - "monitors_read", - "monitors_write", - "monitors_downtime", - "audit_logs_read", - "apm_read", - "slos_read", - "slos_write", - "slos_corrections", + // Events + "events_read", + // HAMR (disaster recovery) + "disaster_recovery_status_read", + "disaster_recovery_status_write", + // Hosts + "hosts_read", + "host_tags_write", + // Incidents "incident_read", "incident_write", "incident_notification_settings_read", "incident_settings_read", "incident_settings_write", - "synthetics_read", - "synthetics_write", - "synthetics_global_variable_read", - "synthetics_global_variable_write", - "synthetics_private_location_read", - "synthetics_private_location_write", - "security_monitoring_signals_read", - "security_monitoring_rules_read", - "security_monitoring_findings_read", - "security_monitoring_suppressions_read", - "security_monitoring_filters_read", - "rum_apps_read", - "rum_apps_write", - "rum_retention_filters_read", - "rum_retention_filters_write", - "hosts_read", - "user_access_read", - "user_self_profile_read", - "cases_read", - "cases_write", - "events_read", + // Integrations (Jira, ServiceNow, Slack, Webhooks) + "integrations_read", + "manage_integrations", + // Logs "logs_generate_metrics", + "logs_modify_indexes", "logs_read_archives", "logs_read_config", "logs_read_data", "logs_read_index_data", "logs_write_archives", + // Metrics "metrics_read", + // Monitors + "monitors_read", + "monitors_write", + "monitors_downtime", + // Notebooks + "notebooks_read", + "notebooks_write", + // OCI "oci_configuration_edit", "oci_configuration_read", "oci_configurations_manage", - "timeseries_query", - "usage_read", - // Integrations (Jira, ServiceNow, Slack, Webhooks) - "integrations_read", - "manage_integrations", // Organizations "org_management", - // Security Monitoring (filters + rules write) - "security_monitoring_filters_write", - "security_monitoring_rules_write", - // Logs index management - "logs_modify_indexes", - // HAMR (disaster recovery) - "disaster_recovery_status_read", - "disaster_recovery_status_write", - // Notebooks - "notebooks_read", - "notebooks_write", // RUM + "rum_apps_read", + "rum_apps_write", "rum_generate_metrics", + "rum_retention_filters_read", + "rum_retention_filters_write", "rum_session_replay_read", + // Security + "security_monitoring_filters_read", + "security_monitoring_filters_write", + "security_monitoring_findings_read", + "security_monitoring_rules_read", + "security_monitoring_rules_write", + "security_monitoring_signals_read", + // SLOs + "slos_read", + "slos_write", + // Status Pages + "status_pages_settings_read", + "status_pages_settings_write", + // Synthetics + "synthetics_read", + "synthetics_write", + "synthetics_private_location_read", + // Teams + "teams_read", + // Timeseries + "timeseries_query", + // Usage + "usage_read", + // Users + "user_access_read", ] } @@ -156,10 +184,20 @@ mod tests { #[test] fn test_default_scopes() { let scopes = default_scopes(); - assert!(scopes.len() > 20); + assert_eq!(scopes.len(), 68); assert!(scopes.contains(&"dashboards_read")); assert!(scopes.contains(&"monitors_read")); assert!(scopes.contains(&"logs_read_data")); + // Batch 2 additions + assert!(scopes.contains(&"integrations_read")); + assert!(scopes.contains(&"org_management")); + assert!(scopes.contains(&"disaster_recovery_status_read")); + assert!(scopes.contains(&"notebooks_read")); + assert!(scopes.contains(&"rum_generate_metrics")); + assert!(scopes.contains(&"ci_visibility_read")); + assert!(scopes.contains(&"teams_read")); + assert!(scopes.contains(&"apm_service_catalog_read")); + assert!(scopes.contains(&"status_pages_settings_read")); } #[test] From 905594985c52b3ac4de383ad349eba4e2ebd5cb8 Mon Sep 17 00:00:00 2001 From: Cody Lee Date: Tue, 3 Mar 2026 18:00:20 -0600 Subject: [PATCH 4/6] style(client): fix rustfmt formatting in test Co-Authored-By: Claude Sonnet 4.6 (1M context) --- src/client.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/client.rs b/src/client.rs index 56befd65..57c366ea 100644 --- a/src/client.rs +++ b/src/client.rs @@ -491,7 +491,10 @@ mod tests { #[test] fn test_no_fallback_for_rum() { - assert!(!requires_api_key_fallback("GET", "/api/v2/rum/applications")); + assert!(!requires_api_key_fallback( + "GET", + "/api/v2/rum/applications" + )); assert!(!requires_api_key_fallback( "GET", "/api/v2/rum/applications/abc-123" From 8b85ad2f6b75f155ff69de3215f51430a3642a2b Mon Sep 17 00:00:00 2001 From: Stephen Rosenthal Date: Tue, 3 Mar 2026 17:21:12 -0800 Subject: [PATCH 5/6] fix(auth): enable OAuth for Status Pages and add Azure scope MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove 3 validate_api_and_app_keys() gates in main.rs for status-pages (Pages, Components, Degradations) — the command module already had bearer client support via make_api() - Add azure_configuration_read scope (69 total) Co-Authored-By: Claude Opus 4.6 --- src/auth/types.rs | 4 +++- src/main.rs | 3 --- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/auth/types.rs b/src/auth/types.rs index 71de868d..b9a61181 100644 --- a/src/auth/types.rs +++ b/src/auth/types.rs @@ -47,6 +47,8 @@ pub fn default_scopes() -> Vec<&'static str> { "apm_service_catalog_read", // Audit "audit_logs_read", + // Azure + "azure_configuration_read", // BITS "bits_investigations_read", "bits_investigations_write", @@ -184,7 +186,7 @@ mod tests { #[test] fn test_default_scopes() { let scopes = default_scopes(); - assert_eq!(scopes.len(), 68); + assert_eq!(scopes.len(), 69); assert!(scopes.contains(&"dashboards_read")); assert!(scopes.contains(&"monitors_read")); assert!(scopes.contains(&"logs_read_data")); diff --git a/src/main.rs b/src/main.rs index ebd31fb0..dcf77552 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5997,7 +5997,6 @@ async fn main_inner() -> anyhow::Result<()> { // --- Status Pages --- Commands::StatusPages { action } => match action { StatusPageActions::Pages { action } => { - cfg.validate_api_and_app_keys()?; match action { StatusPagePageActions::List => commands::status_pages::pages_list(&cfg).await?, StatusPagePageActions::Get { page_id } => { @@ -6015,7 +6014,6 @@ async fn main_inner() -> anyhow::Result<()> { } } StatusPageActions::Components { action } => { - cfg.validate_api_and_app_keys()?; match action { StatusPageComponentActions::List { page_id } => { commands::status_pages::components_list(&cfg, &page_id).await?; @@ -6053,7 +6051,6 @@ async fn main_inner() -> anyhow::Result<()> { } } StatusPageActions::Degradations { action } => { - cfg.validate_api_and_app_keys()?; match action { StatusPageDegradationActions::List => { commands::status_pages::degradations_list(&cfg).await?; From 8d31742792e212adc8e20400f5dce62a4e24e9ee Mon Sep 17 00:00:00 2001 From: Cody Lee Date: Tue, 3 Mar 2026 19:59:58 -0600 Subject: [PATCH 6/6] style(main): run cargo fmt to fix formatting The status_pages match arms were written with manual nesting instead of the rustfmt-preferred inline match style. Co-Authored-By: Claude Sonnet 4.6 (1M context) --- src/main.rs | 162 +++++++++++++++++++++++----------------------------- 1 file changed, 73 insertions(+), 89 deletions(-) diff --git a/src/main.rs b/src/main.rs index dcf77552..04cb4368 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5996,101 +5996,85 @@ async fn main_inner() -> anyhow::Result<()> { } // --- Status Pages --- Commands::StatusPages { action } => match action { - StatusPageActions::Pages { action } => { - match action { - StatusPagePageActions::List => commands::status_pages::pages_list(&cfg).await?, - StatusPagePageActions::Get { page_id } => { - commands::status_pages::pages_get(&cfg, &page_id).await?; - } - StatusPagePageActions::Create { file } => { - commands::status_pages::pages_create(&cfg, &file).await?; - } - StatusPagePageActions::Update { page_id, file } => { - commands::status_pages::pages_update(&cfg, &page_id, &file).await?; - } - StatusPagePageActions::Delete { page_id } => { - commands::status_pages::pages_delete(&cfg, &page_id).await?; - } + StatusPageActions::Pages { action } => match action { + StatusPagePageActions::List => commands::status_pages::pages_list(&cfg).await?, + StatusPagePageActions::Get { page_id } => { + commands::status_pages::pages_get(&cfg, &page_id).await?; } - } - StatusPageActions::Components { action } => { - match action { - StatusPageComponentActions::List { page_id } => { - commands::status_pages::components_list(&cfg, &page_id).await?; - } - StatusPageComponentActions::Get { - page_id, - component_id, - } => { - commands::status_pages::components_get(&cfg, &page_id, &component_id) - .await?; - } - StatusPageComponentActions::Create { page_id, file } => { - commands::status_pages::components_create(&cfg, &page_id, &file).await?; - } - StatusPageComponentActions::Update { - page_id, - component_id, - file, - } => { - commands::status_pages::components_update( - &cfg, - &page_id, - &component_id, - &file, - ) + StatusPagePageActions::Create { file } => { + commands::status_pages::pages_create(&cfg, &file).await?; + } + StatusPagePageActions::Update { page_id, file } => { + commands::status_pages::pages_update(&cfg, &page_id, &file).await?; + } + StatusPagePageActions::Delete { page_id } => { + commands::status_pages::pages_delete(&cfg, &page_id).await?; + } + }, + StatusPageActions::Components { action } => match action { + StatusPageComponentActions::List { page_id } => { + commands::status_pages::components_list(&cfg, &page_id).await?; + } + StatusPageComponentActions::Get { + page_id, + component_id, + } => { + commands::status_pages::components_get(&cfg, &page_id, &component_id).await?; + } + StatusPageComponentActions::Create { page_id, file } => { + commands::status_pages::components_create(&cfg, &page_id, &file).await?; + } + StatusPageComponentActions::Update { + page_id, + component_id, + file, + } => { + commands::status_pages::components_update(&cfg, &page_id, &component_id, &file) .await?; - } - StatusPageComponentActions::Delete { - page_id, - component_id, - } => { - commands::status_pages::components_delete(&cfg, &page_id, &component_id) - .await?; - } } - } - StatusPageActions::Degradations { action } => { - match action { - StatusPageDegradationActions::List => { - commands::status_pages::degradations_list(&cfg).await?; - } - StatusPageDegradationActions::Get { - page_id, - degradation_id, - } => { - commands::status_pages::degradations_get(&cfg, &page_id, °radation_id) - .await?; - } - StatusPageDegradationActions::Create { page_id, file } => { - commands::status_pages::degradations_create(&cfg, &page_id, &file).await?; - } - StatusPageDegradationActions::Update { - page_id, - degradation_id, - file, - } => { - commands::status_pages::degradations_update( - &cfg, - &page_id, - °radation_id, - &file, - ) + StatusPageComponentActions::Delete { + page_id, + component_id, + } => { + commands::status_pages::components_delete(&cfg, &page_id, &component_id) .await?; - } - StatusPageDegradationActions::Delete { - page_id, - degradation_id, - } => { - commands::status_pages::degradations_delete( - &cfg, - &page_id, - °radation_id, - ) + } + }, + StatusPageActions::Degradations { action } => match action { + StatusPageDegradationActions::List => { + commands::status_pages::degradations_list(&cfg).await?; + } + StatusPageDegradationActions::Get { + page_id, + degradation_id, + } => { + commands::status_pages::degradations_get(&cfg, &page_id, °radation_id) .await?; - } } - } + StatusPageDegradationActions::Create { page_id, file } => { + commands::status_pages::degradations_create(&cfg, &page_id, &file).await?; + } + StatusPageDegradationActions::Update { + page_id, + degradation_id, + file, + } => { + commands::status_pages::degradations_update( + &cfg, + &page_id, + °radation_id, + &file, + ) + .await?; + } + StatusPageDegradationActions::Delete { + page_id, + degradation_id, + } => { + commands::status_pages::degradations_delete(&cfg, &page_id, °radation_id) + .await?; + } + }, StatusPageActions::ThirdParty { action } => match action { StatusPageThirdPartyActions::List { active, search } => { commands::status_pages::third_party_list(&cfg, search.as_deref(), active)