Skip to main content

alien_bindings/providers/service_account/
azure_managed_identity.rs

1use crate::error::{binding_env_var, 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                env_var: binding_env_var("service-account"),
37                binding_name: "service-account".to_string(),
38                reason: "Failed to resolve client_id from binding".to_string(),
39            })
40    }
41
42    /// Get the resource ID from the binding, resolving template expressions if needed
43    fn get_resource_id(&self) -> Result<String> {
44        self.binding
45            .resource_id
46            .clone()
47            .into_value("service-account", "resource_id")
48            .context(ErrorData::BindingConfigInvalid {
49                env_var: binding_env_var("service-account"),
50                binding_name: "service-account".to_string(),
51                reason: "Failed to resolve resource_id from binding".to_string(),
52            })
53    }
54
55    /// Get the principal ID from the binding, resolving template expressions if needed
56    fn get_principal_id(&self) -> Result<String> {
57        self.binding
58            .principal_id
59            .clone()
60            .into_value("service-account", "principal_id")
61            .context(ErrorData::BindingConfigInvalid {
62                env_var: binding_env_var("service-account"),
63                binding_name: "service-account".to_string(),
64                reason: "Failed to resolve principal_id from binding".to_string(),
65            })
66    }
67}
68
69impl Binding for AzureManagedIdentityServiceAccount {}
70
71#[async_trait]
72impl ServiceAccount for AzureManagedIdentityServiceAccount {
73    async fn get_info(&self) -> Result<ServiceAccountInfo> {
74        let client_id = self.get_client_id()?;
75        let resource_id = self.get_resource_id()?;
76        let principal_id = self.get_principal_id()?;
77
78        Ok(ServiceAccountInfo::Azure(AzureServiceAccountInfo {
79            client_id,
80            resource_id,
81            principal_id,
82        }))
83    }
84
85    async fn impersonate(&self, _request: ImpersonationRequest) -> Result<ClientConfig> {
86        let client_id = self.get_client_id()?;
87
88        let env_vars = std::env::vars().collect::<HashMap<_, _>>();
89        let tenant_id = env_vars
90            .get("AZURE_TENANT_ID")
91            .cloned()
92            .unwrap_or_else(|| self.config.tenant_id.clone());
93
94        let credentials = if let Some(federated_token_file) =
95            env_vars.get("AZURE_FEDERATED_TOKEN_FILE")
96        {
97            AzureCredentials::WorkloadIdentity {
98                client_id: client_id.clone(),
99                tenant_id: tenant_id.clone(),
100                federated_token_file: federated_token_file.clone(),
101                authority_host: env_vars
102                    .get("AZURE_AUTHORITY_HOST")
103                    .cloned()
104                    .unwrap_or_else(|| "https://login.microsoftonline.com/".to_string()),
105            }
106        } else if let (Some(identity_endpoint), Some(identity_header)) = (
107            env_vars.get("IDENTITY_ENDPOINT"),
108            env_vars.get("IDENTITY_HEADER"),
109        ) {
110            AzureCredentials::ManagedIdentity {
111                client_id: client_id.clone(),
112                identity_endpoint: identity_endpoint.clone(),
113                identity_header: identity_header.clone(),
114            }
115        } else {
116            return Err(alien_error::AlienError::new(ErrorData::Other {
117                    message: "Azure managed identity impersonation requires workload identity (AZURE_FEDERATED_TOKEN_FILE) or managed identity (IDENTITY_ENDPOINT and IDENTITY_HEADER) credentials".to_string(),
118                }));
119        };
120
121        let impersonated_config = CoreAzureClientConfig {
122            subscription_id: self.config.subscription_id.clone(),
123            tenant_id,
124            region: self.config.region.clone(),
125            credentials,
126            service_overrides: self.config.service_overrides.clone(),
127        };
128
129        Ok(ClientConfig::Azure(Box::new(impersonated_config)))
130    }
131
132    fn as_any(&self) -> &dyn std::any::Any {
133        self
134    }
135}