Skip to main content

alien_bindings/providers/service_account/
azure_managed_identity.rs

1use crate::error::{ErrorData, Result};
2use crate::traits::{
3    AzureServiceAccountInfo, Binding, ImpersonationRequest, ServiceAccount, ServiceAccountInfo,
4};
5use alien_azure_clients::AzureClientConfig;
6use alien_core::bindings::AzureServiceAccountBinding;
7use alien_core::{AzureClientConfig as CoreAzureClientConfig, AzureCredentials, ClientConfig};
8use alien_error::Context;
9use async_trait::async_trait;
10use std::collections::HashMap;
11
12/// Azure User-Assigned Managed Identity service account binding implementation
13///
14/// Note: Azure impersonation works differently than AWS/GCP. The managed identity
15/// must already be attached to the workload (Container App, VM, etc.) at provisioning time.
16/// This binding allows selecting which attached identity to use at runtime by providing
17/// its client_id to the Azure Identity SDK.
18#[derive(Debug)]
19pub struct AzureManagedIdentityServiceAccount {
20    config: AzureClientConfig,
21    binding: AzureServiceAccountBinding,
22}
23
24impl AzureManagedIdentityServiceAccount {
25    pub fn new(config: AzureClientConfig, binding: AzureServiceAccountBinding) -> Self {
26        Self { config, binding }
27    }
28
29    /// Get the client ID from the binding, resolving template expressions if needed
30    fn get_client_id(&self) -> Result<String> {
31        self.binding
32            .client_id
33            .clone()
34            .into_value("service-account", "client_id")
35            .context(ErrorData::BindingConfigInvalid {
36                binding_name: "service-account".to_string(),
37                reason: "Failed to resolve client_id from binding".to_string(),
38            })
39    }
40
41    /// Get the resource ID from the binding, resolving template expressions if needed
42    fn get_resource_id(&self) -> Result<String> {
43        self.binding
44            .resource_id
45            .clone()
46            .into_value("service-account", "resource_id")
47            .context(ErrorData::BindingConfigInvalid {
48                binding_name: "service-account".to_string(),
49                reason: "Failed to resolve resource_id from binding".to_string(),
50            })
51    }
52
53    /// Get the principal ID from the binding, resolving template expressions if needed
54    fn get_principal_id(&self) -> Result<String> {
55        self.binding
56            .principal_id
57            .clone()
58            .into_value("service-account", "principal_id")
59            .context(ErrorData::BindingConfigInvalid {
60                binding_name: "service-account".to_string(),
61                reason: "Failed to resolve principal_id from binding".to_string(),
62            })
63    }
64}
65
66impl Binding for AzureManagedIdentityServiceAccount {}
67
68#[async_trait]
69impl ServiceAccount for AzureManagedIdentityServiceAccount {
70    async fn get_info(&self) -> Result<ServiceAccountInfo> {
71        let client_id = self.get_client_id()?;
72        let resource_id = self.get_resource_id()?;
73        let principal_id = self.get_principal_id()?;
74
75        Ok(ServiceAccountInfo::Azure(AzureServiceAccountInfo {
76            client_id,
77            resource_id,
78            principal_id,
79        }))
80    }
81
82    async fn impersonate(&self, _request: ImpersonationRequest) -> Result<ClientConfig> {
83        let client_id = self.get_client_id()?;
84
85        let env_vars = std::env::vars().collect::<HashMap<_, _>>();
86        let tenant_id = env_vars
87            .get("AZURE_TENANT_ID")
88            .cloned()
89            .unwrap_or_else(|| self.config.tenant_id.clone());
90
91        let credentials = if let Some(federated_token_file) =
92            env_vars.get("AZURE_FEDERATED_TOKEN_FILE")
93        {
94            AzureCredentials::WorkloadIdentity {
95                client_id: client_id.clone(),
96                tenant_id: tenant_id.clone(),
97                federated_token_file: federated_token_file.clone(),
98                authority_host: env_vars
99                    .get("AZURE_AUTHORITY_HOST")
100                    .cloned()
101                    .unwrap_or_else(|| "https://login.microsoftonline.com/".to_string()),
102            }
103        } else if let (Some(identity_endpoint), Some(identity_header)) = (
104            env_vars.get("IDENTITY_ENDPOINT"),
105            env_vars.get("IDENTITY_HEADER"),
106        ) {
107            AzureCredentials::ManagedIdentity {
108                client_id: client_id.clone(),
109                identity_endpoint: identity_endpoint.clone(),
110                identity_header: identity_header.clone(),
111            }
112        } else {
113            return Err(alien_error::AlienError::new(ErrorData::Other {
114                    message: "Azure managed identity impersonation requires workload identity (AZURE_FEDERATED_TOKEN_FILE) or managed identity (IDENTITY_ENDPOINT and IDENTITY_HEADER) credentials".to_string(),
115                }));
116        };
117
118        let impersonated_config = CoreAzureClientConfig {
119            subscription_id: self.config.subscription_id.clone(),
120            tenant_id,
121            region: self.config.region.clone(),
122            credentials,
123            service_overrides: self.config.service_overrides.clone(),
124        };
125
126        Ok(ClientConfig::Azure(Box::new(impersonated_config)))
127    }
128
129    fn as_any(&self) -> &dyn std::any::Any {
130        self
131    }
132}