spin_variables/
azure_key_vault.rs1use 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#[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#[derive(Clone, Debug)]
56pub enum AzureKeyVaultAuthOptions {
57 RuntimeConfigValues {
59 client_id: String,
60 client_secret: String,
61 tenant_id: String,
62 authority_host: AzureAuthorityHost,
63 },
64 Environmental,
104}
105
106#[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}