spin_variables/azure_key_vault.rs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166
use std::sync::Arc;
use anyhow::Context as _;
use azure_core::{auth::TokenCredential, Url};
use azure_security_keyvault::SecretClient;
use serde::Deserialize;
use spin_expressions::{Key, Provider};
use spin_factors::anyhow;
use spin_world::async_trait;
use tracing::{instrument, Level};
/// Azure KeyVault runtime config literal options for authentication
///
/// Some of these fields are optional. Whether they are set determines whether
/// environmental variables will be used to resolve the information instead.
#[derive(Clone, Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct AzureKeyVaultVariablesConfig {
pub vault_url: String,
pub client_id: Option<String>,
pub client_secret: Option<String>,
pub tenant_id: Option<String>,
pub authority_host: Option<AzureAuthorityHost>,
}
#[derive(Debug, Copy, Clone, Deserialize, Default)]
pub enum AzureAuthorityHost {
#[default]
AzurePublicCloud,
AzureChina,
AzureGermany,
AzureGovernment,
}
impl TryFrom<AzureKeyVaultVariablesConfig> for AzureKeyVaultAuthOptions {
type Error = anyhow::Error;
fn try_from(value: AzureKeyVaultVariablesConfig) -> Result<Self, Self::Error> {
match (value.client_id, value.tenant_id, value.client_secret) {
(Some(client_id), Some(tenant_id), Some(client_secret)) => Ok(
AzureKeyVaultAuthOptions::RuntimeConfigValues{
client_id,
client_secret,
tenant_id,
authority_host: value.authority_host.unwrap_or_default(),
}
),
(None, None, None) => Ok(AzureKeyVaultAuthOptions::Environmental),
_ => anyhow::bail!("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).")
}
}
}
/// 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 {
client_id: String,
client_secret: String,
tenant_id: String,
authority_host: AzureAuthorityHost,
},
/// 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,
}
/// A provider that fetches variables from Azure Key Vault.
#[derive(Debug)]
pub struct AzureKeyVaultProvider {
secret_client: SecretClient,
}
impl AzureKeyVaultProvider {
pub fn create(
vault_url: impl Into<String>,
auth_options: AzureKeyVaultAuthOptions,
) -> anyhow::Result<Self> {
let http_client = azure_core::new_http_client();
let token_credential = match auth_options {
AzureKeyVaultAuthOptions::RuntimeConfigValues {
client_id,
client_secret,
tenant_id,
authority_host,
} => {
let credential = azure_identity::ClientSecretCredential::new(
http_client,
authority_host.into(),
tenant_id,
client_id,
client_secret,
);
Arc::new(credential) as Arc<dyn TokenCredential>
}
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", level = Level::DEBUG, skip(self), err(level = Level::INFO), fields(otel.kind = "client"))]
async fn get(&self, key: &Key) -> anyhow::Result<Option<String>> {
let secret = self
.secret_client
.get(key.as_str())
.await
.context("Failed to read variable from Azure Key Vault")?;
Ok(Some(secret.value))
}
}
impl From<AzureAuthorityHost> for Url {
fn from(value: AzureAuthorityHost) -> Self {
let url = match value {
AzureAuthorityHost::AzureChina => "https://login.chinacloudapi.cn/",
AzureAuthorityHost::AzureGovernment => "https://login.microsoftonline.us/",
AzureAuthorityHost::AzureGermany => "https://login.microsoftonline.de/",
AzureAuthorityHost::AzurePublicCloud => "https://login.microsoftonline.com/",
};
Url::parse(url).unwrap()
}
}