alien_permissions/generators/
gcp_runtime.rs1use crate::{
2 error::{ErrorData, Result},
3 variables::VariableInterpolator,
4 BindingTarget, PermissionContext,
5};
6use alien_core::PermissionSet;
7use serde::{Deserialize, Serialize};
8
9#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
11#[serde(rename_all = "camelCase")]
12pub struct GcpCustomRole {
13 pub title: String,
15 pub description: String,
17 pub stage: String,
19 pub included_permissions: Vec<String>,
21 pub name: String,
23}
24
25#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
27#[serde(rename_all = "camelCase")]
28pub struct GcpIamCondition {
29 pub title: String,
31 pub description: String,
33 pub expression: String,
35}
36
37#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
39#[serde(rename_all = "camelCase")]
40pub struct GcpIamBinding {
41 pub role: String,
43 pub members: Vec<String>,
45 #[serde(skip_serializing_if = "Option::is_none")]
47 pub condition: Option<GcpIamCondition>,
48}
49
50#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
52#[serde(rename_all = "camelCase")]
53pub struct GcpIamBindings {
54 pub bindings: Vec<GcpIamBinding>,
56}
57
58pub struct GcpRuntimePermissionsGenerator;
60
61impl GcpRuntimePermissionsGenerator {
62 pub fn new() -> Self {
64 Self
65 }
66
67 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 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 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 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 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 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 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 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 fn generate_role_id(&self, permission_set_id: &str) -> String {
224 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 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}