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<GcpIamBinding> = Vec::new();
147
148        // All entries in a permission set share the same custom role. We only
149        // need one binding per unique condition — entries without conditions
150        // collapse into a single unconditional binding.
151        let mut has_unconditional = false;
152
153        for platform_permission in gcp_platform_permissions {
154            let binding_spec = match binding_target {
155                BindingTarget::Stack => match platform_permission.binding.stack.as_ref() {
156                    Some(spec) => spec,
157                    None => continue,
158                },
159                BindingTarget::Resource => match platform_permission.binding.resource.as_ref() {
160                    Some(spec) => spec,
161                    None => continue,
162                },
163            };
164
165            if let Some(gcp_condition) = &binding_spec.condition {
166                let interpolated_condition = self.interpolate_condition(gcp_condition, context)?;
167                let condition = GcpIamCondition {
168                    title: interpolated_condition.title.clone(),
169                    description: format!("Limit to {}", interpolated_condition.title),
170                    expression: interpolated_condition.expression,
171                };
172
173                // Only add if we don't already have a binding with this condition
174                let already_exists = bindings.iter().any(|b| {
175                    b.condition
176                        .as_ref()
177                        .map(|c| c.expression == condition.expression)
178                        .unwrap_or(false)
179                });
180                if !already_exists {
181                    bindings.push(GcpIamBinding {
182                        role: full_role_name.clone(),
183                        members: vec![service_account.clone()],
184                        condition: Some(condition),
185                    });
186                }
187            } else if !has_unconditional {
188                has_unconditional = true;
189                bindings.push(GcpIamBinding {
190                    role: full_role_name.clone(),
191                    members: vec![service_account.clone()],
192                    condition: None,
193                });
194            }
195        }
196
197        Ok(GcpIamBindings { bindings })
198    }
199
200    /// Generate a human-readable role name
201    fn generate_role_name(&self, permission_set_id: &str) -> String {
202        permission_set_id
203            .split('/')
204            .map(|part| {
205                part.split('-')
206                    .map(|word| {
207                        let mut chars = word.chars();
208                        match chars.next() {
209                            None => String::new(),
210                            Some(first) => {
211                                first.to_uppercase().collect::<String>() + chars.as_str()
212                            }
213                        }
214                    })
215                    .collect::<Vec<String>>()
216                    .join(" ")
217            })
218            .collect::<Vec<String>>()
219            .join(" ")
220    }
221
222    /// Generate a valid GCP role ID
223    fn generate_role_id(&self, permission_set_id: &str) -> String {
224        // Convert to camelCase and remove special characters for valid GCP role ID
225        let all_parts: Vec<&str> = permission_set_id
226            .split('/')
227            .flat_map(|part| part.split('-'))
228            .collect();
229
230        all_parts
231            .iter()
232            .enumerate()
233            .map(|(i, word)| {
234                if i == 0 {
235                    word.to_lowercase()
236                } else {
237                    let mut chars = word.chars();
238                    match chars.next() {
239                        None => String::new(),
240                        Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
241                    }
242                }
243            })
244            .collect::<String>()
245    }
246
247    /// Interpolate variables in a GCP condition
248    fn interpolate_condition(
249        &self,
250        condition: &alien_core::GcpCondition,
251        context: &PermissionContext,
252    ) -> Result<alien_core::GcpCondition> {
253        let interpolated_title =
254            VariableInterpolator::interpolate_variables(&condition.title, context)?;
255        let interpolated_expression =
256            VariableInterpolator::interpolate_variables(&condition.expression, context)?;
257
258        Ok(alien_core::GcpCondition {
259            title: interpolated_title,
260            expression: interpolated_expression,
261        })
262    }
263}
264
265impl Default for GcpRuntimePermissionsGenerator {
266    fn default() -> Self {
267        Self::new()
268    }
269}