alien-permissions 1.3.0

Alien Developer Platform
Documentation
use crate::{
    error::{ErrorData, Result},
    variables::VariableInterpolator,
    BindingTarget, PermissionContext,
};
use alien_core::PermissionSet;
use serde::{Deserialize, Serialize};

/// Azure role definition
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "PascalCase")]
pub struct AzureRoleDefinition {
    /// Human-readable role name
    pub name: String,
    /// Role ID (will be generated by Azure)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub id: Option<String>,
    /// Whether this is a custom role
    pub is_custom: bool,
    /// Description of what the role allows
    pub description: String,
    /// List of allowed actions
    pub actions: Vec<String>,
    /// List of denied actions
    pub not_actions: Vec<String>,
    /// List of allowed data actions
    pub data_actions: Vec<String>,
    /// List of denied data actions
    pub not_data_actions: Vec<String>,
    /// Scopes where this role can be assigned
    pub assignable_scopes: Vec<String>,
}

/// Azure role assignment properties
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct AzureRoleAssignmentProperties {
    /// Role definition ID
    pub role_definition_id: String,
    /// Principal ID (user, group, or service principal)
    pub principal_id: String,
    /// Scope where the role is assigned
    pub scope: String,
}

/// Azure role assignment
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct AzureRoleAssignment {
    /// Role assignment properties
    pub properties: AzureRoleAssignmentProperties,
}

/// Azure runtime permissions generator for role definitions and role assignments
pub struct AzureRuntimePermissionsGenerator;

impl AzureRuntimePermissionsGenerator {
    /// Create a new Azure runtime permissions generator
    pub fn new() -> Self {
        Self
    }

    /// Generate an Azure role definition from a permission set
    ///
    /// Takes a PermissionSet and produces Azure role definitions
    /// that can be created at runtime.
    pub fn generate_role_definition(
        &self,
        permission_set: &PermissionSet,
        binding_target: BindingTarget,
        context: &PermissionContext,
    ) -> Result<AzureRoleDefinition> {
        let azure_platform_permissions =
            permission_set.platforms.azure.as_ref().ok_or_else(|| {
                alien_error::AlienError::new(ErrorData::PlatformNotSupported {
                    platform: "azure".to_string(),
                    permission_set_id: permission_set.id.clone(),
                })
            })?;

        let role_name = self.generate_role_name(&permission_set.id);

        // Aggregate actions and data actions from all platform permissions
        let mut all_actions = Vec::new();
        let mut all_data_actions = Vec::new();
        let mut assignable_scopes = Vec::new();

        for platform_permission in azure_platform_permissions {
            // Extract actions and data actions
            if let Some(actions) = &platform_permission.grant.actions {
                all_actions.extend(actions.clone());
            }
            if let Some(data_actions) = &platform_permission.grant.data_actions {
                all_data_actions.extend(data_actions.clone());
            }

            // Generate assignable scopes based on binding target
            let binding_spec = match binding_target {
                BindingTarget::Stack => {
                    platform_permission.binding.stack.as_ref().ok_or_else(|| {
                        alien_error::AlienError::new(ErrorData::BindingTargetNotSupported {
                            platform: "azure".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: "azure".to_string(),
                            binding_target: "resource".to_string(),
                            permission_set_id: permission_set.id.clone(),
                        })
                    })?,
            };

            // Interpolate variables in the scope
            let interpolated_scope =
                VariableInterpolator::interpolate_variables(&binding_spec.scope, context)?;
            assignable_scopes.push(interpolated_scope);
        }

        // Remove duplicates and sort
        all_actions.sort();
        all_actions.dedup();
        all_data_actions.sort();
        all_data_actions.dedup();
        assignable_scopes.sort();
        assignable_scopes.dedup();

        Ok(AzureRoleDefinition {
            name: role_name,
            id: None, // Will be generated by Azure
            is_custom: true,
            description: permission_set.description.clone(),
            actions: all_actions,
            not_actions: vec![],
            data_actions: all_data_actions,
            not_data_actions: vec![],
            assignable_scopes,
        })
    }

    /// Generate an Azure role assignment
    ///
    /// Takes a PermissionSet and binding target, produces Azure role assignments
    /// that can be created at runtime.
    pub fn generate_role_assignment(
        &self,
        permission_set: &PermissionSet,
        binding_target: BindingTarget,
        context: &PermissionContext,
    ) -> Result<AzureRoleAssignment> {
        let azure_platform_permissions =
            permission_set.platforms.azure.as_ref().ok_or_else(|| {
                alien_error::AlienError::new(ErrorData::PlatformNotSupported {
                    platform: "azure".to_string(),
                    permission_set_id: permission_set.id.clone(),
                })
            })?;

        // For this example, we'll use placeholder values
        let role_definition_id = format!(
            "/subscriptions/{}/providers/Microsoft.Authorization/roleDefinitions/${{roleDefinitionGuid}}",
            context.subscription_id.as_deref().unwrap_or("SUBSCRIPTION_ID")
        );

        let principal_id = context
            .principal_id
            .as_deref()
            .unwrap_or("PRINCIPAL_ID")
            .to_string();

        // Use the first platform permission's binding for simplicity
        // In practice, you might want to handle multiple bindings differently
        let first_platform_permission = &azure_platform_permissions[0];
        let binding_spec = match binding_target {
            BindingTarget::Stack => first_platform_permission
                .binding
                .stack
                .as_ref()
                .ok_or_else(|| {
                    alien_error::AlienError::new(ErrorData::BindingTargetNotSupported {
                        platform: "azure".to_string(),
                        binding_target: "stack".to_string(),
                        permission_set_id: permission_set.id.clone(),
                    })
                })?,
            BindingTarget::Resource => first_platform_permission
                .binding
                .resource
                .as_ref()
                .ok_or_else(|| {
                    alien_error::AlienError::new(ErrorData::BindingTargetNotSupported {
                        platform: "azure".to_string(),
                        binding_target: "resource".to_string(),
                        permission_set_id: permission_set.id.clone(),
                    })
                })?,
        };

        // Interpolate variables in the scope
        let interpolated_scope =
            VariableInterpolator::interpolate_variables(&binding_spec.scope, context)?;

        Ok(AzureRoleAssignment {
            properties: AzureRoleAssignmentProperties {
                role_definition_id,
                principal_id,
                scope: interpolated_scope,
            },
        })
    }

    /// Generate a human-readable role name
    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(" ")
    }
}

impl Default for AzureRuntimePermissionsGenerator {
    fn default() -> Self {
        Self::new()
    }
}