Skip to main content

alien_permissions/generators/
aws_runtime.rs

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