vector/sinks/azure_common/
config.rs1use std::path::PathBuf;
2use std::sync::Arc;
3
4#[cfg(test)]
5use base64::prelude::*;
6
7use azure_core::http::ClientMethodOptions;
8
9use azure_core::credentials::{TokenCredential, TokenRequestOptions};
10use azure_core::{Error, error::ErrorKind};
11
12use azure_identity::{
13 AzureCliCredential, ClientAssertion, ClientAssertionCredential, ClientCertificateCredential,
14 ClientCertificateCredentialOptions, ClientSecretCredential, ManagedIdentityCredential,
15 ManagedIdentityCredentialOptions, UserAssignedId, WorkloadIdentityCredential,
16 WorkloadIdentityCredentialOptions,
17};
18
19use vector_lib::{configurable::configurable_component, sensitive_string::SensitiveString};
20
21#[configurable_component]
23#[derive(Clone, Debug, Default)]
24#[serde(deny_unknown_fields)]
25pub struct AzureBlobTlsConfig {
26 #[serde(alias = "ca_path")]
30 #[configurable(metadata(docs::examples = "/path/to/certificate_authority.crt"))]
31 #[configurable(metadata(docs::human_name = "CA File Path"))]
32 pub ca_file: Option<PathBuf>,
33}
34
35#[configurable_component]
37#[derive(Clone, Debug, Eq, PartialEq)]
38#[serde(deny_unknown_fields, untagged)]
39pub enum AzureAuthentication {
40 #[configurable(metadata(docs::enum_tag_description = "The kind of Azure credential to use."))]
41 Specific(SpecificAzureCredential),
42
43 #[cfg(test)]
45 #[serde(skip)]
46 MockCredential,
47}
48
49impl Default for AzureAuthentication {
50 fn default() -> Self {
54 Self::Specific(SpecificAzureCredential::ManagedIdentity {
55 user_assigned_managed_identity_id: None,
56 user_assigned_managed_identity_id_type: None,
57 })
58 }
59}
60
61#[configurable_component]
62#[derive(Clone, Debug, Eq, PartialEq)]
63#[serde(deny_unknown_fields, rename_all = "snake_case")]
64#[derive(Default)]
65pub enum UserAssignedManagedIdentityIdType {
67 #[default]
68 ClientId,
70 ObjectId,
72 ResourceId,
74}
75
76#[configurable_component]
78#[derive(Clone, Debug, Eq, PartialEq)]
79#[serde(
80 tag = "azure_credential_kind",
81 rename_all = "snake_case",
82 deny_unknown_fields
83)]
84pub enum SpecificAzureCredential {
85 #[cfg(not(target_arch = "wasm32"))]
87 AzureCli {},
88
89 ClientCertificateCredential {
91 #[configurable(metadata(docs::examples = "00000000-0000-0000-0000-000000000000"))]
95 #[configurable(metadata(docs::examples = "${AZURE_TENANT_ID:?err}"))]
96 azure_tenant_id: String,
97
98 #[configurable(metadata(docs::examples = "00000000-0000-0000-0000-000000000000"))]
102 #[configurable(metadata(docs::examples = "${AZURE_CLIENT_ID:?err}"))]
103 azure_client_id: String,
104
105 #[configurable(metadata(docs::examples = "path/to/certificate.pfx"))]
107 #[configurable(metadata(docs::examples = "${AZURE_CLIENT_CERTIFICATE_PATH:?err}"))]
108 certificate_path: PathBuf,
109
110 #[configurable(metadata(docs::examples = "${AZURE_CLIENT_CERTIFICATE_PASSWORD}"))]
112 certificate_password: Option<SensitiveString>,
113 },
114
115 ClientSecretCredential {
117 #[configurable(metadata(docs::examples = "00000000-0000-0000-0000-000000000000"))]
121 #[configurable(metadata(docs::examples = "${AZURE_TENANT_ID:?err}"))]
122 azure_tenant_id: String,
123
124 #[configurable(metadata(docs::examples = "00000000-0000-0000-0000-000000000000"))]
128 #[configurable(metadata(docs::examples = "${AZURE_CLIENT_ID:?err}"))]
129 azure_client_id: String,
130
131 #[configurable(metadata(docs::examples = "00-00~000000-0000000~0000000000000000000"))]
135 #[configurable(metadata(docs::examples = "${AZURE_CLIENT_SECRET:?err}"))]
136 azure_client_secret: SensitiveString,
137 },
138
139 ManagedIdentity {
141 #[configurable(metadata(docs::examples = "00000000-0000-0000-0000-000000000000"))]
143 #[serde(default, skip_serializing_if = "Option::is_none")]
144 user_assigned_managed_identity_id: Option<String>,
145
146 user_assigned_managed_identity_id_type: Option<UserAssignedManagedIdentityIdType>,
149 },
150
151 ManagedIdentityClientAssertion {
153 #[configurable(metadata(docs::examples = "00000000-0000-0000-0000-000000000000"))]
155 #[configurable(metadata(
156 docs::examples = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-vector/providers/Microsoft.ManagedIdentity/userAssignedIdentities/id-vector-uami"
157 ))]
158 #[serde(default, skip_serializing_if = "Option::is_none")]
159 user_assigned_managed_identity_id: Option<String>,
160
161 user_assigned_managed_identity_id_type: Option<UserAssignedManagedIdentityIdType>,
163
164 #[configurable(metadata(docs::examples = "00000000-0000-0000-0000-000000000000"))]
166 client_assertion_tenant_id: String,
167
168 #[configurable(metadata(docs::examples = "00000000-0000-0000-0000-000000000000"))]
170 client_assertion_client_id: String,
171 },
172
173 WorkloadIdentity {
175 #[configurable(metadata(docs::examples = "00000000-0000-0000-0000-000000000000"))]
179 #[configurable(metadata(docs::examples = "${AZURE_TENANT_ID}"))]
180 tenant_id: Option<String>,
181
182 #[configurable(metadata(docs::examples = "00000000-0000-0000-0000-000000000000"))]
186 #[configurable(metadata(docs::examples = "${AZURE_CLIENT_ID}"))]
187 client_id: Option<String>,
188
189 #[configurable(metadata(
191 docs::examples = "/var/run/secrets/azure/tokens/azure-identity-token"
192 ))]
193 #[configurable(metadata(docs::examples = "${AZURE_FEDERATED_TOKEN_FILE}"))]
194 token_file_path: Option<PathBuf>,
195 },
196}
197
198#[derive(Debug)]
199struct ManagedIdentityClientAssertion {
200 credential: Arc<dyn TokenCredential>,
201 scope: String,
202}
203
204#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
205#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
206impl ClientAssertion for ManagedIdentityClientAssertion {
207 async fn secret(&self, options: Option<ClientMethodOptions<'_>>) -> azure_core::Result<String> {
208 Ok(self
209 .credential
210 .get_token(
211 &[&self.scope],
212 Some(TokenRequestOptions {
213 method_options: options.unwrap_or_default(),
214 }),
215 )
216 .await?
217 .token
218 .secret()
219 .to_string())
220 }
221}
222
223impl AzureAuthentication {
224 pub async fn credential(&self) -> azure_core::Result<Arc<dyn TokenCredential>> {
226 match self {
227 Self::Specific(specific) => specific.credential().await,
228
229 #[cfg(test)]
230 Self::MockCredential => Ok(Arc::new(MockTokenCredential) as Arc<dyn TokenCredential>),
231 }
232 }
233}
234
235impl SpecificAzureCredential {
236 pub async fn credential(&self) -> azure_core::Result<Arc<dyn TokenCredential>> {
238 let credential: Arc<dyn TokenCredential> = match self {
239 #[cfg(not(target_arch = "wasm32"))]
240 Self::AzureCli {} => AzureCliCredential::new(None)?,
241
242 Self::ClientCertificateCredential {
244 azure_tenant_id,
245 azure_client_id,
246 certificate_path,
247 certificate_password,
248 } => {
249 let certificate_bytes: Vec<u8> = std::fs::read(certificate_path).map_err(|e| {
250 Error::with_message(
251 ErrorKind::Credential,
252 format!(
253 "Failed to read certificate file {}: {e}",
254 certificate_path.display()
255 ),
256 )
257 })?;
258
259 let mut options: ClientCertificateCredentialOptions =
260 ClientCertificateCredentialOptions::default();
261 if let Some(password) = certificate_password {
262 options.password = Some(password.inner().to_string().into());
263 }
264
265 ClientCertificateCredential::new(
266 azure_tenant_id.clone(),
267 azure_client_id.clone(),
268 certificate_bytes.into(),
269 Some(options),
270 )?
271 }
272
273 Self::ClientSecretCredential {
274 azure_tenant_id,
275 azure_client_id,
276 azure_client_secret,
277 } => {
278 if azure_tenant_id.is_empty() {
279 return Err(Error::with_message(ErrorKind::Credential,
280 "`auth.azure_tenant_id` is blank; either use `auth.azure_credential_kind`, or provide tenant ID, client ID, and secret.".to_string()
281 ));
282 }
283 if azure_client_id.is_empty() {
284 return Err(Error::with_message(ErrorKind::Credential,
285 "`auth.azure_client_id` is blank; either use `auth.azure_credential_kind`, or provide tenant ID, client ID, and secret.".to_string()
286 ));
287 }
288 if azure_client_secret.inner().is_empty() {
289 return Err(Error::with_message(ErrorKind::Credential,
290 "`auth.azure_client_secret` is blank; either use `auth.azure_credential_kind`, or provide tenant ID, client ID, and secret.".to_string()
291 ));
292 }
293
294 let secret: String = azure_client_secret.inner().into();
295 ClientSecretCredential::new(
296 &azure_tenant_id.clone(),
297 azure_client_id.clone(),
298 secret.into(),
299 None,
300 )?
301 }
302
303 Self::ManagedIdentity {
304 user_assigned_managed_identity_id,
305 user_assigned_managed_identity_id_type,
306 } => {
307 let mut options = ManagedIdentityCredentialOptions::default();
308 if let Some(id) = user_assigned_managed_identity_id {
309 options.user_assigned_id = match user_assigned_managed_identity_id_type
310 .as_ref()
311 .unwrap_or(&Default::default())
312 {
313 UserAssignedManagedIdentityIdType::ClientId => {
314 Some(UserAssignedId::ClientId(id.clone()))
315 }
316 UserAssignedManagedIdentityIdType::ObjectId => {
317 Some(UserAssignedId::ObjectId(id.clone()))
318 }
319 UserAssignedManagedIdentityIdType::ResourceId => {
320 Some(UserAssignedId::ResourceId(id.clone()))
321 }
322 };
323 }
324 ManagedIdentityCredential::new(Some(options))?
325 }
326
327 Self::ManagedIdentityClientAssertion {
328 user_assigned_managed_identity_id,
329 user_assigned_managed_identity_id_type,
330 client_assertion_tenant_id,
331 client_assertion_client_id,
332 } => {
333 let mut options = ManagedIdentityCredentialOptions::default();
334 if let Some(id) = user_assigned_managed_identity_id {
335 options.user_assigned_id = match user_assigned_managed_identity_id_type
336 .as_ref()
337 .unwrap_or(&Default::default())
338 {
339 UserAssignedManagedIdentityIdType::ClientId => {
340 Some(UserAssignedId::ClientId(id.clone()))
341 }
342 UserAssignedManagedIdentityIdType::ObjectId => {
343 Some(UserAssignedId::ObjectId(id.clone()))
344 }
345 UserAssignedManagedIdentityIdType::ResourceId => {
346 Some(UserAssignedId::ResourceId(id.clone()))
347 }
348 };
349 }
350 let msi: Arc<dyn TokenCredential> = ManagedIdentityCredential::new(Some(options))?;
351 let assertion = ManagedIdentityClientAssertion {
352 credential: msi,
353 scope: "api://AzureADTokenExchange/.default".to_string(),
355 };
356
357 ClientAssertionCredential::new(
358 client_assertion_tenant_id.clone(),
359 client_assertion_client_id.clone(),
360 assertion,
361 None,
362 )?
363 }
364
365 Self::WorkloadIdentity {
366 tenant_id,
367 client_id,
368 token_file_path,
369 } => {
370 let options = WorkloadIdentityCredentialOptions {
371 tenant_id: tenant_id.clone(),
372 client_id: client_id.clone(),
373 token_file_path: token_file_path.clone(),
374 ..Default::default()
375 };
376
377 WorkloadIdentityCredential::new(Some(options))?
378 }
379 };
380 Ok(credential)
381 }
382}
383
384#[cfg(test)]
385#[derive(Debug)]
386struct MockTokenCredential;
387
388#[cfg(test)]
389#[async_trait::async_trait]
390impl TokenCredential for MockTokenCredential {
391 async fn get_token(
392 &self,
393 scopes: &[&str],
394 _options: Option<azure_core::credentials::TokenRequestOptions<'_>>,
395 ) -> azure_core::Result<azure_core::credentials::AccessToken> {
396 let Some(scope) = scopes.first() else {
397 return Err(Error::with_message(
398 ErrorKind::Credential,
399 "no scopes were provided",
400 ));
401 };
402
403 let jwt = serde_json::json!({
406 "aud": scope.strip_suffix("/.default").unwrap_or(*scope),
407 "exp": 2147483647,
408 "iat": 0,
409 "iss": "https://sts.windows.net/",
410 "nbf": 0,
411 });
412
413 let jwt_base64 = format!(
416 "e30.{}.",
417 BASE64_STANDARD
418 .encode(serde_json::to_string(&jwt).unwrap())
419 .trim_end_matches("=")
420 )
421 .to_string();
422
423 warn!(
424 "Using mock token credential, JWT: {}, base64: {}",
425 serde_json::to_string(&jwt).unwrap(),
426 jwt_base64
427 );
428
429 Ok(azure_core::credentials::AccessToken::new(
430 jwt_base64,
431 azure_core::time::OffsetDateTime::now_utc() + std::time::Duration::from_secs(3600),
432 ))
433 }
434}
435
436#[cfg(test)]
437#[tokio::test]
438async fn azure_mock_token_credential_test() {
439 let credential = MockTokenCredential;
440 let access_token = credential
441 .get_token(&["https://example.com/.default"], None)
442 .await
443 .expect("valid credential should return a token");
444 assert_eq!(
445 access_token.token.secret(),
446 "e30.eyJhdWQiOiJodHRwczovL2V4YW1wbGUuY29tIiwiZXhwIjoyMTQ3NDgzNjQ3LCJpYXQiOjAsImlzcyI6Imh0dHBzOi8vc3RzLndpbmRvd3MubmV0LyIsIm5iZiI6MH0."
447 );
448}