diff --git a/crates/fluvio-hub-protocol/src/infinyon_tok.rs b/crates/fluvio-hub-protocol/src/infinyon_tok.rs index 1084663e0ed..1eed335a7d7 100644 --- a/crates/fluvio-hub-protocol/src/infinyon_tok.rs +++ b/crates/fluvio-hub-protocol/src/infinyon_tok.rs @@ -2,11 +2,13 @@ // minimal login token read module that just exposes a // 'read_infinyon_token' function to read from the current login config // +use std::collections::HashMap; use std::env; use std::fs; use std::path::Path; use serde::{Deserialize, Serialize}; +use serde_json; use tracing::debug; use fluvio_types::defaults::CLI_CONFIG_PATH; @@ -20,6 +22,9 @@ type InfinyonRemote = String; #[derive(thiserror::Error, Debug)] pub enum InfinyonCredentialError { + #[error("no org access token found, please login or switch to an org with 'fluvio cloud org switch'")] + MissingOrgToken, + #[error("{0}")] Read(String), @@ -27,13 +32,114 @@ pub enum InfinyonCredentialError { UnableToParseCredentials, } +pub enum AccessToken { + V3(InfinyonToken), + V4(CliAccessTokens), +} + +impl AccessToken { + pub fn get_hub_token(&self) -> Result { + match self { + AccessToken::V3(tok) => Ok(tok.to_owned()), + AccessToken::V4(cli_access_tokens) => cli_access_tokens.get_current_org_token(), + } + } + + pub fn is_v4(&self) -> bool { + matches!(self, AccessToken::V4(_)) + } +} + +// multi-org access token output +#[derive(Debug, Serialize, Deserialize)] +pub struct CliAccessTokens { + pub remote: String, + pub user_access_token: String, + pub org_access_tokens: HashMap, +} + +impl CliAccessTokens { + pub fn get_current_org_name(&self) -> Result { + let key = self + .org_access_tokens + .keys() + .next() + .ok_or(InfinyonCredentialError::MissingOrgToken)?; + Ok(key.to_owned()) + } + pub fn get_current_org_token(&self) -> Result { + let org = self.get_current_org_name()?; + let tok = self + .org_access_tokens + .get(&org) + .ok_or(InfinyonCredentialError::MissingOrgToken)? + .to_owned(); + Ok(tok) + } +} + +/// replaces old read_infinyon_token +pub fn read_access_token() -> Result { + match read_infinyon_token_v4() { + Ok(cli_access_tokens) => { + println!( + "Using org access: {}", + cli_access_tokens.get_current_org_name()? + ); + return Ok(AccessToken::V4(cli_access_tokens)); + } + Err(_e) => { + // fallback to old token logic + } + }; + let tok = read_infinyon_token_v3()?; + Ok(AccessToken::V3(tok)) +} + pub fn read_infinyon_token() -> Result { - // the ENV variable should point directly to the applicable profile - if let Ok(profilepath) = env::var(INFINYON_CONFIG_PATH_ENV) { - let cred = Credentials::load(Path::new(&profilepath))?; - debug!("{INFINYON_CONFIG_PATH_ENV} {profilepath} loaded"); - return Ok(cred.token); + match read_infinyon_token_v4() { + Ok(cli_access_tokens) => { + tracing::debug!( + "using v4 token for org {}", + cli_access_tokens.get_current_org_name()? + ); + return cli_access_tokens.get_current_org_token(); + } + Err(_e) => { + // fallback to old token logic + } + }; + read_infinyon_token_v3() +} + +pub fn read_infinyon_token_v4() -> Result { + const LOGIN_BIN: &str = "fluvio-cloud-v4"; + + let mut cmd = std::process::Command::new(LOGIN_BIN); + cmd.arg("cli-access-tokens"); + cmd.env_remove("RUST_LOG"); // remove RUST_LOG to avoid debug output + match cmd.output() { + Ok(output) => { + let output = String::from_utf8_lossy(&output.stdout); + let cli_access_tokens: CliAccessTokens = + serde_json::from_slice(output.as_bytes()).map_err(|e| { + tracing::debug!("failed to parse v4 output: {}\n$ fluvio-cloud-v4 cli-access-tokens\n-->>{}<<--", e, output); + InfinyonCredentialError::UnableToParseCredentials + })?; + tracing::trace!("cli access tokens: {:#?}", cli_access_tokens); + Ok(cli_access_tokens) + } + Err(e) => { + tracing::debug!("failed to execute v4: {}", e); + Err(InfinyonCredentialError::Read( + "failed to execute v4".to_owned(), + )) + } } +} + +// depcreated, will be removed after multi-org is stable +pub fn read_infinyon_token_v3() -> Result { let cfgpath = default_file_path(); // this will read the indirection file to resolve the profile let cred = Credentials::try_load(cfgpath)?; diff --git a/crates/fluvio-hub-util/src/hubaccess.rs b/crates/fluvio-hub-util/src/hubaccess.rs index 549b77c68df..fe39b48cf16 100644 --- a/crates/fluvio-hub-util/src/hubaccess.rs +++ b/crates/fluvio-hub-util/src/hubaccess.rs @@ -18,9 +18,10 @@ use fluvio_future::task::run_block_on; use fluvio_hub_protocol::infinyon_tok::read_infinyon_token_rem; use fluvio_hub_protocol::{Result, HubError}; -use fluvio_hub_protocol::infinyon_tok::read_infinyon_token; +use fluvio_hub_protocol::infinyon_tok::read_access_token; use fluvio_hub_protocol::constants::{HUB_API_ACT, HUB_API_HUBID, HUB_REMOTE, CLI_CONFIG_HUB}; use fluvio_types::defaults::CLI_CONFIG_PATH; +use fluvio_hub_protocol::infinyon_tok::AccessToken; #[cfg(not(target_arch = "wasm32"))] use crate::htclient; @@ -149,15 +150,16 @@ impl HubAccess { action: &str, authn_token: &str, ) -> Result { - self.make_action_token(action, authn_token.into()).await + let access_token = AccessToken::V3(authn_token.to_string()); + self.make_action_token(action, Some(access_token)).await } async fn get_action_auth(&self, action: &str) -> Result { - let cloud_token = read_infinyon_token().unwrap_or_default(); - self.make_action_token(action, cloud_token).await + let access_token = read_access_token().ok(); + self.make_action_token(action, access_token).await } - async fn make_action_token(&self, action: &str, authn_token: String) -> Result { + async fn make_action_token(&self, action: &str, token: Option) -> Result { let host = &self.remote; let api_url = format!("{host}/{HUB_API_ACT}"); let mat = MsgActionToken { @@ -167,8 +169,24 @@ impl HubAccess { .map_err(|_e| HubError::HubAccess("Failed access setup".to_string()))?; let mut builder = http::Request::post(&api_url); - if !authn_token.is_empty() { - builder = builder.header("Authorization", &authn_token); + match token { + Some(AccessToken::V4(cli_access_tokens)) => { + let org = cli_access_tokens.get_current_org_name()?; + let tok = cli_access_tokens + .org_access_tokens + .get(&org) + .ok_or(HubError::HubAccess("Missing org token".to_string()))?; + let authn_token = format!("bearer {tok}"); + builder = builder.header("Authorization", &authn_token); + } + Some(AccessToken::V3(tok)) => { + // v3 does not use "Bearer" prefix + builder = builder.header("Authorization", &tok); + } + None => { + // no token is allowed for some actions like downloading public + // packages + } } let req = builder .header(http::header::CONTENT_TYPE, mime::JSON.as_str())