Skip to main content

alien_permissions/generators/
aws_runtime.rs

1use crate::{
2    error::{ErrorData, Result},
3    generators::labels::{entry_pascal_label, has_explicit_label},
4    variables::VariableInterpolator,
5    BindingTarget, PermissionContext,
6};
7use alien_core::{PermissionGrant, PermissionSet};
8use indexmap::IndexMap;
9use serde::{Deserialize, Serialize};
10
11/// AWS IAM policy statement
12#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
13#[serde(rename_all = "PascalCase")]
14pub struct AwsIamStatement {
15    /// Statement ID
16    pub sid: String,
17    /// Effect (Allow/Deny)
18    pub effect: String,
19    /// List of IAM actions
20    pub action: Vec<String>,
21    /// List of resource ARNs
22    pub resource: Vec<String>,
23    /// Optional conditions
24    #[serde(skip_serializing_if = "Option::is_none")]
25    pub condition: Option<IndexMap<String, IndexMap<String, String>>>,
26}
27
28/// AWS IAM policy document
29#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
30#[serde(rename_all = "PascalCase")]
31pub struct AwsIamPolicy {
32    /// Policy version
33    pub version: String,
34    /// List of policy statements
35    pub statement: Vec<AwsIamStatement>,
36}
37
38/// AWS runtime permissions generator for IAM policy documents
39pub struct AwsRuntimePermissionsGenerator;
40
41impl AwsRuntimePermissionsGenerator {
42    /// Create a new AWS runtime permissions generator
43    pub fn new() -> Self {
44        Self
45    }
46
47    /// Generate an IAM policy document from a permission set and binding target
48    ///
49    /// Takes a PermissionSet and where to bind it, produces AWS IAM policy documents
50    /// that can be created at runtime.
51    pub fn generate_policy(
52        &self,
53        permission_set: &PermissionSet,
54        binding_target: BindingTarget,
55        context: &PermissionContext,
56    ) -> Result<AwsIamPolicy> {
57        let aws_platform_permissions = permission_set.platforms.aws.as_ref().ok_or_else(|| {
58            alien_error::AlienError::new(ErrorData::PlatformNotSupported {
59                platform: "aws".to_string(),
60                permission_set_id: permission_set.id.clone(),
61            })
62        })?;
63
64        let mut statements = Vec::new();
65
66        // Process each AWS platform permission in the permission set
67        for platform_permission in aws_platform_permissions {
68            let actions = platform_permission.grant.actions.as_ref().ok_or_else(|| {
69                alien_error::AlienError::new(ErrorData::GeneratorError {
70                    platform: "aws".to_string(),
71                    message: "AWS permission grant must have 'actions' field".to_string(),
72                })
73            })?;
74
75            let binding_spec = match binding_target {
76                BindingTarget::Stack => {
77                    platform_permission.binding.stack.as_ref().ok_or_else(|| {
78                        alien_error::AlienError::new(ErrorData::BindingTargetNotSupported {
79                            platform: "aws".to_string(),
80                            binding_target: "stack".to_string(),
81                            permission_set_id: permission_set.id.clone(),
82                        })
83                    })?
84                }
85                BindingTarget::Resource => platform_permission
86                    .binding
87                    .resource
88                    .as_ref()
89                    .ok_or_else(|| {
90                        alien_error::AlienError::new(ErrorData::BindingTargetNotSupported {
91                            platform: "aws".to_string(),
92                            binding_target: "resource".to_string(),
93                            permission_set_id: permission_set.id.clone(),
94                        })
95                    })?,
96            };
97
98            let resources =
99                VariableInterpolator::interpolate_string_list(&binding_spec.resources, context)?;
100            let conditions = self.extract_conditions(binding_spec, context)?;
101
102            let statement_id = self.statement_id(
103                permission_set,
104                &platform_permission.grant,
105                platform_permission.label.as_deref(),
106                aws_platform_permissions.len() > 1,
107            );
108
109            let statement = AwsIamStatement {
110                sid: statement_id,
111                effect: platform_permission.effect.as_str().to_string(),
112                action: actions.clone(),
113                resource: resources,
114                condition: if conditions.is_empty() {
115                    None
116                } else {
117                    Some(conditions)
118                },
119            };
120
121            statements.push(statement);
122        }
123
124        Ok(AwsIamPolicy {
125            version: "2012-10-17".to_string(),
126            statement: statements,
127        })
128    }
129
130    /// Extract AWS conditions from binding spec
131    fn extract_conditions(
132        &self,
133        binding_spec: &alien_core::AwsBindingSpec,
134        context: &PermissionContext,
135    ) -> Result<IndexMap<String, IndexMap<String, String>>> {
136        if let Some(condition_template) = &binding_spec.condition {
137            let mut interpolated_conditions = IndexMap::new();
138
139            for (condition_key, condition_values) in condition_template {
140                let condition_key =
141                    VariableInterpolator::interpolate_variables(condition_key, context)?;
142                let mut interpolated_values = IndexMap::new();
143
144                for (value_key, value_template) in condition_values {
145                    let value_key =
146                        VariableInterpolator::interpolate_variables(value_key, context)?;
147                    let interpolated_value =
148                        VariableInterpolator::interpolate_variables(value_template, context)?;
149                    interpolated_values.insert(value_key, interpolated_value);
150                }
151
152                interpolated_conditions.insert(condition_key, interpolated_values);
153            }
154
155            Ok(interpolated_conditions)
156        } else {
157            Ok(IndexMap::new())
158        }
159    }
160
161    /// Generate a valid IAM statement ID from a permission set ID
162    fn generate_statement_id(&self, permission_set_id: &str) -> String {
163        // Convert to PascalCase and remove special characters for valid AWS Sid
164        permission_set_id
165            .split('/')
166            .map(|part| {
167                part.split('-')
168                    .map(|word| {
169                        let mut chars = word.chars();
170                        match chars.next() {
171                            None => String::new(),
172                            Some(first) => {
173                                first.to_uppercase().collect::<String>()
174                                    + &chars.as_str().to_lowercase()
175                            }
176                        }
177                    })
178                    .collect::<String>()
179            })
180            .collect::<String>()
181    }
182
183    fn statement_id(
184        &self,
185        permission_set: &PermissionSet,
186        grant: &PermissionGrant,
187        explicit_label: Option<&str>,
188        include_entry_label: bool,
189    ) -> String {
190        if has_explicit_label(explicit_label) {
191            return entry_pascal_label(explicit_label, grant);
192        }
193
194        let base = self.generate_statement_id(&permission_set.id);
195        if include_entry_label {
196            format!("{base}{}", entry_pascal_label(explicit_label, grant))
197        } else {
198            base
199        }
200    }
201}
202
203impl Default for AwsRuntimePermissionsGenerator {
204    fn default() -> Self {
205        Self::new()
206    }
207}