Skip to content

Commit

Permalink
add: optional v4 token access (#4212)
Browse files Browse the repository at this point in the history
* add: optional v4 token access

* fix: infinyon_tok, user_access_token is optional

* small fixup

* comments: more compact if let Ok
  • Loading branch information
digikata authored Dec 9, 2024
1 parent 2e8499c commit af91c88
Show file tree
Hide file tree
Showing 2 changed files with 180 additions and 12 deletions.
160 changes: 155 additions & 5 deletions crates/fluvio-hub-protocol/src/infinyon_tok.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -20,20 +22,123 @@ 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),

#[error("unable to parse credentials")]
UnableToParseCredentials,
}

pub enum AccessToken {
V3(InfinyonToken),
V4(CliAccessTokens),
}

impl AccessToken {
pub fn get_hub_token(&self) -> Result<String, InfinyonCredentialError> {
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: Option<String>,
pub org_access_tokens: HashMap<String, String>,
}

impl CliAccessTokens {
pub fn get_current_org_name(&self) -> Result<String, InfinyonCredentialError> {
let key = self
.org_access_tokens
.keys()
.next()
.ok_or(InfinyonCredentialError::MissingOrgToken)?;
Ok(key.to_owned())
}
pub fn get_current_org_token(&self) -> Result<String, InfinyonCredentialError> {
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<AccessToken, InfinyonCredentialError> {
if let Ok(cli_access_tokens) = read_infinyon_token_v4() {
println!(
"Using org access: {}",
cli_access_tokens.get_current_org_name()?
);
return Ok(AccessToken::V4(cli_access_tokens));
}
let tok = read_infinyon_token_v3()?;
Ok(AccessToken::V3(tok))
}

pub fn read_infinyon_token() -> Result<InfinyonToken, InfinyonCredentialError> {
// 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);
if let Ok(cli_access_tokens) = read_infinyon_token_v4() {
tracing::debug!(
"using v4 token for org {}",
cli_access_tokens.get_current_org_name()?
);
return cli_access_tokens.get_current_org_token();
}
read_infinyon_token_v3()
}

pub fn read_infinyon_token_v4() -> Result<CliAccessTokens, InfinyonCredentialError> {
const CLOUD_BIN: &str = "fluvio-cloud";
const CLOUD_BIN_V4: &str = "fluvio-cloud-v4";
let res = read_infinyon_token_v4_cli(CLOUD_BIN_V4);
if res.is_err() {
read_infinyon_token_v4_cli(CLOUD_BIN)
} else {
res
}
}

fn read_infinyon_token_v4_cli(cloud_bin: &str) -> Result<CliAccessTokens, InfinyonCredentialError> {
let mut cmd = std::process::Command::new(cloud_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 multi-org output: {}\n$ {cloud_bin} 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 find multi-org login: {}", e);
Err(InfinyonCredentialError::Read(
"failed to find multi-org login".to_owned(),
))
}
}
}

// depcreated, will be removed after multi-org is stable
pub fn read_infinyon_token_v3() -> Result<InfinyonToken, InfinyonCredentialError> {
let cfgpath = default_file_path();
// this will read the indirection file to resolve the profile
let cred = Credentials::try_load(cfgpath)?;
Expand Down Expand Up @@ -100,6 +205,51 @@ fn default_file_path() -> String {
#[cfg(test)]
mod infinyon_tok_tests {
use super::read_infinyon_token;
use super::CliAccessTokens;
use serde_json;

// parse token options
#[test]
fn read_token_outputs() {
let with_uat = r#"
{
"remote": "https://infinyon.cloud",
"user_access_token": "uat_token",
"org_access_tokens": {
"inf-billing": "an_org_token"
}
}
"#;

let cli_access_tokens = serde_json::from_str::<CliAccessTokens>(with_uat);
assert!(cli_access_tokens.is_ok(), "{:?} ", cli_access_tokens);
let cli_access_tokens = cli_access_tokens.expect("should succeed");
let org_token = cli_access_tokens
.get_current_org_token()
.expect("retreiving org token");
assert_eq!(org_token, "an_org_token");
assert_eq!(
cli_access_tokens.user_access_token,
Some("uat_token".to_string())
);

let no_uat = r#"
{
"remote": "https://infinyon.cloud",
"org_access_tokens": {
"inf-billing": "an_org_token"
}
}
"#;
let cli_access_tokens = serde_json::from_str::<CliAccessTokens>(no_uat);
assert!(cli_access_tokens.is_ok(), "{:?} ", cli_access_tokens);
let cli_access_tokens = cli_access_tokens.expect("should succeed");
let org_token = cli_access_tokens
.get_current_org_token()
.expect("retreiving org token");
assert_eq!(org_token, "an_org_token");
assert_eq!(cli_access_tokens.user_access_token, None);
}

// load default credentials (ignore by default becasuse config is not populated in ci env)
#[ignore]
Expand Down
32 changes: 25 additions & 7 deletions crates/fluvio-hub-util/src/hubaccess.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -149,15 +150,16 @@ impl HubAccess {
action: &str,
authn_token: &str,
) -> Result<String> {
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<String> {
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<String> {
async fn make_action_token(&self, action: &str, token: Option<AccessToken>) -> Result<String> {
let host = &self.remote;
let api_url = format!("{host}/{HUB_API_ACT}");
let mat = MsgActionToken {
Expand All @@ -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())
Expand Down

0 comments on commit af91c88

Please sign in to comment.