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

add: optional v4 token access #4212

Merged
merged 4 commits into from
Dec 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading