Skip to main content

alien_permissions/generators/
azure_runtime.rs

1use crate::{
2    error::{ErrorData, Result},
3    variables::VariableInterpolator,
4    BindingTarget, PermissionContext,
5};
6use alien_core::PermissionSet;
7use serde::{Deserialize, Serialize};
8
9/// Azure role definition
10#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
11#[serde(rename_all = "PascalCase")]
12pub struct AzureRoleDefinition {
13    /// Human-readable role name
14    pub name: String,
15    /// Role ID (will be generated by Azure)
16    #[serde(skip_serializing_if = "Option::is_none")]
17    pub id: Option<String>,
18    /// Whether this is a custom role
19    pub is_custom: bool,
20    /// Description of what the role allows
21    pub description: String,
22    /// List of allowed actions
23    pub actions: Vec<String>,
24    /// List of denied actions
25    pub not_actions: Vec<String>,
26    /// List of allowed data actions
27    pub data_actions: Vec<String>,
28    /// List of denied data actions
29    pub not_data_actions: Vec<String>,
30    /// Scopes where this role can be assigned
31    pub assignable_scopes: Vec<String>,
32}
33
34/// Azure role assignment properties
35#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
36#[serde(rename_all = "camelCase")]
37pub struct AzureRoleAssignmentProperties {
38    /// Role definition ID
39    pub role_definition_id: String,
40    /// Principal ID (user, group, or service principal)
41    pub principal_id: String,
42    /// Scope where the role is assigned
43    pub scope: String,
44}
45
46/// Azure role assignment
47#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
48#[serde(rename_all = "camelCase")]
49pub struct AzureRoleAssignment {
50    /// Role assignment properties
51    pub properties: AzureRoleAssignmentProperties,
52}
53
54/// Azure runtime permissions generator for role definitions and role assignments
55pub struct AzureRuntimePermissionsGenerator;
56
57impl AzureRuntimePermissionsGenerator {
58    /// Create a new Azure runtime permissions generator
59    pub fn new() -> Self {
60        Self
61    }
62
63    /// Generate an Azure role definition from a permission set
64    ///
65    /// Takes a PermissionSet and produces Azure role definitions
66    /// that can be created at runtime.
67    pub fn generate_role_definition(
68        &self,
69        permission_set: &PermissionSet,
70        binding_target: BindingTarget,
71        context: &PermissionContext,
72    ) -> Result<AzureRoleDefinition> {
73        let azure_platform_permissions =
74            permission_set.platforms.azure.as_ref().ok_or_else(|| {
75                alien_error::AlienError::new(ErrorData::PlatformNotSupported {
76                    platform: "azure".to_string(),
77                    permission_set_id: permission_set.id.clone(),
78                })
79            })?;
80
81        let base_role_name = self.generate_role_name(&permission_set.id);
82        // Include the stack prefix in the role name to avoid 409
83        // RoleDefinitionWithSameNameExists conflicts when multiple deployments
84        // coexist in the same subscription.
85        let role_name = if let Some(ref prefix) = context.stack_prefix {
86            format!("{} ({})", base_role_name, prefix)
87        } else {
88            base_role_name
89        };
90
91        // Aggregate actions and data actions from all platform permissions
92        let mut all_actions = Vec::new();
93        let mut all_data_actions = Vec::new();
94        let mut assignable_scopes = Vec::new();
95
96        for platform_permission in azure_platform_permissions {
97            // Extract actions and data actions
98            if let Some(actions) = &platform_permission.grant.actions {
99                all_actions.extend(actions.clone());
100            }
101            if let Some(data_actions) = &platform_permission.grant.data_actions {
102                all_data_actions.extend(data_actions.clone());
103            }
104
105            // Generate assignable scopes based on binding target
106            let binding_spec = match binding_target {
107                BindingTarget::Stack => {
108                    platform_permission.binding.stack.as_ref().ok_or_else(|| {
109                        alien_error::AlienError::new(ErrorData::BindingTargetNotSupported {
110                            platform: "azure".to_string(),
111                            binding_target: "stack".to_string(),
112                            permission_set_id: permission_set.id.clone(),
113                        })
114                    })?
115                }
116                BindingTarget::Resource => platform_permission
117                    .binding
118                    .resource
119                    .as_ref()
120                    .ok_or_else(|| {
121                        alien_error::AlienError::new(ErrorData::BindingTargetNotSupported {
122                            platform: "azure".to_string(),
123                            binding_target: "resource".to_string(),
124                            permission_set_id: permission_set.id.clone(),
125                        })
126                    })?,
127            };
128
129            // Interpolate variables in the scope
130            let interpolated_scope =
131                VariableInterpolator::interpolate_variables(&binding_spec.scope, context)?;
132            assignable_scopes.push(interpolated_scope);
133        }
134
135        // Remove duplicates and sort
136        all_actions.sort();
137        all_actions.dedup();
138        all_data_actions.sort();
139        all_data_actions.dedup();
140        assignable_scopes.sort();
141        assignable_scopes.dedup();
142
143        Ok(AzureRoleDefinition {
144            name: role_name,
145            id: None, // Will be generated by Azure
146            is_custom: true,
147            description: permission_set.description.clone(),
148            actions: all_actions,
149            not_actions: vec![],
150            data_actions: all_data_actions,
151            not_data_actions: vec![],
152            assignable_scopes,
153        })
154    }
155
156    /// Generate an Azure role assignment
157    ///
158    /// Takes a PermissionSet and binding target, produces Azure role assignments
159    /// that can be created at runtime.
160    pub fn generate_role_assignment(
161        &self,
162        permission_set: &PermissionSet,
163        binding_target: BindingTarget,
164        context: &PermissionContext,
165    ) -> Result<AzureRoleAssignment> {
166        let azure_platform_permissions =
167            permission_set.platforms.azure.as_ref().ok_or_else(|| {
168                alien_error::AlienError::new(ErrorData::PlatformNotSupported {
169                    platform: "azure".to_string(),
170                    permission_set_id: permission_set.id.clone(),
171                })
172            })?;
173
174        // For this example, we'll use placeholder values
175        let role_definition_id = format!(
176            "/subscriptions/{}/providers/Microsoft.Authorization/roleDefinitions/${{roleDefinitionGuid}}",
177            context.subscription_id.as_deref().unwrap_or("SUBSCRIPTION_ID")
178        );
179
180        let principal_id = context
181            .principal_id
182            .as_deref()
183            .unwrap_or("PRINCIPAL_ID")
184            .to_string();
185
186        // Use the first platform permission's binding for simplicity
187        // In practice, you might want to handle multiple bindings differently
188        let first_platform_permission = &azure_platform_permissions[0];
189        let binding_spec = match binding_target {
190            BindingTarget::Stack => first_platform_permission
191                .binding
192                .stack
193                .as_ref()
194                .ok_or_else(|| {
195                    alien_error::AlienError::new(ErrorData::BindingTargetNotSupported {
196                        platform: "azure".to_string(),
197                        binding_target: "stack".to_string(),
198                        permission_set_id: permission_set.id.clone(),
199                    })
200                })?,
201            BindingTarget::Resource => first_platform_permission
202                .binding
203                .resource
204                .as_ref()
205                .ok_or_else(|| {
206                    alien_error::AlienError::new(ErrorData::BindingTargetNotSupported {
207                        platform: "azure".to_string(),
208                        binding_target: "resource".to_string(),
209                        permission_set_id: permission_set.id.clone(),
210                    })
211                })?,
212        };
213
214        // Interpolate variables in the scope
215        let interpolated_scope =
216            VariableInterpolator::interpolate_variables(&binding_spec.scope, context)?;
217
218        Ok(AzureRoleAssignment {
219            properties: AzureRoleAssignmentProperties {
220                role_definition_id,
221                principal_id,
222                scope: interpolated_scope,
223            },
224        })
225    }
226
227    /// Generate a human-readable role name
228    fn generate_role_name(&self, permission_set_id: &str) -> String {
229        permission_set_id
230            .split('/')
231            .map(|part| {
232                part.split('-')
233                    .map(|word| {
234                        let mut chars = word.chars();
235                        match chars.next() {
236                            None => String::new(),
237                            Some(first) => {
238                                first.to_uppercase().collect::<String>() + chars.as_str()
239                            }
240                        }
241                    })
242                    .collect::<Vec<String>>()
243                    .join(" ")
244            })
245            .collect::<Vec<String>>()
246            .join(" ")
247    }
248}
249
250impl Default for AzureRuntimePermissionsGenerator {
251    fn default() -> Self {
252        Self::new()
253    }
254}