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, AzureClientConfigExt};
6use alien_core::bindings::AzureServiceAccountBinding;
7use alien_core::{AzureClientConfig as CoreAzureClientConfig, 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        // For Azure, "impersonation" means using a different managed identity that's already
86        // attached to the workload. We do this by creating a new config with the target
87        // identity's client_id, reusing the existing workload identity setup.
88        //
89        // When multiple UAMIs are attached to a Container App, Azure exposes the same
90        // AZURE_TENANT_ID and AZURE_FEDERATED_TOKEN_FILE for all of them. The
91        // AZURE_CLIENT_ID environment variable determines which identity to use when
92        // requesting tokens. By overriding AZURE_CLIENT_ID and calling from_env(),
93        // we effectively "switch" to the target identity.
94
95        // Get current environment variables and override AZURE_CLIENT_ID
96        let mut env_vars: HashMap<String, String> = std::env::vars().collect();
97
98        // Override the client_id to select the target managed identity
99        env_vars.insert("AZURE_CLIENT_ID".to_string(), client_id.clone());
100
101        // Use from_env to create the config - it will handle all the credential setup
102        let impersonated_config =
103            CoreAzureClientConfig::from_env(&env_vars)
104                .await
105                .context(ErrorData::Other {
106                    message: format!(
107                        "Failed to create Azure config for impersonation with client_id: {}",
108                        client_id
109                    ),
110                })?;
111
112        Ok(ClientConfig::Azure(Box::new(impersonated_config)))
113    }
114
115    fn as_any(&self) -> &dyn std::any::Any {
116        self
117    }
118}