Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Use org and url from tokens #1673

Merged
merged 12 commits into from Jul 11, 2023
4 changes: 2 additions & 2 deletions src/commands/login.rs
Expand Up @@ -20,7 +20,7 @@ pub fn make_command(command: Command) -> Command {

fn update_config(config: &Config, token: &str) -> Result<()> {
let mut new_cfg = config.clone();
new_cfg.set_auth(Auth::Token(token.to_string()));
new_cfg.set_auth(Auth::Token(token.to_string()))?;
new_cfg.save()?;
Ok(())
}
Expand Down Expand Up @@ -65,7 +65,7 @@ pub fn execute(matches: &ArgMatches) -> Result<()> {
};

let test_cfg = config.make_copy(|cfg| {
cfg.set_auth(Auth::Token(token.to_string()));
cfg.set_auth(Auth::Token(token.to_string()))?;
Ok(())
})?;
match Api::with_config(test_cfg).get_auth_info() {
Expand Down
18 changes: 9 additions & 9 deletions src/commands/mod.rs
Expand Up @@ -97,21 +97,21 @@ fn preexecute_hooks() -> Result<bool> {
}

fn configure_args(config: &mut Config, matches: &ArgMatches) -> Result<()> {
if let Some(url) = matches.get_one::<String>("url") {
config.set_base_url(url);
if let Some(api_key) = matches.get_one::<String>("api_key") {
config.set_auth(Auth::Key(api_key.to_owned()))?;
}

if let Some(headers) = matches.get_many::<String>("headers") {
let headers = headers.map(|h| h.to_owned()).collect();
config.set_headers(headers);
if let Some(auth_token) = matches.get_one::<String>("auth_token") {
config.set_auth(Auth::Token(auth_token.to_owned()))?;
}

if let Some(api_key) = matches.get_one::<String>("api_key") {
config.set_auth(Auth::Key(api_key.to_owned()));
if let Some(url) = matches.get_one::<String>("url") {
config.set_base_url(url)?;
}

if let Some(auth_token) = matches.get_one::<String>("auth_token") {
config.set_auth(Auth::Token(auth_token.to_owned()));
if let Some(headers) = matches.get_many::<String>("headers") {
let headers = headers.map(|h| h.to_owned()).collect();
config.set_headers(headers);
}

if let Some(level_str) = matches.get_one::<String>("log_level") {
Expand Down
141 changes: 129 additions & 12 deletions src/config.rs
Expand Up @@ -13,6 +13,7 @@ use lazy_static::lazy_static;
use log::{debug, info, set_max_level, warn};
use parking_lot::Mutex;
use sentry::types::Dsn;
use serde::Deserialize;

use crate::constants::DEFAULT_MAX_DIF_ITEM_SIZE;
use crate::constants::DEFAULT_MAX_DIF_UPLOAD_SIZE;
Expand All @@ -26,6 +27,43 @@ pub enum Auth {
Token(String),
}

/// Data parsed from an "org auth token".
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
struct TokenData {
/// An org slug.
org: String,
/// A base Sentry URL.
url: String,
}

impl TokenData {
/// Attempt to extract data from an "org auth token".
///
/// Org auth tokens start with `sntrys` and contain BASE64-encoded
/// data between two underscores.
///
/// Attempting to decode a valid org auth token results in `Ok(Some(data))`.
/// Attempting to decode an org auth token that contains invalid data returns an error.
/// Attempting to decode any other token returns Ok(None).
fn decode(token: &str) -> Result<Option<Self>> {
const ORG_TOKEN_PREFIX: &str = "sntrys_";

let Some(rest) = token.strip_prefix(ORG_TOKEN_PREFIX) else {
return Ok(None);
};

let Some((encoded, _)) = rest.split_once('_') else {
bail!("no closing _");
};

let json = data_encoding::BASE64
.decode(encoded.as_bytes())
.context("invalid base64 data")?;

Ok(serde_json::from_slice(&json)?)
}
}

lazy_static! {
static ref CONFIG: Mutex<Option<Arc<Config>>> = Mutex::new(None);
}
Expand All @@ -40,6 +78,7 @@ pub struct Config {
cached_headers: Option<Vec<String>>,
cached_log_level: log::LevelFilter,
cached_vcs_remote: String,
cached_token_data: Option<TokenData>,
}

impl Config {
Expand All @@ -51,15 +90,37 @@ impl Config {

/// Creates Config based on provided config file.
pub fn from_file(filename: PathBuf, ini: Ini) -> Result<Config> {
let auth = get_default_auth(&ini);
let token_embedded_data = match auth {
Some(Auth::Token(ref token)) => {
TokenData::decode(token).context("Failed to parse org auth token {token}")?
}
_ => None,
};

let mut url = get_default_url(&ini);

if let Some(ref token_embedded_data) = token_embedded_data {
if url == DEFAULT_URL || url.is_empty() {
url = token_embedded_data.url.clone();
} else if url != token_embedded_data.url {
bail!(
"Two different url values supplied: `{}` (from token), `{url}`.",
token_embedded_data.url,
);
}
}

Ok(Config {
filename,
process_bound: false,
cached_auth: get_default_auth(&ini),
cached_base_url: get_default_url(&ini),
cached_auth: auth,
cached_base_url: url,
cached_headers: get_default_headers(&ini),
cached_log_level: get_default_log_level(&ini),
cached_vcs_remote: get_default_vcs_remote(&ini),
ini,
cached_token_data: token_embedded_data,
})
}

Expand Down Expand Up @@ -139,13 +200,20 @@ impl Config {
}

/// Updates the auth info
pub fn set_auth(&mut self, auth: Auth) {
pub fn set_auth(&mut self, auth: Auth) -> Result<()> {
self.cached_auth = Some(auth);

self.ini.delete_from(Some("auth"), "api_key");
self.ini.delete_from(Some("auth"), "token");
match self.cached_auth {
Some(Auth::Token(ref val)) => {
self.cached_token_data =
TokenData::decode(val).context("Failed to parse org auth token {token}")?;

if let Some(ref data) = self.cached_token_data {
self.cached_base_url = data.url.clone();
}

self.ini
.set_to(Some("auth"), "token".into(), val.to_string());
}
Expand All @@ -155,6 +223,8 @@ impl Config {
}
None => {}
}

Ok(())
}

/// Returns the base url (without trailing slashes)
Expand All @@ -170,10 +240,19 @@ impl Config {
}

/// Sets the URL
pub fn set_base_url(&mut self, url: &str) {
pub fn set_base_url(&mut self, url: &str) -> Result<()> {
if let Some(ref org_token) = self.cached_token_data {
if url != org_token.url {
bail!(
"Two different url values supplied: `{}` (from token), `{url}`.",
org_token.url,
);
}
}
self.cached_base_url = url.to_owned();
self.ini
.set_to(Some("defaults"), "url".into(), self.cached_base_url.clone());
Ok(())
}

/// Sets headers that should be attached to all requests
Expand Down Expand Up @@ -273,16 +352,35 @@ impl Config {

/// Given a match object from clap, this returns the org from it.
pub fn get_org(&self, matches: &ArgMatches) -> Result<String> {
matches
let org_from_token = self.cached_token_data.as_ref().map(|t| &t.org);

let org_from_cli = matches
.get_one::<String>("org")
.cloned()
.or_else(|| env::var("SENTRY_ORG").ok())
.or_else(|| {
self.ini
.get_from(Some("defaults"), "org")
.map(str::to_owned)
})
.ok_or_else(|| format_err!("An organization slug is required (provide with --org)"))
.or_else(|| env::var("SENTRY_ORG").ok());

match (org_from_token, org_from_cli) {
(None, None) => self
.ini
.get_from(Some("defaults"), "org")
.map(str::to_owned)
.ok_or_else(|| {
format_err!("An organization slug is required (provide with --org)")
}),
(None, Some(cli_org)) => Ok(cli_org),
(Some(token_org), None) => Ok(token_org.to_string()),
(Some(token_org), Some(cli_org)) => {
if cli_org.is_empty() {
return Ok(token_org.to_owned());
}
if cli_org != *token_org {
return Err(format_err!(
"Two different org values supplied: `{token_org}` (from token), `{cli_org}`."
));
}
Ok(cli_org)
}
}
}

/// Given a match object from clap, this returns the release from it.
Expand Down Expand Up @@ -574,6 +672,7 @@ impl Clone for Config {
cached_headers: self.cached_headers.clone(),
cached_log_level: self.cached_log_level,
cached_vcs_remote: self.cached_vcs_remote.clone(),
cached_token_data: self.cached_token_data.clone(),
}
}
}
Expand Down Expand Up @@ -641,3 +740,21 @@ fn get_default_vcs_remote(ini: &Ini) -> String {
"origin".to_string()
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_decode_token_data() {
let token = "sntrys_eyJ1cmwiOiJodHRwczovL3NlbnRyeS5pbyIsIm9yZyI6InRlc3Qtb3JnIn0=_foobarthisdoesntmatter";

assert_eq!(
TokenData::decode(token).unwrap().unwrap(),
TokenData {
org: "test-org".to_string(),
url: "https://sentry.io".to_string(),
}
);
}
}
11 changes: 11 additions & 0 deletions tests/integration/_cases/org_tokens/org-match.trycmd
@@ -0,0 +1,11 @@
```
$ sentry-cli --auth-token sntrys_eyJ1cmwiOiJodHRwczovL3NlbnRyeS10b2tlbml6ZWQuaW8iLCJvcmciOiJ0ZXN0LW9yZy10b2tlbml6ZWQifQ==_ sourcemaps upload --org test-org
? failed
error: the following required arguments were not provided:
<PATHS>...

Usage: sentry-cli[EXE] sourcemaps upload --org <ORG> <PATHS>...

For more information, try '--help'.

```
9 changes: 9 additions & 0 deletions tests/integration/_cases/org_tokens/org-mismatch.trycmd
@@ -0,0 +1,9 @@
```
$ sentry-cli --auth-token sntrys_eyJ1cmwiOiJodHRwczovL3NlbnRyeS10b2tlbml6ZWQuaW8iLCJvcmciOiJ0ZXN0LW9yZy10b2tlbml6ZWQifQ==_ sourcemaps upload --org sentry test_path
? failed
error: Two different org values supplied: `test-org-tokenized` (from token), `sentry`.

Add --log-level=[info|debug] or export SENTRY_LOG_LEVEL=[info|debug] to see more output.
Please attach the full debug log to all bug reports.

```
11 changes: 11 additions & 0 deletions tests/integration/_cases/org_tokens/url-match.trycmd
@@ -0,0 +1,11 @@
```
$ sentry-cli --auth-token sntrys_eyJ1cmwiOiJodHRwczovL3NlbnRyeS10b2tlbml6ZWQuaW8iLCJvcmciOiJ0ZXN0LW9yZy10b2tlbml6ZWQifQ==_ --url http://sentry.io sourcemaps upload
? failed
error: the following required arguments were not provided:
<PATHS>...

Usage: sentry-cli[EXE] sourcemaps upload <PATHS>...

For more information, try '--help'.

```
9 changes: 9 additions & 0 deletions tests/integration/_cases/org_tokens/url-mismatch.trycmd
@@ -0,0 +1,9 @@
```
$ sentry-cli --auth-token sntrys_eyJ1cmwiOiJodHRwczovL3NlbnRyeS10b2tlbml6ZWQuaW8iLCJvcmciOiJ0ZXN0LW9yZy10b2tlbml6ZWQifQ==_ --url http://example.com sourcemaps upload test_path
? failed
error: Two different url values supplied: `https://sentry-tokenized.io` (from token), `http://example.com`.

Add --log-level=[info|debug] or export SENTRY_LOG_LEVEL=[info|debug] to see more output.
Please attach the full debug log to all bug reports.

```
12 changes: 12 additions & 0 deletions tests/integration/_cases/org_tokens/url-works.trycmd
@@ -0,0 +1,12 @@
```
$ sentry-cli --auth-token sntrys_eyJ1cmwiOiJodHRwczovL3NlbnRyeS10b2tlbml6ZWQuaW8iLCJvcmciOiJ0ZXN0LW9yZy10b2tlbml6ZWQifQ==_ info
? failed
Sentry Server: https://sentry-tokenized.io
Default Organization: wat-org
Default Project: wat-project

Authentication Info:
Method: Auth Token
(failure on authentication: API request failed)

```
1 change: 1 addition & 0 deletions tests/integration/mod.rs
Expand Up @@ -6,6 +6,7 @@ mod help;
mod info;
mod login;
mod monitors;
mod org_tokens;
mod organizations;
mod projects;
mod releases;
Expand Down
26 changes: 26 additions & 0 deletions tests/integration/org_tokens.rs
@@ -0,0 +1,26 @@
use crate::integration::register_test;

#[test]
fn org_token_url_mismatch() {
register_test("org_tokens/url-mismatch.trycmd");
}

#[test]
fn org_token_org_mismatch() {
register_test("org_tokens/org-mismatch.trycmd");
}

#[test]
fn org_token_url_match() {
register_test("org_tokens/url-match.trycmd");
}

#[test]
fn org_token_org_match() {
register_test("org_tokens/org-match.trycmd");
}

#[test]
fn org_token_url_works() {
register_test("org_tokens/url-works.trycmd");
}