Skip to main content

alien_permissions/generators/
gcp_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/// GCP custom role definition
10#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
11#[serde(rename_all = "camelCase")]
12pub struct GcpCustomRole {
13    /// Human-readable role title
14    pub title: String,
15    /// Description of what the role allows
16    pub description: String,
17    /// Role stage (GA, BETA, ALPHA)
18    pub stage: String,
19    /// List of GCP permissions included in this role
20    pub included_permissions: Vec<String>,
21    /// Full GCP role name (projects/{project}/roles/{roleId})
22    pub name: String,
23}
24
25/// GCP IAM binding condition
26#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
27#[serde(rename_all = "camelCase")]
28pub struct GcpIamCondition {
29    /// Human-readable condition title
30    pub title: String,
31    /// Description of the condition
32    pub description: String,
33    /// CEL expression for the condition
34    pub expression: String,
35}
36
37/// GCP IAM policy binding
38#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
39#[serde(rename_all = "camelCase")]
40pub struct GcpIamBinding {
41    /// Role to bind to members
42    pub role: String,
43    /// List of members (users, service accounts, groups)
44    pub members: Vec<String>,
45    /// Optional condition for conditional IAM
46    #[serde(skip_serializing_if = "Option::is_none")]
47    pub condition: Option<GcpIamCondition>,
48}
49
50/// GCP IAM bindings wrapper
51#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
52#[serde(rename_all = "camelCase")]
53pub struct GcpIamBindings {
54    /// List of IAM bindings
55    pub bindings: Vec<GcpIamBinding>,
56}
57
58/// GCP runtime permissions generator for custom roles and IAM bindings
59pub struct GcpRuntimePermissionsGenerator;
60
61impl GcpRuntimePermissionsGenerator {
62    /// Create a new GCP runtime permissions generator
63    pub fn new() -> Self {
64        Self
65    }
66
67    /// Generate a GCP custom role from a permission set
68    ///
69    /// Takes a PermissionSet and produces GCP custom role definitions
70    /// that can be created at runtime.
71    pub fn generate_custom_role(
72        &self,
73        permission_set: &PermissionSet,
74        context: &PermissionContext,
75    ) -> Result<GcpCustomRole> {
76        let gcp_platform_permissions = permission_set.platforms.gcp.as_ref().ok_or_else(|| {
77            alien_error::AlienError::new(ErrorData::PlatformNotSupported {
78                platform: "gcp".to_string(),
79                permission_set_id: permission_set.id.clone(),
80            })
81        })?;
82
83        // For custom role generation, we aggregate all permissions from all platform permissions
84        let mut all_permissions = Vec::new();
85
86        for platform_permission in gcp_platform_permissions {
87            if let Some(permissions) = &platform_permission.grant.permissions {
88                all_permissions.extend(permissions.clone());
89            }
90        }
91
92        if all_permissions.is_empty() {
93            return Err(alien_error::AlienError::new(ErrorData::GeneratorError {
94                platform: "gcp".to_string(),
95                message: "GCP permission grant must have 'permissions' field".to_string(),
96            }));
97        }
98
99        let role_name = self.generate_role_name(&permission_set.id);
100        let role_id = self.generate_role_id(&permission_set.id);
101
102        // Get project from context for full role name
103        let project = context.project_name.as_deref().unwrap_or("PROJECT_NAME");
104        let full_role_name = format!("projects/{}/roles/{}", project, role_id);
105
106        Ok(GcpCustomRole {
107            title: role_name,
108            description: permission_set.description.clone(),
109            stage: "GA".to_string(),
110            included_permissions: all_permissions,
111            name: full_role_name,
112        })
113    }
114
115    /// Generate IAM bindings from permission set and binding target
116    ///
117    /// Takes a PermissionSet and binding target, produces GCP IAM bindings
118    /// that can be applied to resources at runtime.
119    pub fn generate_bindings(
120        &self,
121        permission_set: &PermissionSet,
122        binding_target: BindingTarget,
123        context: &PermissionContext,
124    ) -> Result<GcpIamBindings> {
125        let gcp_platform_permissions = permission_set.platforms.gcp.as_ref().ok_or_else(|| {
126            alien_error::AlienError::new(ErrorData::PlatformNotSupported {
127                platform: "gcp".to_string(),
128                permission_set_id: permission_set.id.clone(),
129            })
130        })?;
131
132        let role_id = self.generate_role_id(&permission_set.id);
133        let project = context.project_name.as_deref().unwrap_or("PROJECT_NAME");
134        let full_role_name = format!("projects/{}/roles/{}", project, role_id);
135
136        // For this example, we'll use a placeholder service account
137        let service_account = format!(
138            "serviceAccount:{}@{}.iam.gserviceaccount.com",
139            context
140                .service_account_name
141                .as_deref()
142                .unwrap_or("SERVICE_ACCOUNT"),
143            project
144        );
145
146        let mut bindings = Vec::new();
147
148        // Process each GCP platform permission in the permission set
149        for platform_permission in gcp_platform_permissions {
150            let binding_spec = match binding_target {
151                BindingTarget::Stack => {
152                    platform_permission.binding.stack.as_ref().ok_or_else(|| {
153                        alien_error::AlienError::new(ErrorData::BindingTargetNotSupported {
154                            platform: "gcp".to_string(),
155                            binding_target: "stack".to_string(),
156                            permission_set_id: permission_set.id.clone(),
157                        })
158                    })?
159                }
160                BindingTarget::Resource => platform_permission
161                    .binding
162                    .resource
163                    .as_ref()
164                    .ok_or_else(|| {
165                        alien_error::AlienError::new(ErrorData::BindingTargetNotSupported {
166                            platform: "gcp".to_string(),
167                            binding_target: "resource".to_string(),
168                            permission_set_id: permission_set.id.clone(),
169                        })
170                    })?,
171            };
172
173            let mut binding = GcpIamBinding {
174                role: full_role_name.clone(),
175                members: vec![service_account.clone()],
176                condition: None,
177            };
178
179            // Add conditions if present
180            if let Some(gcp_condition) = &binding_spec.condition {
181                let interpolated_condition = self.interpolate_condition(gcp_condition, context)?;
182                binding.condition = Some(GcpIamCondition {
183                    title: interpolated_condition.title.clone(),
184                    description: format!("Limit to {}", interpolated_condition.title),
185                    expression: interpolated_condition.expression,
186                });
187            }
188
189            bindings.push(binding);
190        }
191
192        Ok(GcpIamBindings { bindings })
193    }
194
195    /// Generate a human-readable role name
196    fn generate_role_name(&self, permission_set_id: &str) -> String {
197        permission_set_id
198            .split('/')
199            .map(|part| {
200                part.split('-')
201                    .map(|word| {
202                        let mut chars = word.chars();
203                        match chars.next() {
204                            None => String::new(),
205                            Some(first) => {
206                                first.to_uppercase().collect::<String>() + chars.as_str()
207                            }
208                        }
209                    })
210                    .collect::<Vec<String>>()
211                    .join(" ")
212            })
213            .collect::<Vec<String>>()
214            .join(" ")
215    }
216
217    /// Generate a valid GCP role ID
218    fn generate_role_id(&self, permission_set_id: &str) -> String {
219        // Convert to camelCase and remove special characters for valid GCP role ID
220        let all_parts: Vec<&str> = permission_set_id
221            .split('/')
222            .flat_map(|part| part.split('-'))
223            .collect();
224
225        all_parts
226            .iter()
227            .enumerate()
228            .map(|(i, word)| {
229                if i == 0 {
230                    word.to_lowercase()
231                } else {
232                    let mut chars = word.chars();
233                    match chars.next() {
234                        None => String::new(),
235                        Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
236                    }
237                }
238            })
239            .collect::<String>()
240    }
241
242    /// Interpolate variables in a GCP condition
243    fn interpolate_condition(
244        &self,
245        condition: &alien_core::GcpCondition,
246        context: &PermissionContext,
247    ) -> Result<alien_core::GcpCondition> {
248        let interpolated_title =
249            VariableInterpolator::interpolate_variables(&condition.title, context)?;
250        let interpolated_expression =
251            VariableInterpolator::interpolate_variables(&condition.expression, context)?;
252
253        Ok(alien_core::GcpCondition {
254            title: interpolated_title,
255            expression: interpolated_expression,
256        })
257    }
258}
259
260impl Default for GcpRuntimePermissionsGenerator {
261    fn default() -> Self {
262        Self::new()
263    }
264}