Skip to main content

spin_variables_azure/
lib.rs

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