spin_variables/
azure_key_vault.rs

1use std::sync::Arc;
2
3use anyhow::Context as _;
4use azure_core::{auth::TokenCredential, Url};
5use azure_security_keyvault::SecretClient;
6use serde::Deserialize;
7use spin_expressions::{Key, Provider};
8use spin_factors::anyhow;
9use spin_world::async_trait;
10use tracing::{instrument, Level};
11
12/// Azure KeyVault runtime config literal options for authentication
13///
14/// Some of these fields are optional. Whether they are set determines whether
15/// environmental variables will be used to resolve the information instead.
16#[derive(Clone, Debug, Deserialize)]
17#[serde(deny_unknown_fields)]
18pub struct AzureKeyVaultVariablesConfig {
19    pub vault_url: String,
20    pub client_id: Option<String>,
21    pub client_secret: Option<String>,
22    pub tenant_id: Option<String>,
23    pub authority_host: Option<AzureAuthorityHost>,
24}
25
26#[derive(Debug, Copy, Clone, Deserialize, Default)]
27pub enum AzureAuthorityHost {
28    #[default]
29    AzurePublicCloud,
30    AzureChina,
31    AzureGermany,
32    AzureGovernment,
33}
34
35impl TryFrom<AzureKeyVaultVariablesConfig> for AzureKeyVaultAuthOptions {
36    type Error = anyhow::Error;
37
38    fn try_from(value: AzureKeyVaultVariablesConfig) -> Result<Self, Self::Error> {
39        match (value.client_id, value.tenant_id, value.client_secret) {
40            (Some(client_id), Some(tenant_id), Some(client_secret)) => Ok(
41                AzureKeyVaultAuthOptions::RuntimeConfigValues{
42                    client_id,
43                    client_secret,
44                    tenant_id,
45                    authority_host: value.authority_host.unwrap_or_default(),
46                }
47            ),
48            (None, None, None) => Ok(AzureKeyVaultAuthOptions::Environmental),
49            _ => 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).")
50        }
51    }
52}
53
54/// Azure Cosmos Key / Value enumeration for the possible authentication options
55#[derive(Clone, Debug)]
56pub enum AzureKeyVaultAuthOptions {
57    /// Runtime Config values indicates the service principal credentials have been supplied
58    RuntimeConfigValues {
59        client_id: String,
60        client_secret: String,
61        tenant_id: String,
62        authority_host: AzureAuthorityHost,
63    },
64    /// Environmental indicates that the environment variables of the process
65    /// should be used to create the TokenCredential for the Cosmos client. This
66    /// will use the Azure Rust SDK's DefaultCredentialChain to derive the
67    /// TokenCredential based on what environment variables have been set.
68    ///
69    /// Service Principal with client secret:
70    /// - `AZURE_TENANT_ID`: ID of the service principal's Azure tenant.
71    /// - `AZURE_CLIENT_ID`: the service principal's client ID.
72    /// - `AZURE_CLIENT_SECRET`: one of the service principal's secrets.
73    ///
74    /// Service Principal with certificate:
75    /// - `AZURE_TENANT_ID`: ID of the service principal's Azure tenant.
76    /// - `AZURE_CLIENT_ID`: the service principal's client ID.
77    /// - `AZURE_CLIENT_CERTIFICATE_PATH`: path to a PEM or PKCS12 certificate
78    ///   file including the private key.
79    /// - `AZURE_CLIENT_CERTIFICATE_PASSWORD`: (optional) password for the
80    ///   certificate file.
81    ///
82    /// Workload Identity (Kubernetes, injected by the Workload Identity
83    /// mutating webhook):
84    /// - `AZURE_TENANT_ID`: ID of the service principal's Azure tenant.
85    /// - `AZURE_CLIENT_ID`: the service principal's client ID.
86    /// - `AZURE_FEDERATED_TOKEN_FILE`: TokenFilePath is the path of a file
87    ///   containing a Kubernetes service account token.
88    ///
89    /// Managed Identity (User Assigned or System Assigned identities):
90    /// - `AZURE_CLIENT_ID`: (optional) if using a user assigned identity, this
91    ///   will be the client ID of the identity.
92    ///
93    /// Azure CLI:
94    /// - `AZURE_TENANT_ID`: (optional) use a specific tenant via the Azure CLI.
95    ///
96    /// Common across each:
97    /// - `AZURE_AUTHORITY_HOST`: (optional) the host for the identity provider.
98    ///   For example, for Azure public cloud the host defaults to
99    ///   `"https://login.microsoftonline.com"`.
100    ///
101    /// See also:
102    /// <https://github.com/Azure/azure-sdk-for-rust/blob/main/sdk/identity/README.md>
103    Environmental,
104}
105
106/// A provider that fetches variables from Azure Key Vault.
107#[derive(Debug)]
108pub struct AzureKeyVaultProvider {
109    secret_client: SecretClient,
110}
111
112impl AzureKeyVaultProvider {
113    pub fn create(
114        vault_url: impl Into<String>,
115        auth_options: AzureKeyVaultAuthOptions,
116    ) -> anyhow::Result<Self> {
117        let http_client = azure_core::new_http_client();
118        let token_credential = match auth_options {
119            AzureKeyVaultAuthOptions::RuntimeConfigValues {
120                client_id,
121                client_secret,
122                tenant_id,
123                authority_host,
124            } => {
125                let credential = azure_identity::ClientSecretCredential::new(
126                    http_client,
127                    authority_host.into(),
128                    tenant_id,
129                    client_id,
130                    client_secret,
131                );
132                Arc::new(credential) as Arc<dyn TokenCredential>
133            }
134            AzureKeyVaultAuthOptions::Environmental => azure_identity::create_default_credential()?,
135        };
136
137        Ok(Self {
138            secret_client: SecretClient::new(&vault_url.into(), token_credential)?,
139        })
140    }
141}
142
143#[async_trait]
144impl Provider for AzureKeyVaultProvider {
145    #[instrument(name = "spin_variables.get_from_azure_key_vault", level = Level::DEBUG, skip(self), err(level = Level::INFO), fields(otel.kind = "client"))]
146    async fn get(&self, key: &Key) -> anyhow::Result<Option<String>> {
147        let secret = self
148            .secret_client
149            .get(key.as_str())
150            .await
151            .context("Failed to read variable from Azure Key Vault")?;
152        Ok(Some(secret.value))
153    }
154}
155
156impl From<AzureAuthorityHost> for Url {
157    fn from(value: AzureAuthorityHost) -> Self {
158        let url = match value {
159            AzureAuthorityHost::AzureChina => "https://login.chinacloudapi.cn/",
160            AzureAuthorityHost::AzureGovernment => "https://login.microsoftonline.us/",
161            AzureAuthorityHost::AzureGermany => "https://login.microsoftonline.de/",
162            AzureAuthorityHost::AzurePublicCloud => "https://login.microsoftonline.com/",
163        };
164        Url::parse(url).unwrap()
165    }
166}