spin_variables_azure/
lib.rs1use 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#[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#[derive(Clone, Debug)]
58pub enum AzureKeyVaultAuthOptions {
59 RuntimeConfigValues {
61 client_id: String,
62 client_secret: String,
63 tenant_id: String,
64 authority_host: AzureAuthorityHost,
65 },
66 Environmental,
106}
107
108#[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}