Skip to content

Commit

Permalink
feat(cli): fallback to plain text for tokens
Browse files Browse the repository at this point in the history
  • Loading branch information
Nuttymoon committed Nov 30, 2023
1 parent 8e8fa3a commit 13ff74e
Show file tree
Hide file tree
Showing 6 changed files with 168 additions and 31 deletions.
1 change: 1 addition & 0 deletions crates/ash_cli/src/console.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ enum ConsoleSubcommands {
const KEYRING_TARGET: &str = "ash-console";
const KEYRING_ACCESS_TOKEN_SERVICE: &str = "access_token";
const KEYRING_REFRESH_TOKEN_SERVICE: &str = "refresh_token";
const KEYRING_FALLBACK_FILES_DIR: &str = "~/.ash-console/tokens";

// Load the console configuation
fn load_console(config: Option<&str>) -> Result<AshConsole, CliError> {
Expand Down
58 changes: 45 additions & 13 deletions crates/ash_cli/src/console/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@

use crate::{
console::{
load_console, KEYRING_ACCESS_TOKEN_SERVICE, KEYRING_REFRESH_TOKEN_SERVICE, KEYRING_TARGET,
load_console, KEYRING_ACCESS_TOKEN_SERVICE, KEYRING_FALLBACK_FILES_DIR,
KEYRING_REFRESH_TOKEN_SERVICE, KEYRING_TARGET,
},
utils::{error::CliError, keyring::*, templating::*, version_tx_cmd},
};
Expand All @@ -24,7 +25,9 @@ pub(crate) struct AuthCommand {

#[derive(Subcommand)]
enum AuthSubcommands {
/// Login to the Console. Credentials are stored in the device keyring.
/// Login to the Console
///
/// Credentials are stored in the device keyring or in plain text files if keyring is not available.
#[command(version = version_tx_cmd(false))]
Login,
/// Refresh the Console access token
Expand All @@ -33,7 +36,9 @@ enum AuthSubcommands {
/// Show the current Console access token
#[command(version = version_tx_cmd(false))]
ShowToken,
/// Logout from the Console. Credentials are removed from the device keyring.
/// Logout from the Console
///
/// Credentials are removed from the device keyring or from plain text files if keyring is not available.
#[command(version = version_tx_cmd(false))]
Logout,
/// Displays information about the Console authentication state
Expand Down Expand Up @@ -66,7 +71,11 @@ pub(crate) fn refresh_keyring_access_token(console: &mut AshConsole) -> Result<(
console.oauth2.init();

// Get the refresh token from the keyring
let refresh_token = get_keyring_value(KEYRING_TARGET, KEYRING_REFRESH_TOKEN_SERVICE)?;
let refresh_token = get_keyring_value(
KEYRING_TARGET,
KEYRING_REFRESH_TOKEN_SERVICE,
KEYRING_FALLBACK_FILES_DIR,
)?;

// Exchange the refresh token for a new access token
let access_token = console
Expand All @@ -79,14 +88,19 @@ pub(crate) fn refresh_keyring_access_token(console: &mut AshConsole) -> Result<(
KEYRING_TARGET,
KEYRING_ACCESS_TOKEN_SERVICE,
&access_token.secret().to_string(),
KEYRING_FALLBACK_FILES_DIR,
)?;

Ok(())
}

// Get the current access token from the keyring
pub(crate) fn get_keyring_access_token() -> Result<String, CliError> {
get_keyring_value(KEYRING_TARGET, KEYRING_ACCESS_TOKEN_SERVICE)
get_keyring_value(
KEYRING_TARGET,
KEYRING_ACCESS_TOKEN_SERVICE,
KEYRING_FALLBACK_FILES_DIR,
)
}

// Decode the access token to get its token data
Expand All @@ -113,7 +127,7 @@ pub(crate) fn get_access_token(console: &mut AshConsole) -> Result<String, CliEr
// Get the access token from the keyring
let access_token = get_keyring_access_token().map_err(|_| {
CliError::dataerr(
"Error getting access token from keyring. You are probably not logged in (try `ash console auth status`).".to_string()
"Error getting local access token. You are probably not logged in (try `ash console auth status`).".to_string()
)
})?;

Expand Down Expand Up @@ -160,16 +174,22 @@ fn login(config: Option<&str>) -> Result<(), CliError> {
KEYRING_TARGET,
KEYRING_ACCESS_TOKEN_SERVICE,
&access_token.secret().to_string(),
KEYRING_FALLBACK_FILES_DIR,
)?;
set_keyring_value(
let keyring_available = set_keyring_value(
KEYRING_TARGET,
KEYRING_REFRESH_TOKEN_SERVICE,
&refresh_token.secret().to_string(),
KEYRING_FALLBACK_FILES_DIR,
)?;

println!(
"\n{} The credentials have been stored in your device keyring.",
"Login successful!".green()
"\n{} The credentials have been stored in {}",
"Login successful!".green(),
match keyring_available {
true => "your device keyring".to_string(),
false => format!("plain text files in '{}'", KEYRING_FALLBACK_FILES_DIR),
}
);

Ok(())
Expand Down Expand Up @@ -242,12 +262,24 @@ fn logout(config: Option<&str>) -> Result<(), CliError> {
}

// Delete the access token and refresh token from the keyring
delete_keyring_value(KEYRING_TARGET, KEYRING_ACCESS_TOKEN_SERVICE)?;
delete_keyring_value(KEYRING_TARGET, KEYRING_REFRESH_TOKEN_SERVICE)?;
delete_keyring_value(
KEYRING_TARGET,
KEYRING_ACCESS_TOKEN_SERVICE,
KEYRING_FALLBACK_FILES_DIR,
)?;
let keyring_available = delete_keyring_value(
KEYRING_TARGET,
KEYRING_REFRESH_TOKEN_SERVICE,
KEYRING_FALLBACK_FILES_DIR,
)?;

println!(
"\n{} The credentials have been removed from your device keyring.",
"Logout successful!".green()
"\n{} The credentials have been removed from {}",
"Logout successful!".green(),
match keyring_available {
true => "your device keyring".to_string(),
false => format!("plain text files in '{}'", KEYRING_FALLBACK_FILES_DIR),
}
);

Ok(())
Expand Down
2 changes: 1 addition & 1 deletion crates/ash_cli/src/console/project.rs
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ fn create(project: &str, config: Option<&str>, json: bool) -> Result<(), CliErro
let switch_message = format!(
"Switched to project '{}' ({})!",
response.name.unwrap_or_default(),
response.id.unwrap_or_default().to_string()
response.id.unwrap_or_default()
)
.green();

Expand Down
6 changes: 3 additions & 3 deletions crates/ash_cli/src/console/resource.rs
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ fn info(
let api_config = create_api_config_with_access_token(&mut console)?;

let response = task::block_on(async {
console::api::get_project_resource_by_id(&api_config, project_id, &resource_id).await
console::api::get_project_resource_by_id(&api_config, project_id, resource_id).await
})
.map_err(|e| CliError::dataerr(format!("Error getting resource: {e}")))?;

Expand Down Expand Up @@ -253,7 +253,7 @@ fn delete(
}

let response = task::block_on(async {
console::api::delete_project_resource_by_id(&api_config, project_id, &resource_id).await
console::api::delete_project_resource_by_id(&api_config, project_id, resource_id).await
})
.map_err(|e| CliError::dataerr(format!("Error removing resource: {e}")))?;

Expand Down Expand Up @@ -289,7 +289,7 @@ fn restart(
}

let response = task::block_on(async {
console::api::restart_project_resource_by_id(&api_config, project_id, &resource_id).await
console::api::restart_project_resource_by_id(&api_config, project_id, resource_id).await
})
.map_err(|e| CliError::dataerr(format!("Error restarting resource: {e}")))?;

Expand Down
130 changes: 117 additions & 13 deletions crates/ash_cli/src/utils/keyring.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,132 @@
// Copyright (c) 2023, E36 Knots

use crate::utils::error::CliError;
use keyring::Entry;
use colored::Colorize;
use keyring::{Entry, Error};
use std::{fs, path::Path};

/// Store a value in the device keyring
pub(crate) fn set_keyring_value(target: &str, service: &str, value: &str) -> Result<(), CliError> {
Entry::new_with_target(target, service, &whoami::username())
/// Returns true if the value was stored in the keyring, false if it was stored in a plain text file
pub(crate) fn set_keyring_value(
target: &str,
service: &str,
value: &str,
fallback_files_dir: &str,
) -> Result<bool, CliError> {
let new_entry = Entry::new_with_target(target, service, &whoami::username())
.map_err(|e| CliError::dataerr(format!("Error storing access token: {e}")))?
.set_password(value)
.map_err(|e| CliError::dataerr(format!("Error storing access token: {e}")))
.set_password(value);

match new_entry {
Ok(_) => Ok(true),
Err(Error::PlatformFailure(_)) => {
eprintln!("{}", "Your platform does not support keyring storage. Falling back to plain text storage.".red());
write_plaintext_file(service, value, fallback_files_dir)?;
Ok(false)
}
Err(e) => Err(CliError::dataerr(format!(
"Error storing access token: {e}"
))),
}
}

/// Get a value from the device keyring
pub(crate) fn get_keyring_value(target: &str, service: &str) -> Result<String, CliError> {
Entry::new_with_target(target, service, &whoami::username())
pub(crate) fn get_keyring_value(
target: &str,
service: &str,
fallback_files_dir: &str,
) -> Result<String, CliError> {
let new_entry = Entry::new_with_target(target, service, &whoami::username())
.map_err(|e| CliError::dataerr(format!("Error getting access token: {e}")))?
.get_password()
.map_err(|e| CliError::dataerr(format!("Error getting access token: {e}")))
.get_password();

match new_entry {
Ok(entry) => Ok(entry),
Err(Error::PlatformFailure(_)) => read_plaintext_file(service, fallback_files_dir),
Err(e) => Err(CliError::dataerr(format!(
"Error getting access token: {e}"
))),
}
}

/// Remove a value from the device keyring
pub(crate) fn delete_keyring_value(target: &str, service: &str) -> Result<(), CliError> {
Entry::new_with_target(target, service, &whoami::username())
/// Returns true if the value was removed from the keyring, false if it was removed from a plain text file
pub(crate) fn delete_keyring_value(
target: &str,
service: &str,
fallback_files_dir: &str,
) -> Result<bool, CliError> {
let new_entry = Entry::new_with_target(target, service, &whoami::username())
.map_err(|e| CliError::dataerr(format!("Error removing access token: {e}")))?
.delete_password()
.map_err(|e| CliError::dataerr(format!("Error removing access token: {e}")))
.delete_password();

match new_entry {
Ok(_) => Ok(true),
Err(Error::PlatformFailure(_)) => {
delete_plaintext_file(service, fallback_files_dir)?;
Ok(false)
}
Err(e) => Err(CliError::dataerr(format!(
"Error removing access token: {e}"
))),
}
}

/// Store a value in a plain text file
/// This is used as a fallback if the device does not support keyring storage
fn write_plaintext_file(
service: &str,
value: &str,
fallback_files_dir: &str,
) -> Result<(), CliError> {
let plaintext_dir = shellexpand::tilde(fallback_files_dir).to_string();
let plaintext_dir_path = Path::new(&plaintext_dir);

if !plaintext_dir_path.exists() {
fs::create_dir_all(plaintext_dir_path)
.map_err(|e| CliError::dataerr(format!("Error creating output directory: {e}")))?;
}

let plaintext_file_path = plaintext_dir_path.join(service);

fs::write(plaintext_file_path, value)
.map_err(|e| CliError::dataerr(format!("Error writing plain text file: {e}")))?;

Ok(())
}

/// Get a value from a plain text file
/// This is used as a fallback if the device does not support keyring storage
fn read_plaintext_file(service: &str, fallback_files_dir: &str) -> Result<String, CliError> {
let plaintext_dir = shellexpand::tilde(fallback_files_dir).to_string();
let plaintext_dir_path = Path::new(&plaintext_dir);

if !plaintext_dir_path.exists() {
CliError::dataerr(format!(
"Plain text storage directory does not exist: {plaintext_dir}"
));
}

let plaintext_file_path = plaintext_dir_path.join(service);

fs::read_to_string(plaintext_file_path)
.map_err(|e| CliError::dataerr(format!("Error reading plain text file: {e}")))
}

/// Delete a plain text file
/// This is used as a fallback if the device does not support keyring storage
fn delete_plaintext_file(service: &str, fallback_files_dir: &str) -> Result<(), CliError> {
let plaintext_dir = shellexpand::tilde(fallback_files_dir).to_string();
let plaintext_dir_path = Path::new(&plaintext_dir);

if !plaintext_dir_path.exists() {
CliError::dataerr(format!(
"Plain text storage directory does not exist: {plaintext_dir}"
));
}

let plaintext_file_path = plaintext_dir_path.join(service);

fs::remove_file(plaintext_file_path)
.map_err(|e| CliError::dataerr(format!("Error removing plain text file: {e}")))
}
2 changes: 1 addition & 1 deletion crates/ash_cli/src/utils/templating.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1060,7 +1060,7 @@ pub(crate) fn template_avalanche_node_props_table(
),
]);

return props_table;
props_table
}

pub(crate) fn template_resources_table(
Expand Down

0 comments on commit 13ff74e

Please sign in to comment.