diff --git a/secrets/secrets-vault/Cargo.toml b/secrets/secrets-vault/Cargo.toml index 7e4bffd..99bd20a 100644 --- a/secrets/secrets-vault/Cargo.toml +++ b/secrets/secrets-vault/Cargo.toml @@ -23,9 +23,9 @@ tokio = { version = "1.38.0", default-features = false, features = ["full"] } tracing = { version = "0.1.40", default-features = false, features = [] } tracing-subscriber = { version = "0.3.18", default-features = false, features = ["fmt", "env-filter"] } vaultrs = { version = "0.7.2", default-features = false, features = ["rustls"] } -wasmcloud-secrets-types = { version = "0.1.0" } +wasmcloud-secrets-types = { version = "0.2.0" } wascap = { version = "0.15.0" } [dev-dependencies] testcontainers = { version = "0.20.0", default-features = false } -wasmcloud-secrets-client = { version = "0.1.0" } +wasmcloud-secrets-client = { version = "0.2.0" } diff --git a/secrets/secrets-vault/src/lib.rs b/secrets/secrets-vault/src/lib.rs index 5a9505a..9adec03 100644 --- a/secrets/secrets-vault/src/lib.rs +++ b/secrets/secrets-vault/src/lib.rs @@ -6,7 +6,7 @@ use jsonwebtoken::{get_current_timestamp, Algorithm, EncodingKey}; use nkeys::{KeyPair, XKey}; use serde::{Deserialize, Serialize}; use std::{net::SocketAddrV4, result::Result as StdResult, str::FromStr}; -use tracing::debug; +use tracing::info; use vaultrs::{ api::kv2::{requests::ReadSecretRequest, responses::ReadSecretResponse}, client::{Client, VaultClient, VaultClientSettingsBuilder}, @@ -98,8 +98,17 @@ impl TryFrom<&SecretRequest> for VaultPolicy { type Error = anyhow::Error; fn try_from(request: &SecretRequest) -> StdResult { - serde_json::from_str::(&request.context.application.policy) - .map_err(|e| anyhow!("failed to deserialize policy: {}", e.to_string())) + let policy = serde_json::from_str::(&request.context.application.policy) + .map_err(|e| anyhow!("failed to extract policy: {}", e.to_string()))?; + let properties = policy + .get("properties") + .ok_or_else(|| anyhow!("failed to extract policy properties"))?; + serde_json::from_str::(&properties.to_string()).map_err(|e| { + anyhow!( + "failed to deserialize vault policy from properties: {}", + e.to_string() + ) + }) } } @@ -118,7 +127,8 @@ struct VaultAuthClaims { } struct VaultSecretRef { - secret_path: String, + path: String, + field: Option, version: Option, } @@ -126,18 +136,18 @@ impl TryFrom<&SecretRequest> for VaultSecretRef { type Error = anyhow::Error; fn try_from(request: &SecretRequest) -> StdResult { - let secret_path = request.name.clone(); - let version = if let Some(version) = request.version.clone() { - version - .parse::() - .map(Some) - .map_err(|_| anyhow!("unable to convert requested version to integer"))? - } else { - None - }; + let version = request + .version + .to_owned() + .map(|v| { + v.parse::() + .map_err(|_| anyhow!("unable to convert requested version to integer")) + }) + .transpose()?; Ok(Self { - secret_path, + path: request.key.to_owned(), + field: request.field.to_owned(), version, }) } @@ -225,7 +235,7 @@ impl VaultSecretsBackend { } async fn start_nats_subscriber(&self) -> Result<()> { - debug!( + info!( "Subscribing to messages addressed to {} under queue group {}", self.subject_mapper.secrets_wildcard_subject(), self.subject_mapper.queue_group_name(), @@ -323,20 +333,30 @@ impl VaultSecretsBackend { let secret_ref = VaultSecretRef::try_from(&secret_request) .map_err(|e| GetSecretError::Other(e.to_string()))?; - let secret = Self::fetch_secret( + let response = Self::fetch_secret( &vault_client, &policy .secret_engine_path .unwrap_or_else(|| self.vault_config.default_secret_engine.to_owned()), - secret_ref, + &secret_ref, ) .await?; + let secret_version = response.metadata.version.to_string(); + let secret = if let Some(field) = secret_ref.field { + response + .data + .get(field) + .map(|v| v.as_str().unwrap()) + .map(ToString::to_string) + } else { + Some(response.data.to_string()) + }; + let secret_response = SecretResponse { secret: Some(Secret { - name: secret_request.name, - version: secret.metadata.version.to_string(), - string_secret: Some(secret.data.to_string()), + version: secret_version, + string_secret: secret, binary_secret: None, }), error: None, @@ -480,11 +500,11 @@ impl VaultSecretsBackend { async fn fetch_secret( client: &VaultClient, mount: &str, - secret_ref: VaultSecretRef, + secret_ref: &VaultSecretRef, ) -> Result { let request = ReadSecretRequest::builder() .mount(mount) - .path(secret_ref.secret_path) + .path(&secret_ref.path) .version(secret_ref.version) .build() .unwrap(); diff --git a/secrets/secrets-vault/src/main.rs b/secrets/secrets-vault/src/main.rs index a10b5e7..bdc49fa 100644 --- a/secrets/secrets-vault/src/main.rs +++ b/secrets/secrets-vault/src/main.rs @@ -21,7 +21,7 @@ struct Args { struct NatsClientOpts { /// NATS Server address to connect to listen for secrets requests. #[arg(long = "nats-address", env = "SV_NATS_ADDRESS")] - pub address: String, + pub nats_address: String, /// JWT for authenticating the NATS connection #[arg(long = "nats-jwt", env = "SV_NATS_JWT", requires = "seed")] @@ -42,7 +42,7 @@ impl NatsClientOpts { async move { kp.sign(&nonce).map_err(async_nats::AuthError::new) } }); } - Ok(async_nats::connect_with_options(self.address, options).await?) + Ok(async_nats::connect_with_options(self.nats_address, options).await?) } } @@ -81,7 +81,7 @@ struct SecretsServerOpts { struct VaultOpts { /// Vault server address to connect to. #[arg(long = "vault-address", env = "SV_VAULT_ADDRESS")] - pub address: String, + pub vault_address: String, /// Path where the JWT auth method is mounted #[arg(long = "vault-auth-method", env = "SV_VAULT_AUTH_METHOD")] @@ -110,7 +110,7 @@ struct VaultOpts { impl From for VaultConfig { fn from(opts: VaultOpts) -> Self { Self { - address: opts.address, + address: opts.vault_address, auth_mount: opts.auth_method_path.trim_matches('/').to_owned(), jwt_audience: opts.jwt_audience, default_secret_engine: opts.default_secret_engine.trim_matches('/').to_owned(), diff --git a/secrets/secrets-vault/tests/integration.rs b/secrets/secrets-vault/tests/integration.rs index ec5161d..ef1e4ec 100644 --- a/secrets/secrets-vault/tests/integration.rs +++ b/secrets/secrets-vault/tests/integration.rs @@ -4,7 +4,7 @@ use std::{collections::HashMap, net::SocketAddrV4}; use anyhow::Result; use nkeys::{KeyPair, XKey}; use secrets_vault::{SubjectMapper, VaultConfig, VaultSecretsBackend}; -use serde::{Deserialize, Serialize}; +use serde::Serialize; use serde_json::json; use testcontainers::{ core::{Host as TestHost, WaitFor}, @@ -30,11 +30,6 @@ const NATS_SERVER_PORT: u16 = 4222; const VAULT_SERVER_PORT: u16 = 8200; const VAULT_ROOT_TOKEN_ID: &str = "vault-root-token-id"; -#[derive(Serialize, Deserialize)] -struct StoredSecret { - value: String, -} - #[tokio::test] async fn test_server_xkey() -> Result<()> { let xkey = nkeys::XKey::new(); @@ -148,20 +143,17 @@ async fn test_get() -> Result<()> { configure_vault_jwt_auth(&vault_client, resolve_jwks_url(jwks_port)).await?; - let stored_secret = StoredSecret { - value: "this-is-a-secret".to_string(), - }; + let secret_key = "secret-key"; + let stored_secret = HashMap::from([(secret_key, "this-is-a-secret")]); + store_secret_in_engine_at_path( &vault_client, - &json!(stored_secret), + stored_secret.clone(), SECRETS_ENGINE_MOUNT, SECRETS_SECRET_NAME, ) .await?; - //let nats_wrpc_client = async_nats::connect(nats_address) - //.await - //.expect("should be able to create a connection to nats"); let wsc = wasmcloud_secrets_client::Client::new("vault-test", "wasmcloud.secrets", nats_client) .await .expect("should be able to instantiate wasmcloud-secrets-client"); @@ -182,7 +174,8 @@ async fn test_get() -> Result<()> { let request_xkey = XKey::new(); let secret_request = SecretRequest { - name: SECRETS_SECRET_NAME.to_string(), + key: SECRETS_SECRET_NAME.to_string(), + field: Some(secret_key.to_owned()), version: None, context: SecretsContext { entity_jwt: entity_claims.encode(&claims_signer).unwrap(), @@ -190,8 +183,10 @@ async fn test_get() -> Result<()> { application: Application { name: Some("test-app".to_string()), policy: json!({ - "role_name": SECRETS_ROLE_NAME, - "namespace": "foobar" + "type": "properties.secret.wasmcloud.dev/v1alpha1", + "properties": { + "roleName": SECRETS_ROLE_NAME, + } }) .to_string(), }, @@ -202,11 +197,10 @@ async fn test_get() -> Result<()> { .await .expect("should have gotten a secret"); - let actual: StoredSecret = serde_json::from_str(secret.string_secret.unwrap().as_str()) - .expect("should have deserialized secret.string_secret into StoredSecret"); - let expected = stored_secret.value; + let actual = secret.string_secret.unwrap_or_default(); + let expected = stored_secret.get(secret_key).unwrap(); - assert_eq!(actual.value, expected); + assert_eq!(&actual, expected); Ok(()) } @@ -316,7 +310,7 @@ async fn configure_vault_jwt_auth(vault_client: &VaultClient, jwks_url: String) async fn store_secret_in_engine_at_path( vault_client: &VaultClient, - value: &impl Serialize, + value: impl Serialize, mount: &str, path: &str, ) -> Result<()> {