diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c4e2c3875..be1c1707a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ - (snapshots) Compress preprod snapshot manifest with zstd ([#3336](https://github.com/getsentry/sentry-cli/pull/3336)) +### Fixes + +- (auth) Route org token requests to region_url ([#3342](https://github.com/getsentry/sentry-cli/pull/3342)) + ## 3.5.1 ### Internal Changes 🔧 diff --git a/src/config.rs b/src/config.rs index 30296962dd..52ecf847b1 100644 --- a/src/config.rs +++ b/src/config.rs @@ -75,7 +75,7 @@ impl Config { let manually_configured_url = configured_url(&ini); let token_url = token_embedded_data .as_ref() - .map(|td| td.url.as_str()) + .map(|td| td.base_url()) .unwrap_or_default(); let url = if token_url.is_empty() { @@ -201,7 +201,7 @@ impl Config { Some(Auth::Token(ref val)) => { self.cached_token_data = val.payload().cloned(); - if let Some(token_url) = self.cached_token_data.as_ref().map(|td| td.url.as_str()) { + if let Some(token_url) = self.cached_token_data.as_ref().map(|td| td.base_url()) { self.cached_base_url = token_url.to_owned(); } @@ -232,7 +232,7 @@ impl Config { let token_url = self .cached_token_data .as_ref() - .map(|td| td.url.as_str()) + .map(|td| td.base_url()) .unwrap_or_default(); if !token_url.is_empty() && url != token_url { diff --git a/src/utils/auth_token/org_auth_token.rs b/src/utils/auth_token/org_auth_token.rs index c464a2d0cf..b2997109b4 100644 --- a/src/utils/auth_token/org_auth_token.rs +++ b/src/utils/auth_token/org_auth_token.rs @@ -19,6 +19,23 @@ pub struct AuthTokenPayload { // URL may be missing from some old auth tokens, see getsentry/sentry#57123 #[serde(deserialize_with = "url_deserializer")] pub url: String, + + // Region URL is the host that actually serves this org's API requests. It is + // absent from older tokens, in which case requests fall back to `url`. + #[serde(default, deserialize_with = "url_deserializer")] + pub region_url: String, +} + +impl AuthTokenPayload { + /// Returns the base URL that API requests for this org should target, + /// preferring the region URL and falling back to `url` when it is absent. + pub fn base_url(&self) -> &str { + if self.region_url.is_empty() { + &self.url + } else { + &self.region_url + } + } } /// Deserializes a URL from a string, returning an empty string if the URL is missing or null. diff --git a/src/utils/auth_token/test.rs b/src/utils/auth_token/test.rs index a25568b150..52e452fd59 100644 --- a/src/utils/auth_token/test.rs +++ b/src/utils/auth_token/test.rs @@ -21,12 +21,54 @@ fn test_valid_org_auth_token() { let payload = token.payload().unwrap(); assert_eq!(payload.org, "sentry"); assert_eq!(payload.url, "http://localhost:8000"); + assert_eq!(payload.region_url, "http://localhost:8000"); + assert_eq!(payload.base_url(), "http://localhost:8000"); assert_eq!(good_token, token.raw().expose_secret().clone()); assert!(token.format_recognized()); } +#[test] +fn test_valid_org_auth_token_region_url_differs() { + // Payload: {"url":"http://control.example","region_url":"http://region.example","org":"sentry"} + let good_token = String::from( + "sntrys_\ + eyJpYXQiOiAxNzA0MjA1ODAyLjE5OTc0MywgInVybCI6ICJodHRwOi8vY29udHJvbC5leGFtcGxlIiw\ + gInJlZ2lvbl91cmwiOiAiaHR0cDovL3JlZ2lvbi5leGFtcGxlIiwgIm9yZyI6ICJzZW50cnkifQ==_\ + lQ5ETt61cHhvJa35fxvxARsDXeVrd0pu4/smF4sRieA", + ); + + let token = AuthToken::from(good_token); + let payload = token.payload().unwrap(); + + assert_eq!(payload.org, "sentry"); + assert_eq!(payload.url, "http://control.example"); + assert_eq!(payload.region_url, "http://region.example"); + // base_url prefers region_url when present. + assert_eq!(payload.base_url(), "http://region.example"); +} + +#[test] +fn test_valid_org_auth_token_missing_region_url() { + // Legacy token with no region_url field at all; base_url falls back to url. + // Payload: {"url":"http://legacy.example","org":"sentry"} + let good_token = String::from( + "sntrys_\ + eyJpYXQiOiAxNzA0MjA1ODAyLjE5OTc0MywgInVybCI6ICJodHRwOi8vbGVnYWN5LmV4YW1wbGUiL\ + CAib3JnIjogInNlbnRyeSJ9_\ + lQ5ETt61cHhvJa35fxvxARsDXeVrd0pu4/smF4sRieA", + ); + + let token = AuthToken::from(good_token); + let payload = token.payload().unwrap(); + + assert_eq!(payload.org, "sentry"); + assert_eq!(payload.url, "http://legacy.example"); + assert!(payload.region_url.is_empty()); + assert_eq!(payload.base_url(), "http://legacy.example"); +} + #[test] fn test_valid_org_auth_token_missing_url() { let good_token = String::from( @@ -43,6 +85,9 @@ fn test_valid_org_auth_token_missing_url() { let payload = token.payload().unwrap(); assert_eq!(payload.org, "sentry"); assert!(payload.url.is_empty()); + // url is null but region_url is present, so base_url falls back to region_url. + assert_eq!(payload.region_url, "http://localhost:8000"); + assert_eq!(payload.base_url(), "http://localhost:8000"); assert_eq!(good_token, token.raw().expose_secret().clone()); diff --git a/tests/integration/_cases/org_tokens/region-url-routing.trycmd b/tests/integration/_cases/org_tokens/region-url-routing.trycmd new file mode 100644 index 0000000000..f209e9b2ce --- /dev/null +++ b/tests/integration/_cases/org_tokens/region-url-routing.trycmd @@ -0,0 +1,10 @@ +A region-scoped org token where url (control silo) differs from region_url (the org's +region host). The region_url governs routing: it is preferred over the token's url and +overrides --url. Payload: {"url":"http://control.example","region_url":"http://region.example","org":"sentry"} +``` +$ sentry-cli --auth-token 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 --url http://example.com info +? failed +[..]WARN[..] Using http://region.example (embedded in token) rather than manually-configured URL http://example.com. To use http://example.com, please provide an auth token for this URL. +... + +``` diff --git a/tests/integration/_cases/org_tokens/url-mismatch-empty-token.trycmd b/tests/integration/_cases/org_tokens/url-mismatch-empty-token.trycmd index db83846b6c..d95a53240a 100644 --- a/tests/integration/_cases/org_tokens/url-mismatch-empty-token.trycmd +++ b/tests/integration/_cases/org_tokens/url-mismatch-empty-token.trycmd @@ -1,8 +1,9 @@ -This auth token has an empty URL. We expect the --url argument to take precedence here +This token has an empty url but a non-empty region_url. The region_url now governs +routing, so it overrides the --url argument (the warning references the region_url). ``` $ sentry-cli --auth-token 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 --url https://sentry.example.com info ? failed -Sentry Server: https://sentry.example.com +[..]WARN[..] Using http://localhost:8000 (embedded in token) rather than manually-configured URL https://sentry.example.com. To use https://sentry.example.com, please provide an auth token for this URL. ... ```