diff --git a/Cargo.lock b/Cargo.lock index df968c662..70c357288 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1565,6 +1565,28 @@ dependencies = [ "uuid", ] +[[package]] +name = "aws-sdk-secretsmanager" +version = "1.97.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e24a260f3767789990ac437493d2e102fe88d11bcd4a3f96fe8abb95dac37f0a" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes", + "fastrand 2.3.0", + "http 0.2.12", + "regex-lite", + "tracing", +] + [[package]] name = "aws-sdk-signin" version = "1.2.0" @@ -4046,6 +4068,15 @@ dependencies = [ "dirs-sys 0.3.7", ] +[[package]] +name = "dirs" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +dependencies = [ + "dirs-sys 0.4.1", +] + [[package]] name = "dirs" version = "6.0.0" @@ -4076,6 +4107,18 @@ dependencies = [ "winapi", ] +[[package]] +name = "dirs-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +dependencies = [ + "libc", + "option-ext", + "redox_users 0.4.3", + "windows-sys 0.48.0", +] + [[package]] name = "dirs-sys" version = "0.5.0" @@ -14395,6 +14438,7 @@ dependencies = [ "warp_server_client", "warp_terminal", "warp_util", + "warp_vault", "warp_web_event_bus", "warpui", "warpui_extras", @@ -14884,6 +14928,22 @@ dependencies = [ "windows 0.62.2", ] +[[package]] +name = "warp_vault" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-trait", + "aws-config", + "aws-sdk-secretsmanager", + "dirs 5.0.1", + "log", + "serde", + "tokio", + "toml 0.8.23", + "zeroize", +] + [[package]] name = "warp_web_event_bus" version = "0.0.0" @@ -15924,6 +15984,15 @@ dependencies = [ "windows-targets 0.42.2", ] +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + [[package]] name = "windows-sys" version = "0.52.0" @@ -15966,6 +16035,21 @@ dependencies = [ "windows_x86_64_msvc 0.42.2", ] +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + [[package]] name = "windows-targets" version = "0.52.6" @@ -16015,6 +16099,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" @@ -16027,6 +16117,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + [[package]] name = "windows_aarch64_msvc" version = "0.52.6" @@ -16039,6 +16135,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -16057,6 +16159,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + [[package]] name = "windows_i686_msvc" version = "0.52.6" @@ -16069,6 +16177,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + [[package]] name = "windows_x86_64_gnu" version = "0.52.6" @@ -16081,6 +16195,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" @@ -16093,6 +16213,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + [[package]] name = "windows_x86_64_msvc" version = "0.52.6" diff --git a/Cargo.toml b/Cargo.toml index 7ebcc9b39..ff39011fa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -81,6 +81,7 @@ warp_isolation_platform = { path = "crates/isolation_platform" } warp_js = { path = "crates/warp_js" } warp_logging = { path = "crates/warp_logging" } warp_managed_secrets = { path = "crates/managed_secrets" } +warp_vault = { path = "crates/vault" } warp_ripgrep = { path = "crates/warp_ripgrep" } warp_server_client = { path = "crates/warp_server_client" } warp_terminal = { path = "crates/warp_terminal" } diff --git a/app/Cargo.toml b/app/Cargo.toml index 2be9371f8..43d71d23b 100644 --- a/app/Cargo.toml +++ b/app/Cargo.toml @@ -250,6 +250,7 @@ rmcp = { workspace = true, features = ["client"] } warp_isolation_platform.workspace = true warp_ripgrep.workspace = true warp_managed_secrets.workspace = true +warp_vault.workspace = true [target.'cfg(target_os = "macos")'.dependencies] block.workspace = true diff --git a/app/src/ai/agent_sdk/mod.rs b/app/src/ai/agent_sdk/mod.rs index 54e693cbe..38db6d9af 100644 --- a/app/src/ai/agent_sdk/mod.rs +++ b/app/src/ai/agent_sdk/mod.rs @@ -37,6 +37,7 @@ use warp_cli::{ schedule::ScheduleSubcommand, secret::SecretCommand, share::ShareRequest, + vault::VaultCommand, task::{MessageCommand, TaskCommand}, CliCommand, GlobalOptions, }; @@ -101,6 +102,7 @@ pub(crate) mod retry; mod schedule; mod secret; mod telemetry; +mod vault; #[cfg(test)] mod test_support; mod text_layout; @@ -180,6 +182,9 @@ fn dispatch_command( } secret::run(ctx, global_options, secret_cmd) } + CliCommand::Vault(vault_cmd) => { + vault::run(ctx, global_options, vault_cmd) + } CliCommand::Federate(federate_cmd) => { if !FeatureFlag::OzIdentityFederation.is_enabled() { return Err(anyhow::anyhow!("invalid value 'federate'")); @@ -1279,6 +1284,7 @@ fn command_requires_auth(command: &CliCommand) -> bool { CliCommand::Integration(_) => true, CliCommand::Schedule(_) => true, CliCommand::Secret(_) => true, + CliCommand::Vault(_) => false, CliCommand::Federate(_) => true, CliCommand::HarnessSupport(_) => true, CliCommand::Artifact(_) => true, @@ -1478,6 +1484,7 @@ fn command_to_telemetry_event(command: &CliCommand) -> CliTelemetryEvent { SecretCommand::Update(_) => CliTelemetryEvent::SecretUpdate, SecretCommand::List(_) => CliTelemetryEvent::SecretList, }, + CliCommand::Vault(_) => CliTelemetryEvent::VaultInject, CliCommand::Federate(federate_cmd) => match federate_cmd { FederateCommand::IssueToken(_) => CliTelemetryEvent::FederateIssueToken, FederateCommand::IssueGcpToken(_) => CliTelemetryEvent::FederateIssueGcpToken, diff --git a/app/src/ai/agent_sdk/telemetry.rs b/app/src/ai/agent_sdk/telemetry.rs index 7880e9919..abb77d055 100644 --- a/app/src/ai/agent_sdk/telemetry.rs +++ b/app/src/ai/agent_sdk/telemetry.rs @@ -112,6 +112,8 @@ pub(super) enum CliTelemetryEvent { HarnessSupportNotifyUser, /// Executing `warp harness-support finish-task` HarnessSupportFinishTask { success: bool }, + /// Executing `oz vault inject` + VaultInject, } impl TelemetryEvent for CliTelemetryEvent { @@ -188,6 +190,7 @@ impl TelemetryEvent for CliTelemetryEvent { CliTelemetryEvent::HarnessSupportFinishTask { success } => { Some(json!({ "success": success })) } + CliTelemetryEvent::VaultInject => None, } } @@ -274,6 +277,7 @@ impl TelemetryEventDesc for CliTelemetryEventDiscriminants { CliTelemetryEventDiscriminants::HarnessSupportFinishTask => { "CLI.Execute.HarnessSupport.FinishTask" } + CliTelemetryEventDiscriminants::VaultInject => "CLI.Execute.Vault.Inject", } } @@ -396,6 +400,9 @@ impl TelemetryEventDesc for CliTelemetryEventDiscriminants { CliTelemetryEventDiscriminants::HarnessSupportFinishTask => { "Reported task completion via harness-support from the Warp CLI" } + CliTelemetryEventDiscriminants::VaultInject => { + "Injected secrets from AWS Secrets Manager via the Warp CLI" + } } } diff --git a/app/src/ai/agent_sdk/vault.rs b/app/src/ai/agent_sdk/vault.rs new file mode 100644 index 000000000..97c23b0f6 --- /dev/null +++ b/app/src/ai/agent_sdk/vault.rs @@ -0,0 +1,48 @@ +use anyhow::Result; +use warp_cli::{vault::VaultCommand, GlobalOptions}; +use warp_vault::{ + config::{ProviderConfig, ProviderType, SecretMapping, VaultConfig}, + fetch_secrets, + provider::aws::AwsProvider, +}; +use warpui::AppContext; + +pub fn run(_ctx: &mut AppContext, _global_options: GlobalOptions, command: VaultCommand) -> Result<()> { + match command { + VaultCommand::Inject(args) => { + let rt = tokio::runtime::Runtime::new()?; + rt.block_on(async { + let (mappings, provider_config) = + if let (Some(path), Some(env_var)) = (args.path, args.env_var) { + let provider_config = ProviderConfig { + provider_type: ProviderType::Aws, + region: None, + }; + (vec![SecretMapping::new(path, env_var)?], provider_config) + } else { + let config = VaultConfig::load()?; + let provider_config = config.provider; + (config.mappings()?, provider_config) + }; + + if mappings.is_empty() { + anyhow::bail!("vault: no mappings found — add entries to ~/.warp/vault.toml or pass both a path and --as flag"); + } + + let provider = match provider_config.provider_type { + ProviderType::Aws => AwsProvider::new(provider_config.region).await?, + }; + + let secrets = fetch_secrets(&provider, &mappings).await?; + + for secret in &secrets { + let escaped = secret.value().replace('\'', "'\\''"); + eprintln!("✓ {} ready", secret.env_var()); + println!("export {}='{}'", secret.env_var(), escaped); + } + + Ok(()) + }) + } + } +} diff --git a/crates/vault/Cargo.toml b/crates/vault/Cargo.toml new file mode 100644 index 000000000..49ca1aa48 --- /dev/null +++ b/crates/vault/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "warp_vault" +version = "0.1.0" +authors.workspace = true +publish.workspace = true +license.workspace = true +edition = "2024" + +[dependencies] +anyhow.workspace = true +async-trait.workspace = true +log.workspace = true +serde.workspace = true +toml = "0.8" +zeroize = "1.8" +dirs = "5" +tokio = { version = "1", features = ["full"] } +aws-config = { version = "1", features = ["behavior-version-latest"] } +aws-sdk-secretsmanager = "1" diff --git a/crates/vault/src/config.rs b/crates/vault/src/config.rs new file mode 100644 index 000000000..15c032602 --- /dev/null +++ b/crates/vault/src/config.rs @@ -0,0 +1,74 @@ +#[cfg(test)] +#[path = "config_tests.rs"] +mod tests; + +use std::{collections::HashMap, fs, path::PathBuf}; + +use anyhow::{Context, Result}; +use serde::Deserialize; + +#[derive(Debug, Deserialize)] +pub struct VaultConfig { + pub provider: ProviderConfig, + #[serde(default)] + pub mappings: HashMap, +} + +#[derive(Debug, Deserialize)] +pub struct ProviderConfig { + #[serde(rename = "type")] + pub provider_type: ProviderType, + pub region: Option, +} + +#[derive(Debug, Deserialize, PartialEq)] +#[serde(rename_all = "lowercase")] +pub enum ProviderType { + Aws, +} + +pub struct SecretMapping { + pub path: String, + env_var: String, +} + +impl SecretMapping { + pub fn new(path: String, env_var: String) -> anyhow::Result { + if !crate::provider::is_valid_env_var(&env_var) { + anyhow::bail!( + "vault: invalid environment variable name '{}' — must contain only letters, digits, and underscores", + env_var + ); + } + Ok(Self { path, env_var }) + } + + pub fn env_var(&self) -> &str { + &self.env_var + } +} + +impl VaultConfig { + pub fn load() -> Result { + let path = config_path()?; + let contents = fs::read_to_string(&path).with_context(|| { + format!( + "no config found at {}. Create it with:\n\n [provider]\n type = \"aws\"\n region = \"us-east-1\"\n\n [mappings]\n \"your/secret/path\" = \"ENV_VAR_NAME\"", + path.display() + ) + })?; + toml::from_str(&contents).context("failed to parse vault config") + } + + pub fn mappings(&self) -> anyhow::Result> { + self.mappings + .iter() + .map(|(path, env_var)| SecretMapping::new(path.clone(), env_var.clone())) + .collect() + } +} + +fn config_path() -> Result { + let home = dirs::home_dir().ok_or_else(|| anyhow::anyhow!("vault: could not determine home directory"))?; + Ok(home.join(".warp").join("vault.toml")) +} diff --git a/crates/vault/src/config_tests.rs b/crates/vault/src/config_tests.rs new file mode 100644 index 000000000..e1abe1130 --- /dev/null +++ b/crates/vault/src/config_tests.rs @@ -0,0 +1,84 @@ +use crate::config::{ProviderType, VaultConfig}; + +#[test] +fn test_parse_valid_config() { + let toml = r#" + [provider] + type = "aws" + region = "us-east-1" + + [mappings] + "prod/api-key" = "API_KEY" + "prod/db-password" = "DB_PASSWORD" + "#; + + let config: VaultConfig = toml::from_str(toml).unwrap(); + assert_eq!(config.provider.provider_type, ProviderType::Aws); + assert_eq!(config.provider.region, Some("us-east-1".to_string())); + assert_eq!(config.mappings.len(), 2); + assert_eq!(config.mappings.get("prod/api-key").unwrap(), "API_KEY"); +} + +#[test] +fn test_parse_config_no_region() { + let toml = r#" + [provider] + type = "aws" + "#; + + let config: VaultConfig = toml::from_str(toml).unwrap(); + assert_eq!(config.provider.region, None); +} + +#[test] +fn test_parse_config_empty_mappings() { + let toml = r#" + [provider] + type = "aws" + "#; + + let config: VaultConfig = toml::from_str(toml).unwrap(); + assert!(config.mappings().unwrap().is_empty()); +} + +#[test] +fn test_mappings_returns_correct_pairs() { + let toml = r#" + [provider] + type = "aws" + + [mappings] + "prod/secret" = "MY_SECRET" + "#; + + let config: VaultConfig = toml::from_str(toml).unwrap(); + let mappings = config.mappings().unwrap(); + assert_eq!(mappings.len(), 1); + assert_eq!(mappings[0].path, "prod/secret"); + assert_eq!(mappings[0].env_var(), "MY_SECRET"); +} + +#[test] +fn test_parse_invalid_provider_type() { + let toml = r#" + [provider] + type = "gcp" + "#; + + let result: Result = toml::from_str(toml); + assert!(result.is_err()); +} + +#[test] +fn test_mappings_rejects_invalid_env_var() { + let toml = r#" + [provider] + type = "aws" + + [mappings] + "prod/secret" = "INVALID NAME" + "#; + + let config: VaultConfig = toml::from_str(toml).unwrap(); + assert!(config.mappings().is_err()); +} diff --git a/crates/vault/src/injector.rs b/crates/vault/src/injector.rs new file mode 100644 index 000000000..5922c6ad9 --- /dev/null +++ b/crates/vault/src/injector.rs @@ -0,0 +1,16 @@ +use anyhow::Result; + +use crate::config::SecretMapping; +use crate::provider::{Secret, SecretProvider}; + +pub async fn fetch_secrets( + provider: &dyn SecretProvider, + mappings: &[SecretMapping], +) -> Result> { + let mut secrets = Vec::new(); + for mapping in mappings { + let secret = provider.fetch(&mapping.path, mapping.env_var()).await?; + secrets.push(secret); + } + Ok(secrets) +} diff --git a/crates/vault/src/lib.rs b/crates/vault/src/lib.rs new file mode 100644 index 000000000..8c9395db2 --- /dev/null +++ b/crates/vault/src/lib.rs @@ -0,0 +1,7 @@ +pub mod config; +mod injector; +pub mod provider; + +pub use config::{SecretMapping, VaultConfig}; +pub use injector::fetch_secrets; +pub use provider::is_valid_env_var; diff --git a/crates/vault/src/provider/aws.rs b/crates/vault/src/provider/aws.rs new file mode 100644 index 000000000..bb10a6153 --- /dev/null +++ b/crates/vault/src/provider/aws.rs @@ -0,0 +1,42 @@ +use anyhow::Result; +use async_trait::async_trait; +use aws_sdk_secretsmanager::Client; + +use super::{Secret, SecretProvider}; + +pub struct AwsProvider { + client: Client, +} + +impl AwsProvider { + pub async fn new(region: Option) -> Result { + let mut config_loader = aws_config::defaults(aws_config::BehaviorVersion::latest()); + if let Some(region) = region { + config_loader = config_loader.region(aws_config::Region::new(region)); + } + let config = config_loader.load().await; + Ok(Self { + client: Client::new(&config), + }) + } +} + +#[async_trait] +impl SecretProvider for AwsProvider { + async fn fetch(&self, path: &str, env_var: &str) -> Result { + let response = self + .client + .get_secret_value() + .secret_id(path) + .send() + .await + .map_err(|e| anyhow::anyhow!("vault: failed to fetch '{}': {}", path, e))?; + + let value = response + .secret_string() + .ok_or_else(|| anyhow::anyhow!("vault: secret '{}' has no string value", path))? + .to_string(); + + Secret::new(env_var.to_string(), value) + } +} diff --git a/crates/vault/src/provider/mod.rs b/crates/vault/src/provider/mod.rs new file mode 100644 index 000000000..644dc2ced --- /dev/null +++ b/crates/vault/src/provider/mod.rs @@ -0,0 +1,50 @@ +pub mod aws; + +#[cfg(test)] +#[path = "provider_tests.rs"] +mod tests; + +use anyhow::{bail, Result}; +use async_trait::async_trait; + +pub struct Secret { + env_var: String, + value: zeroize::Zeroizing, +} + +impl Secret { + pub fn new(env_var: String, value: String) -> Result { + if !is_valid_env_var(&env_var) { + bail!( + "vault: invalid environment variable name '{}' — must contain only letters, digits, and underscores", + env_var + ); + } + Ok(Self { + env_var, + value: zeroize::Zeroizing::new(value), + }) + } + + pub fn env_var(&self) -> &str { + &self.env_var + } + + pub fn value(&self) -> &str { + &self.value + } +} + +pub fn is_valid_env_var(name: &str) -> bool { + let mut chars = name.chars(); + match chars.next() { + Some(c) if c.is_ascii_alphabetic() || c == '_' => {} + _ => return false, + } + chars.all(|c| c.is_ascii_alphanumeric() || c == '_') +} + +#[async_trait] +pub trait SecretProvider: Send + Sync { + async fn fetch(&self, path: &str, env_var: &str) -> Result; +} diff --git a/crates/vault/src/provider/provider_tests.rs b/crates/vault/src/provider/provider_tests.rs new file mode 100644 index 000000000..1f08190d3 --- /dev/null +++ b/crates/vault/src/provider/provider_tests.rs @@ -0,0 +1,47 @@ +use crate::provider::{is_valid_env_var, Secret}; + +#[test] +fn test_secret_new_valid() { + let s = Secret::new("API_KEY".to_string(), "mysecret".to_string()); + assert!(s.is_ok()); +} + +#[test] +fn test_secret_new_rejects_shell_injection() { + let s = Secret::new("FOO$(cmd)".to_string(), "value".to_string()); + assert!(s.is_err()); +} + +#[test] +fn test_secret_new_rejects_space() { + let s = Secret::new("FOO BAR".to_string(), "value".to_string()); + assert!(s.is_err()); +} + +#[test] +fn test_secret_new_rejects_leading_digit() { + let s = Secret::new("1FOO".to_string(), "value".to_string()); + assert!(s.is_err()); +} + +#[test] +fn test_secret_new_allows_underscore_prefix() { + let s = Secret::new("_FOO".to_string(), "value".to_string()); + assert!(s.is_ok()); +} + +#[test] +fn test_env_var_getter_matches_input() { + let s = Secret::new("MY_KEY".to_string(), "val".to_string()).unwrap(); + assert_eq!(s.env_var(), "MY_KEY"); +} + +#[test] +fn test_is_valid_env_var() { + assert!(is_valid_env_var("VALID_KEY")); + assert!(is_valid_env_var("_PRIVATE")); + assert!(!is_valid_env_var("1INVALID")); + assert!(!is_valid_env_var("FOO BAR")); + assert!(!is_valid_env_var("FOO$(cmd)")); + assert!(!is_valid_env_var("")); +} diff --git a/crates/warp_cli/src/lib.rs b/crates/warp_cli/src/lib.rs index 96a7ad799..6550af855 100644 --- a/crates/warp_cli/src/lib.rs +++ b/crates/warp_cli/src/lib.rs @@ -32,6 +32,7 @@ pub mod schedule; pub mod secret; pub mod share; pub mod task; +pub mod vault; pub const OZ_RUN_ID_ENV: &str = "OZ_RUN_ID"; pub const OZ_PARENT_RUN_ID_ENV: &str = "OZ_PARENT_RUN_ID"; pub const OZ_CLI_ENV: &str = "OZ_CLI"; @@ -524,6 +525,10 @@ pub enum CliCommand { #[command(subcommand)] Secret(crate::secret::SecretCommand), + /// Inject secrets from external vaults into the current shell session. + #[command(subcommand)] + Vault(crate::vault::VaultCommand), + /// Issue and manage federated identity tokens. #[command(subcommand)] Federate(crate::federate::FederateCommand), diff --git a/crates/warp_cli/src/vault.rs b/crates/warp_cli/src/vault.rs new file mode 100644 index 000000000..7b65dd1c4 --- /dev/null +++ b/crates/warp_cli/src/vault.rs @@ -0,0 +1,15 @@ +use clap::{Args, Subcommand}; + +#[derive(Debug, Clone, Subcommand)] +pub enum VaultCommand { + Inject(InjectArgs), +} + +#[derive(Debug, Clone, Args)] +pub struct InjectArgs { + #[arg(requires = "env_var")] + pub path: Option, + + #[arg(long = "as", requires = "path")] + pub env_var: Option, +}