Files
galaxy/app/src/external_secrets/mod.rs
T

353 lines
11 KiB
Rust

// Most of this module is dead code on web as it is not possible to retrieve
// external secrets from the browser.
#![cfg_attr(target_family = "wasm", allow(dead_code, unused_variables))]
use anyhow::anyhow;
use core::fmt;
use galaxy_util::path::ShellFamily;
use itertools::Itertools;
use lazy_static::lazy_static;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::path::PathBuf;
#[cfg(all(not(target_family = "wasm"), feature = "local_tty"))]
use crate::terminal::local_shell::execute_command;
use crate::{terminal::shell::ShellType, ui_components::icons::Icon};
lazy_static! {
// Used as a delimeter to separate metadata (such as names and references)
// in cases the cli tool doesn't display secrets in a common format (i.e. json)
static ref WARP_SECRET_DELIMETER: &'static str = "/warp-secret-delimeter/";
static ref LASTPASS_LIST_SECRETS_COMMAND: Vec<String> = {
vec![
"lpass".to_owned(),
"ls".to_owned(),
format!("--format=%an{}%ai", *WARP_SECRET_DELIMETER),
]
};
}
const ONE_PASSWORD_INSTALLED_COMMAND: [&str; 2] = ["op", "-v"];
// 1Password has more categories (logins, servers, etc),
// but we're limiting our support to these as categories
// may differ in the way they extract sensitive information
// (i.e. api credentials use the --credential field seen
// in get_secret_extraction_command, whereas logins use
// the --password field). We're waiting to see how users
// use secrets to inform what to support.
const ONE_PASSWORD_LIST_SECRETS_COMMAND: [&str; 6] = [
"op",
"item",
"list",
"--categories",
"Database,Api\\ Credential",
"--format=json",
];
const LASTPASS_INSTALLED_COMMAND: [&str; 2] = ["lpass", "-v"];
const ONEPASSWORD_DOCS_LINK: &str = "https://developer.1password.com/docs/cli/get-started/";
const LASTPASS_DOCS_LINK: &str = "https://github.com/lastpass/lastpass-cli";
/// Represents a "completed" secret
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub enum ExternalSecret {
OnePassword(OnePasswordSecret),
LastPass(LastPassSecret),
}
impl ExternalSecret {
pub fn get_secret_extraction_command(&self, shell_family: ShellFamily) -> String {
let prefix = match shell_family {
ShellFamily::Posix => "\\",
ShellFamily::PowerShell => "",
};
match self {
ExternalSecret::OnePassword(secret) => {
format!(
"{}op item get --fields credential --reveal {}",
prefix, secret.reference
)
}
ExternalSecret::LastPass(secret) => {
format!("{}lpass show --password {}", prefix, secret.reference)
}
}
}
pub fn get_display_name(&self) -> String {
match self {
ExternalSecret::OnePassword(secret) => secret.name.clone(),
ExternalSecret::LastPass(secret) => secret.name.clone(),
}
}
}
/// Used to check if a secret manager is installed/fetch list of secrets
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub enum SecretManager {
OnePassword,
LastPass,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub enum SecretErrorType {
NotInstalled,
FetchFailed,
InvalidPlatform,
}
pub struct ErrorMessageAndCommand {
pub message: String,
pub link_message: Option<String>,
pub link: Option<String>,
}
impl SecretManager {
async fn is_installed(
&self,
shell_type: ShellType,
shell_path: PathBuf,
path_env_var: Option<String>,
) -> bool {
#[cfg(all(not(target_family = "wasm"), feature = "local_tty"))]
{
match self {
SecretManager::OnePassword => {
return execute_command(
shell_type,
shell_path,
path_env_var,
ONE_PASSWORD_INSTALLED_COMMAND.join(" ").as_str(),
)
.await
.is_ok()
}
SecretManager::LastPass => {
return execute_command(
shell_type,
shell_path,
path_env_var,
LASTPASS_INSTALLED_COMMAND.join(" ").as_str(),
)
.await
.is_ok()
}
}
}
#[allow(unreachable_code)]
false
}
async fn fetch_secrets(
&self,
shell_type: ShellType,
shell_path: PathBuf,
path_env_var: Option<String>,
) -> Option<Vec<ExternalSecret>> {
#[cfg(all(not(target_family = "wasm"), feature = "local_tty"))]
{
match self {
SecretManager::OnePassword => {
return execute_command(
shell_type,
shell_path,
path_env_var,
ONE_PASSWORD_LIST_SECRETS_COMMAND.join(" ").as_str(),
)
.await
.ok()
.and_then(|output| parse_onepassword_secrets(&output).ok())
}
SecretManager::LastPass => {
let lastpass_command: Vec<&str> = LASTPASS_LIST_SECRETS_COMMAND
.iter()
.map(|s| s.as_str())
.collect();
return execute_command(
shell_type,
shell_path,
path_env_var,
lastpass_command.join(" ").as_str(),
)
.await
.ok()
.and_then(|output| parse_lastpass_secrets(&output).ok());
}
}
}
#[allow(unreachable_code)]
None
}
pub async fn verify_installed_and_fetch_secrets(
&self,
shell_type: ShellType,
shell_path: PathBuf,
path_env_var: Option<String>,
) -> Result<Vec<ExternalSecret>, SecretErrorType> {
#[cfg(not(target_family = "wasm"))]
{
let is_installed = self
.is_installed(shell_type, shell_path.clone(), path_env_var.clone())
.await;
if !is_installed {
return Err(SecretErrorType::NotInstalled);
}
let secrets = self
.fetch_secrets(shell_type, shell_path, path_env_var)
.await;
if let Some(secrets) = secrets {
return Ok(secrets);
} else {
return Err(SecretErrorType::FetchFailed);
}
}
#[allow(unreachable_code)]
Err(SecretErrorType::InvalidPlatform)
}
pub fn get_toast_message_and_link(
&self,
error_type: SecretErrorType,
) -> ErrorMessageAndCommand {
match error_type {
SecretErrorType::NotInstalled => {
let message = format!("{} CLI is not installed", &self);
let (link, link_message) = (
match self {
SecretManager::OnePassword => Some(ONEPASSWORD_DOCS_LINK.to_owned()),
SecretManager::LastPass => Some(LASTPASS_DOCS_LINK.to_owned()),
},
Some(format!("View {} CLI installation documentation", &self)),
);
ErrorMessageAndCommand {
message,
link,
link_message,
}
}
SecretErrorType::FetchFailed => {
let (link, link_message) = match self {
SecretManager::OnePassword => (
Some(ONEPASSWORD_DOCS_LINK.to_owned()),
Some("Integrate 1Password app with CLI".to_owned()),
),
SecretManager::LastPass => (None, None),
};
ErrorMessageAndCommand {
message: format!(
"{} didn't return secrets (likely not configured or authenticated)",
&self
),
link,
link_message,
}
}
SecretErrorType::InvalidPlatform => ErrorMessageAndCommand {
message: "Platform not supported".to_owned(),
link: None,
link_message: None,
},
}
}
}
impl fmt::Display for SecretManager {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
SecretManager::OnePassword => write!(f, "1Password"),
SecretManager::LastPass => write!(f, "LastPass"),
}
}
}
pub trait ExternalSecretManager {
fn icon(&self) -> Icon;
}
impl ExternalSecretManager for ExternalSecret {
fn icon(&self) -> Icon {
match self {
ExternalSecret::OnePassword(_) => Icon::OnePassword,
ExternalSecret::LastPass(_) => Icon::LastPass,
}
}
}
impl ExternalSecretManager for SecretManager {
fn icon(&self) -> Icon {
match self {
SecretManager::OnePassword => Icon::OnePassword,
SecretManager::LastPass => Icon::LastPass,
}
}
}
fn parse_onepassword_secrets(output: &str) -> anyhow::Result<Vec<ExternalSecret>> {
let parsed_output: Vec<ExternalSecret> = serde_json::from_str::<Value>(output)
.map_err(|e| anyhow!(e))?
.as_array()
.ok_or(anyhow!("Expected array in JSON"))?
.iter()
.map(|secret| {
let name = secret
.get("title")
.and_then(|v| v.as_str())
.ok_or(anyhow!("Secret is missing title"))?;
let reference = secret
.get("id")
.and_then(|v| v.as_str())
.ok_or(anyhow!("Secret is missing id"))?;
Ok(ExternalSecret::OnePassword(OnePasswordSecret {
name: name.to_string(),
reference: reference.to_string(),
}))
})
.collect::<Result<Vec<_>, anyhow::Error>>()?;
Ok(parsed_output)
}
fn parse_lastpass_secrets(output: &str) -> anyhow::Result<Vec<ExternalSecret>> {
let parsed_output: Vec<ExternalSecret> = output
.lines()
.filter_map(|line| {
let parts = line.split(*WARP_SECRET_DELIMETER).collect_vec();
if parts.len() == 2 && !parts[0].is_empty() && !parts[1].is_empty() {
Some(ExternalSecret::LastPass(LastPassSecret {
name: parts[0].to_owned(),
reference: parts[1].to_owned(),
}))
} else {
None
}
})
.collect();
if !parsed_output.is_empty() {
Ok(parsed_output)
} else {
Err(anyhow!("Failed to parse any secrets"))
}
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct OnePasswordSecret {
name: String,
reference: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct LastPassSecret {
name: String,
reference: String,
}