diff --git a/Cargo.lock b/Cargo.lock index f486b09f8..30fba88b1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -568,33 +568,6 @@ dependencies = [ "tower-service", ] -[[package]] -name = "azure_core" -version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34ce3de4b65b1ee2667c81d1fc692949049502a4cf9c38118d811d6d79a7eaef" -dependencies = [ - "async-trait", - "base64 0.22.0", - "bytes", - "dyn-clone", - "futures", - "getrandom 0.2.12", - "http-types", - "once_cell", - "paste", - "pin-project", - "rand 0.8.5", - "reqwest 0.12.4", - "rustc_version", - "serde 1.0.197", - "serde_json", - "time", - "tracing", - "url", - "uuid", -] - [[package]] name = "azure_core" version = "0.20.0" @@ -629,7 +602,7 @@ version = "0.20.0" source = "git+https://github.com/azure/azure-sdk-for-rust.git?rev=8c4caa251c3903d5eae848b41bb1d02a4d65231c#8c4caa251c3903d5eae848b41bb1d02a4d65231c" dependencies = [ "async-trait", - "azure_core 0.20.0 (git+https://github.com/azure/azure-sdk-for-rust.git?rev=8c4caa251c3903d5eae848b41bb1d02a4d65231c)", + "azure_core", "bytes", "futures", "serde 1.0.197", @@ -641,27 +614,6 @@ dependencies = [ "uuid", ] -[[package]] -name = "azure_identity" -version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72c97790480791ec1ee9b76f5c6499b1d0aac0d4cd1e62010bfc19bb545544c5" -dependencies = [ - "async-lock 3.3.0", - "async-process 2.2.2", - "async-trait", - "azure_core 0.20.0 (registry+https://github.com/rust-lang/crates.io-index)", - "futures", - "oauth2", - "pin-project", - "serde 1.0.197", - "time", - "tracing", - "tz-rs", - "url", - "uuid", -] - [[package]] name = "azure_identity" version = "0.20.0" @@ -670,7 +622,7 @@ dependencies = [ "async-lock 3.3.0", "async-process 2.2.2", "async-trait", - "azure_core 0.20.0 (git+https://github.com/azure/azure-sdk-for-rust.git?rev=8c4caa251c3903d5eae848b41bb1d02a4d65231c)", + "azure_core", "futures", "oauth2", "pin-project", @@ -685,11 +637,10 @@ dependencies = [ [[package]] name = "azure_security_keyvault" version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "338cac645bda0555f59189873be0cccaf420c26791f009b2207b62474cebbab8" +source = "git+https://github.com/azure/azure-sdk-for-rust.git?rev=8c4caa251c3903d5eae848b41bb1d02a4d65231c#8c4caa251c3903d5eae848b41bb1d02a4d65231c" dependencies = [ "async-trait", - "azure_core 0.20.0 (registry+https://github.com/rust-lang/crates.io-index)", + "azure_core", "futures", "serde 1.0.197", "serde_json", @@ -7556,7 +7507,7 @@ version = "2.7.0-pre0" dependencies = [ "anyhow", "azure_data_cosmos", - "azure_identity 0.20.0 (git+https://github.com/azure/azure-sdk-for-rust.git?rev=8c4caa251c3903d5eae848b41bb1d02a4d65231c)", + "azure_identity", "futures", "serde 1.0.197", "spin-core", @@ -8039,8 +7990,8 @@ version = "2.7.0-pre0" dependencies = [ "anyhow", "async-trait", - "azure_core 0.20.0 (registry+https://github.com/rust-lang/crates.io-index)", - "azure_identity 0.20.0 (registry+https://github.com/rust-lang/crates.io-index)", + "azure_core", + "azure_identity", "azure_security_keyvault", "dotenvy", "once_cell", diff --git a/crates/trigger/src/lib.rs b/crates/trigger/src/lib.rs index 033999fdc..506221c58 100644 --- a/crates/trigger/src/lib.rs +++ b/crates/trigger/src/lib.rs @@ -204,7 +204,7 @@ impl TriggerExecutorBuilder { self.loader.add_dynamic_host_component( &mut builder, spin_variables::VariablesHostComponent::new( - runtime_config.variables_providers(), + runtime_config.variables_providers()?, ), )?; } @@ -225,7 +225,7 @@ impl TriggerExecutorBuilder { let app_name = app.borrowed().require_metadata(APP_NAME_KEY)?; let resolver = - spin_variables::make_resolver(app.borrowed(), runtime_config.variables_providers())?; + spin_variables::make_resolver(app.borrowed(), runtime_config.variables_providers()?)?; let prepared_resolver = std::sync::Arc::new(resolver.prepare().await?); resolver_cell .set(prepared_resolver.clone()) diff --git a/crates/trigger/src/runtime_config.rs b/crates/trigger/src/runtime_config.rs index 539939902..59d92ae75 100644 --- a/crates/trigger/src/runtime_config.rs +++ b/crates/trigger/src/runtime_config.rs @@ -60,15 +60,17 @@ impl RuntimeConfig { } /// Return a Vec of configured [`VariablesProvider`]s. - pub fn variables_providers(&self) -> Vec { - let default_provider = VariablesProviderOpts::default_provider_opts(self).build_provider(); + pub fn variables_providers(&self) -> Result> { + let default_provider = + VariablesProviderOpts::default_provider_opts(self).build_provider()?; let mut providers: Vec = vec![default_provider]; - providers.extend(self.opts_layers().flat_map(|opts| { - opts.variables_providers - .iter() - .map(|opts| opts.build_provider()) - })); - providers + for opts in self.opts_layers() { + for var_provider in &opts.variables_providers { + let provider = var_provider.build_provider()?; + providers.push(provider); + } + } + Ok(providers) } /// Return an iterator of named configured [`KeyValueStore`]s. @@ -473,7 +475,7 @@ mod tests { let mut config = RuntimeConfig::new(None); // One default provider - assert_eq!(config.variables_providers().len(), 1); + assert_eq!(config.variables_providers()?.len(), 1); merge_config_toml( &mut config, @@ -485,7 +487,7 @@ mod tests { mount = "root" }, ); - assert_eq!(config.variables_providers().len(), 2); + assert_eq!(config.variables_providers()?.len(), 2); Ok(()) } @@ -495,7 +497,7 @@ mod tests { let mut config = RuntimeConfig::new(None); // One default provider - assert_eq!(config.variables_providers().len(), 1); + assert_eq!(config.variables_providers()?.len(), 1); merge_config_toml( &mut config, @@ -507,7 +509,7 @@ mod tests { mount = "root" }, ); - assert_eq!(config.variables_providers().len(), 2); + assert_eq!(config.variables_providers()?.len(), 2); Ok(()) } diff --git a/crates/trigger/src/runtime_config/variables_provider.rs b/crates/trigger/src/runtime_config/variables_provider.rs index d08633fec..3cfc5c1de 100644 --- a/crates/trigger/src/runtime_config/variables_provider.rs +++ b/crates/trigger/src/runtime_config/variables_provider.rs @@ -1,6 +1,10 @@ use std::path::PathBuf; +use anyhow::{anyhow, Result}; use serde::Deserialize; +use spin_variables::provider::azure_key_vault::{ + AzureKeyVaultAuthOptions, AzureKeyVaultRuntimeConfigOptions, +}; use spin_variables::provider::{ azure_key_vault::{AzureAuthorityHost, AzureKeyVaultProvider}, env::EnvProvider, @@ -27,7 +31,7 @@ impl VariablesProviderOpts { )) } - pub fn build_provider(&self) -> VariablesProvider { + pub fn build_provider(&self) -> Result { match self { Self::Env(opts) => opts.build_provider(), Self::Vault(opts) => opts.build_provider(), @@ -60,11 +64,11 @@ impl EnvVariablesProviderOpts { } } - pub fn build_provider(&self) -> VariablesProvider { - Box::new(EnvProvider::new( + pub fn build_provider(&self) -> Result { + Ok(Box::new(EnvProvider::new( self.prefix.clone(), self.dotenv_path.clone(), - )) + ))) } } @@ -79,35 +83,52 @@ pub struct VaultVariablesProviderOpts { } impl VaultVariablesProviderOpts { - pub fn build_provider(&self) -> VariablesProvider { - Box::new(VaultProvider::new( + pub fn build_provider(&self) -> Result { + Ok(Box::new(VaultProvider::new( &self.url, &self.token, &self.mount, self.prefix.as_deref(), - )) + ))) } } #[derive(Debug, Default, Deserialize)] #[serde(deny_unknown_fields)] pub struct AzureKeyVaultVariablesProviderOpts { - pub client_id: String, - pub client_secret: String, - pub tenant_id: String, pub vault_url: String, - #[serde(default)] - pub authority_host: AzureAuthorityHost, + pub client_id: Option, + pub client_secret: Option, + pub tenant_id: Option, + pub authority_host: Option, } impl AzureKeyVaultVariablesProviderOpts { - pub fn build_provider(&self) -> VariablesProvider { - Box::new(AzureKeyVaultProvider::new( - &self.client_id, - &self.client_secret, - &self.tenant_id, + pub fn build_provider(&self) -> Result { + let auth_config_runtime_vars = [&self.client_id, &self.tenant_id, &self.client_secret]; + let any_some = auth_config_runtime_vars.iter().any(|&var| var.is_some()); + let any_none = auth_config_runtime_vars.iter().any(|&var| var.is_none()); + + if any_none && any_some { + // some of the service principal auth options were specified, but not enough to authenticate. + return Err(anyhow!("The current runtime config specifies some but not all of the Azure KeyVault 'client_id', 'client_secret', and 'tenant_id' values. Provide the missing values to authenticate to Azure KeyVault with the given service principal, or remove all these values to authenticate using ambient authentication (e.g. env vars, Azure CLI, Managed Identity, Workload Identity).")); + } + + let auth_options = if any_some { + // all the service principal auth options were specified in the runtime config + AzureKeyVaultAuthOptions::RuntimeConfigValues(AzureKeyVaultRuntimeConfigOptions::new( + self.client_id.clone().unwrap(), + self.client_secret.clone().unwrap(), + self.tenant_id.clone().unwrap(), + self.authority_host, + )) + } else { + AzureKeyVaultAuthOptions::Environmental + }; + + Ok(Box::new(AzureKeyVaultProvider::new( &self.vault_url, - self.authority_host, - )) + auth_options, + )?)) } } diff --git a/crates/variables/Cargo.toml b/crates/variables/Cargo.toml index e44e83171..f5de219e4 100644 --- a/crates/variables/Cargo.toml +++ b/crates/variables/Cargo.toml @@ -18,9 +18,9 @@ tokio = { version = "1", features = ["rt-multi-thread"] } vaultrs = "0.6.2" serde = "1.0.188" tracing = { workspace = true } -azure_security_keyvault = "0.20.0" -azure_core = "0.20.0" -azure_identity = "0.20.0" +azure_security_keyvault = { git = "https://github.com/azure/azure-sdk-for-rust.git", rev = "8c4caa251c3903d5eae848b41bb1d02a4d65231c" } +azure_core = { git = "https://github.com/azure/azure-sdk-for-rust.git", rev = "8c4caa251c3903d5eae848b41bb1d02a4d65231c" } +azure_identity = { git = "https://github.com/azure/azure-sdk-for-rust.git", rev = "8c4caa251c3903d5eae848b41bb1d02a4d65231c" } [dev-dependencies] toml = "0.5" diff --git a/crates/variables/src/provider/azure_key_vault.rs b/crates/variables/src/provider/azure_key_vault.rs index bc37a6c43..c9009a52f 100644 --- a/crates/variables/src/provider/azure_key_vault.rs +++ b/crates/variables/src/provider/azure_key_vault.rs @@ -2,54 +2,113 @@ use std::sync::Arc; use anyhow::{Context, Result}; use async_trait::async_trait; +use azure_core::auth::TokenCredential; use azure_core::Url; use azure_security_keyvault::SecretClient; use serde::Deserialize; use spin_expressions::{Key, Provider}; use tracing::{instrument, Level}; -#[derive(Debug)] -pub struct AzureKeyVaultProvider { +/// Azure KeyVault runtime config literal options for authentication +#[derive(Clone, Debug)] +pub struct AzureKeyVaultRuntimeConfigOptions { client_id: String, client_secret: String, tenant_id: String, - vault_url: String, authority_host: AzureAuthorityHost, } -impl AzureKeyVaultProvider { +impl AzureKeyVaultRuntimeConfigOptions { pub fn new( - client_id: impl Into, - client_secret: impl Into, - tenant_id: impl Into, - vault_url: impl Into, - authority_host: impl Into, + client_id: String, + client_secret: String, + tenant_id: String, + authority_host: Option, ) -> Self { Self { - client_id: client_id.into(), - client_secret: client_secret.into(), - tenant_id: tenant_id.into(), - vault_url: vault_url.into(), - authority_host: authority_host.into(), + client_id, + client_secret, + tenant_id, + authority_host: authority_host.unwrap_or_default(), } } } +/// Azure Cosmos Key / Value enumeration for the possible authentication options +#[derive(Clone, Debug)] +pub enum AzureKeyVaultAuthOptions { + /// Runtime Config values indicates the service principal credentials have been supplied + RuntimeConfigValues(AzureKeyVaultRuntimeConfigOptions), + /// Environmental indicates that the environment variables of the process should be used to + /// create the TokenCredential for the Cosmos client. This will use the Azure Rust SDK's + /// DefaultCredentialChain to derive the TokenCredential based on what environment variables + /// have been set. + /// + /// Service Principal with client secret: + /// - `AZURE_TENANT_ID`: ID of the service principal's Azure tenant. + /// - `AZURE_CLIENT_ID`: the service principal's client ID. + /// - `AZURE_CLIENT_SECRET`: one of the service principal's secrets. + /// + /// Service Principal with certificate: + /// - `AZURE_TENANT_ID`: ID of the service principal's Azure tenant. + /// - `AZURE_CLIENT_ID`: the service principal's client ID. + /// - `AZURE_CLIENT_CERTIFICATE_PATH`: path to a PEM or PKCS12 certificate file including the private key. + /// - `AZURE_CLIENT_CERTIFICATE_PASSWORD`: (optional) password for the certificate file. + /// + /// Workload Identity (Kubernetes, injected by the Workload Identity mutating webhook): + /// - `AZURE_TENANT_ID`: ID of the service principal's Azure tenant. + /// - `AZURE_CLIENT_ID`: the service principal's client ID. + /// - `AZURE_FEDERATED_TOKEN_FILE`: TokenFilePath is the path of a file containing a Kubernetes service account token. + /// + /// Managed Identity (User Assigned or System Assigned identities): + /// - `AZURE_CLIENT_ID`: (optional) if using a user assigned identity, this will be the client ID of the identity. + /// + /// Azure CLI: + /// - `AZURE_TENANT_ID`: (optional) use a specific tenant via the Azure CLI. + /// + /// Common across each: + /// - `AZURE_AUTHORITY_HOST`: (optional) the host for the identity provider. For example, for Azure public cloud the host defaults to "https://login.microsoftonline.com". + /// See also: https://github.com/Azure/azure-sdk-for-rust/blob/main/sdk/identity/README.md + Environmental, +} + +#[derive(Debug)] +pub struct AzureKeyVaultProvider { + secret_client: SecretClient, +} + +impl AzureKeyVaultProvider { + pub fn new( + vault_url: impl Into, + auth_options: AzureKeyVaultAuthOptions, + ) -> Result { + let http_client = azure_core::new_http_client(); + let token_credential = match auth_options.clone() { + AzureKeyVaultAuthOptions::RuntimeConfigValues(config) => { + let credential = azure_identity::ClientSecretCredential::new( + http_client, + config.authority_host.into(), + config.tenant_id.to_string(), + config.client_id.to_string(), + config.client_secret.to_string(), + ); + Arc::new(credential) as Arc + } + AzureKeyVaultAuthOptions::Environmental => azure_identity::create_default_credential()?, + }; + + Ok(Self { + secret_client: SecretClient::new(&vault_url.into(), token_credential)?, + }) + } +} + #[async_trait] impl Provider for AzureKeyVaultProvider { #[instrument(name = "spin_variables.get_from_azure_key_vault", skip(self), err(level = Level::INFO), fields(otel.kind = "client"))] async fn get(&self, key: &Key) -> Result> { - let http_client = azure_core::new_http_client(); - let credential = azure_identity::ClientSecretCredential::new( - http_client, - self.authority_host.into(), - self.tenant_id.to_string(), - self.client_id.to_string(), - self.client_secret.to_string(), - ); - - let secret_client = SecretClient::new(&self.vault_url, Arc::new(credential))?; - let secret = secret_client + let secret = self + .secret_client .get(key.as_str()) .await .context("Failed to read variable from Azure Key Vault")?; diff --git a/examples/spin-timer/Cargo.lock b/examples/spin-timer/Cargo.lock index 8c9adcfc8..ee337d766 100644 --- a/examples/spin-timer/Cargo.lock +++ b/examples/spin-timer/Cargo.lock @@ -470,33 +470,6 @@ dependencies = [ "tower-service", ] -[[package]] -name = "azure_core" -version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34ce3de4b65b1ee2667c81d1fc692949049502a4cf9c38118d811d6d79a7eaef" -dependencies = [ - "async-trait", - "base64 0.22.0", - "bytes", - "dyn-clone", - "futures", - "getrandom 0.2.12", - "http-types", - "once_cell", - "paste", - "pin-project", - "rand 0.8.5", - "reqwest 0.12.4", - "rustc_version", - "serde 1.0.203", - "serde_json", - "time", - "tracing", - "url", - "uuid", -] - [[package]] name = "azure_core" version = "0.20.0" @@ -531,7 +504,7 @@ version = "0.20.0" source = "git+https://github.com/azure/azure-sdk-for-rust.git?rev=8c4caa251c3903d5eae848b41bb1d02a4d65231c#8c4caa251c3903d5eae848b41bb1d02a4d65231c" dependencies = [ "async-trait", - "azure_core 0.20.0 (git+https://github.com/azure/azure-sdk-for-rust.git?rev=8c4caa251c3903d5eae848b41bb1d02a4d65231c)", + "azure_core", "bytes", "futures", "serde 1.0.203", @@ -543,27 +516,6 @@ dependencies = [ "uuid", ] -[[package]] -name = "azure_identity" -version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72c97790480791ec1ee9b76f5c6499b1d0aac0d4cd1e62010bfc19bb545544c5" -dependencies = [ - "async-lock 3.3.0", - "async-process 2.2.2", - "async-trait", - "azure_core 0.20.0 (registry+https://github.com/rust-lang/crates.io-index)", - "futures", - "oauth2", - "pin-project", - "serde 1.0.203", - "time", - "tracing", - "tz-rs", - "url", - "uuid", -] - [[package]] name = "azure_identity" version = "0.20.0" @@ -572,7 +524,7 @@ dependencies = [ "async-lock 3.3.0", "async-process 2.2.2", "async-trait", - "azure_core 0.20.0 (git+https://github.com/azure/azure-sdk-for-rust.git?rev=8c4caa251c3903d5eae848b41bb1d02a4d65231c)", + "azure_core", "futures", "oauth2", "pin-project", @@ -587,11 +539,10 @@ dependencies = [ [[package]] name = "azure_security_keyvault" version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "338cac645bda0555f59189873be0cccaf420c26791f009b2207b62474cebbab8" +source = "git+https://github.com/azure/azure-sdk-for-rust.git?rev=8c4caa251c3903d5eae848b41bb1d02a4d65231c#8c4caa251c3903d5eae848b41bb1d02a4d65231c" dependencies = [ "async-trait", - "azure_core 0.20.0 (registry+https://github.com/rust-lang/crates.io-index)", + "azure_core", "futures", "serde 1.0.203", "serde_json", @@ -5782,7 +5733,7 @@ version = "2.7.0-pre0" dependencies = [ "anyhow", "azure_data_cosmos", - "azure_identity 0.20.0 (git+https://github.com/azure/azure-sdk-for-rust.git?rev=8c4caa251c3903d5eae848b41bb1d02a4d65231c)", + "azure_identity", "futures", "serde 1.0.203", "spin-core", @@ -6062,8 +6013,8 @@ version = "2.7.0-pre0" dependencies = [ "anyhow", "async-trait", - "azure_core 0.20.0 (registry+https://github.com/rust-lang/crates.io-index)", - "azure_identity 0.20.0 (registry+https://github.com/rust-lang/crates.io-index)", + "azure_core", + "azure_identity", "azure_security_keyvault", "dotenvy", "once_cell",