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