use crate::{
error::{ErrorData, Result},
variables::VariableInterpolator,
BindingTarget, PermissionContext,
};
use alien_core::PermissionSet;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct GcpCustomRole {
pub title: String,
pub description: String,
pub stage: String,
pub included_permissions: Vec<String>,
pub name: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct GcpIamCondition {
pub title: String,
pub description: String,
pub expression: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct GcpIamBinding {
pub role: String,
pub members: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub condition: Option<GcpIamCondition>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct GcpIamBindings {
pub bindings: Vec<GcpIamBinding>,
}
pub struct GcpRuntimePermissionsGenerator;
impl GcpRuntimePermissionsGenerator {
pub fn new() -> Self {
Self
}
pub fn generate_custom_role(
&self,
permission_set: &PermissionSet,
context: &PermissionContext,
) -> Result<GcpCustomRole> {
let gcp_platform_permissions = permission_set.platforms.gcp.as_ref().ok_or_else(|| {
alien_error::AlienError::new(ErrorData::PlatformNotSupported {
platform: "gcp".to_string(),
permission_set_id: permission_set.id.clone(),
})
})?;
let mut all_permissions = Vec::new();
for platform_permission in gcp_platform_permissions {
if let Some(permissions) = &platform_permission.grant.permissions {
all_permissions.extend(permissions.clone());
}
}
if all_permissions.is_empty() {
return Err(alien_error::AlienError::new(ErrorData::GeneratorError {
platform: "gcp".to_string(),
message: "GCP permission grant must have 'permissions' field".to_string(),
}));
}
let role_name = self.generate_role_name(&permission_set.id);
let role_id = self.generate_role_id(&permission_set.id);
let project = context.project_name.as_deref().unwrap_or("PROJECT_NAME");
let full_role_name = format!("projects/{}/roles/{}", project, role_id);
Ok(GcpCustomRole {
title: role_name,
description: permission_set.description.clone(),
stage: "GA".to_string(),
included_permissions: all_permissions,
name: full_role_name,
})
}
pub fn generate_bindings(
&self,
permission_set: &PermissionSet,
binding_target: BindingTarget,
context: &PermissionContext,
) -> Result<GcpIamBindings> {
let gcp_platform_permissions = permission_set.platforms.gcp.as_ref().ok_or_else(|| {
alien_error::AlienError::new(ErrorData::PlatformNotSupported {
platform: "gcp".to_string(),
permission_set_id: permission_set.id.clone(),
})
})?;
let role_id = self.generate_role_id(&permission_set.id);
let project = context.project_name.as_deref().unwrap_or("PROJECT_NAME");
let full_role_name = format!("projects/{}/roles/{}", project, role_id);
let service_account = format!(
"serviceAccount:{}@{}.iam.gserviceaccount.com",
context
.service_account_name
.as_deref()
.unwrap_or("SERVICE_ACCOUNT"),
project
);
let mut bindings = Vec::new();
for platform_permission in gcp_platform_permissions {
let binding_spec = match binding_target {
BindingTarget::Stack => {
platform_permission.binding.stack.as_ref().ok_or_else(|| {
alien_error::AlienError::new(ErrorData::BindingTargetNotSupported {
platform: "gcp".to_string(),
binding_target: "stack".to_string(),
permission_set_id: permission_set.id.clone(),
})
})?
}
BindingTarget::Resource => platform_permission
.binding
.resource
.as_ref()
.ok_or_else(|| {
alien_error::AlienError::new(ErrorData::BindingTargetNotSupported {
platform: "gcp".to_string(),
binding_target: "resource".to_string(),
permission_set_id: permission_set.id.clone(),
})
})?,
};
let mut binding = GcpIamBinding {
role: full_role_name.clone(),
members: vec![service_account.clone()],
condition: None,
};
if let Some(gcp_condition) = &binding_spec.condition {
let interpolated_condition = self.interpolate_condition(gcp_condition, context)?;
binding.condition = Some(GcpIamCondition {
title: interpolated_condition.title.clone(),
description: format!("Limit to {}", interpolated_condition.title),
expression: interpolated_condition.expression,
});
}
bindings.push(binding);
}
Ok(GcpIamBindings { bindings })
}
fn generate_role_name(&self, permission_set_id: &str) -> String {
permission_set_id
.split('/')
.map(|part| {
part.split('-')
.map(|word| {
let mut chars = word.chars();
match chars.next() {
None => String::new(),
Some(first) => {
first.to_uppercase().collect::<String>() + chars.as_str()
}
}
})
.collect::<Vec<String>>()
.join(" ")
})
.collect::<Vec<String>>()
.join(" ")
}
fn generate_role_id(&self, permission_set_id: &str) -> String {
let all_parts: Vec<&str> = permission_set_id
.split('/')
.flat_map(|part| part.split('-'))
.collect();
all_parts
.iter()
.enumerate()
.map(|(i, word)| {
if i == 0 {
word.to_lowercase()
} else {
let mut chars = word.chars();
match chars.next() {
None => String::new(),
Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
}
}
})
.collect::<String>()
}
fn interpolate_condition(
&self,
condition: &alien_core::GcpCondition,
context: &PermissionContext,
) -> Result<alien_core::GcpCondition> {
let interpolated_title =
VariableInterpolator::interpolate_variables(&condition.title, context)?;
let interpolated_expression =
VariableInterpolator::interpolate_variables(&condition.expression, context)?;
Ok(alien_core::GcpCondition {
title: interpolated_title,
expression: interpolated_expression,
})
}
}
impl Default for GcpRuntimePermissionsGenerator {
fn default() -> Self {
Self::new()
}
}