Skip to main content

alien_core/resources/
service_account.rs

1use crate::error::{ErrorData, Result};
2use crate::permissions::{PermissionProfile, PermissionSet, PermissionSetReference};
3use crate::resource::{ResourceDefinition, ResourceOutputsDefinition, ResourceRef, ResourceType};
4use alien_error::AlienError;
5use bon::Builder;
6use serde::{Deserialize, Serialize};
7use std::any::Any;
8use std::fmt::Debug;
9
10/// Represents a non-human identity that can be assumed by compute services
11/// such as Lambda, Cloud Run, ECS, Container Apps, etc.
12///
13/// Maps to:
14/// - AWS: IAM Role
15/// - GCP: Service Account
16/// - Azure: User-assigned Managed Identity
17///
18/// The ServiceAccount is automatically created from permission profiles in the stack
19/// and contains the resolved permission sets for both stack-level and resource-scoped access.
20#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Builder)]
21#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
22#[serde(rename_all = "camelCase", deny_unknown_fields)]
23#[builder(start_fn = new)]
24pub struct ServiceAccount {
25    /// Identifier for the service account. Must contain only alphanumeric characters, hyphens, and underscores ([A-Za-z0-9-_]).
26    /// Maximum 64 characters.
27    #[builder(start_fn)]
28    pub id: String,
29
30    /// Stack-level permission sets that apply to all resources in the stack.
31    /// These are derived from the "*" scope in the permission profile.
32    /// Resource-scoped permissions are handled by individual resource controllers.
33    #[builder(field)]
34    pub stack_permission_sets: Vec<PermissionSet>,
35}
36
37impl ServiceAccount {
38    /// The resource type identifier for ServiceAccount
39    pub const RESOURCE_TYPE: ResourceType = ResourceType::from_static("service-account");
40
41    /// Returns the service account's unique identifier.
42    pub fn id(&self) -> &str {
43        &self.id
44    }
45
46    /// Creates a ServiceAccount from a permission profile by resolving permission set references.
47    /// This is used by the stack processor to convert profiles into concrete ServiceAccount resources.
48    /// Only stack-level permissions ("*" scope) are processed - resource-scoped permissions are
49    /// handled by individual resource controllers when they create their resources.
50    pub fn from_permission_profile(
51        id: String,
52        profile: &PermissionProfile,
53        permission_set_resolver: impl Fn(&str) -> Option<PermissionSet>,
54    ) -> Result<Self> {
55        let mut stack_permission_sets = Vec::new();
56
57        // Only process stack-level permissions ("*" scope)
58        if let Some(permission_set_refs) = profile.0.get("*") {
59            for permission_set_ref in permission_set_refs {
60                let permission_set = match permission_set_ref {
61                    PermissionSetReference::Name(name) => {
62                        // Look up built-in permission set by name
63                        permission_set_resolver(&name).ok_or_else(|| {
64                            AlienError::new(ErrorData::GenericError {
65                                message: format!(
66                                    "Permission set '{}' not found for service account '{}'",
67                                    name, id
68                                ),
69                            })
70                        })?
71                    }
72                    PermissionSetReference::Inline(inline_permission_set) => {
73                        // Use the inline permission set directly
74                        inline_permission_set.clone()
75                    }
76                };
77                stack_permission_sets.push(permission_set);
78            }
79        }
80
81        Ok(ServiceAccount {
82            id,
83            stack_permission_sets,
84        })
85    }
86}
87
88impl ServiceAccountBuilder {
89    /// Adds a stack-level permission set to the service account.
90    /// Stack-level permissions apply to all resources in the stack.
91    pub fn stack_permission_set(mut self, permission_set: PermissionSet) -> Self {
92        self.stack_permission_sets.push(permission_set);
93        self
94    }
95}
96
97// Implementation of ResourceDefinition trait for ServiceAccount
98#[typetag::serde(name = "service-account")]
99impl ResourceDefinition for ServiceAccount {
100    fn resource_type() -> ResourceType {
101        Self::RESOURCE_TYPE.clone()
102    }
103
104    fn get_resource_type(&self) -> ResourceType {
105        Self::resource_type()
106    }
107
108    fn id(&self) -> &str {
109        &self.id
110    }
111
112    fn get_dependencies(&self) -> Vec<ResourceRef> {
113        // ServiceAccount doesn't depend on other resources directly
114        // Dependencies will be managed through the stack processor
115        Vec::new()
116    }
117
118    fn validate_update(&self, new_config: &dyn ResourceDefinition) -> Result<()> {
119        let new_service_account = new_config
120            .as_any()
121            .downcast_ref::<ServiceAccount>()
122            .ok_or_else(|| {
123                AlienError::new(ErrorData::UnexpectedResourceType {
124                    resource_id: self.id.clone(),
125                    expected: Self::RESOURCE_TYPE,
126                    actual: new_config.get_resource_type(),
127                })
128            })?;
129
130        if self.id != new_service_account.id {
131            return Err(AlienError::new(ErrorData::InvalidResourceUpdate {
132                resource_id: self.id.clone(),
133                reason: "the 'id' field is immutable".to_string(),
134            }));
135        }
136
137        Ok(())
138    }
139
140    fn as_any(&self) -> &dyn Any {
141        self
142    }
143
144    fn as_any_mut(&mut self) -> &mut dyn Any {
145        self
146    }
147
148    fn box_clone(&self) -> Box<dyn ResourceDefinition> {
149        Box::new(self.clone())
150    }
151
152    fn resource_eq(&self, other: &dyn ResourceDefinition) -> bool {
153        other.as_any().downcast_ref::<ServiceAccount>() == Some(self)
154    }
155}
156
157/// Outputs generated by a successfully provisioned ServiceAccount.
158#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
159#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
160#[serde(rename_all = "camelCase")]
161pub struct ServiceAccountOutputs {
162    /// The platform-specific identifier of the service account
163    /// - AWS: Role ARN
164    /// - GCP: Service Account email
165    /// - Azure: Managed Identity client ID
166    pub identity: String,
167
168    /// The platform-specific resource name/ID
169    /// - AWS: Role name
170    /// - GCP: Service Account unique ID
171    /// - Azure: Managed Identity resource ID
172    pub resource_id: String,
173}
174
175#[typetag::serde(name = "service-account")]
176impl ResourceOutputsDefinition for ServiceAccountOutputs {
177    fn resource_type() -> ResourceType {
178        ServiceAccount::RESOURCE_TYPE.clone()
179    }
180
181    fn as_any(&self) -> &dyn Any {
182        self
183    }
184
185    fn box_clone(&self) -> Box<dyn ResourceOutputsDefinition> {
186        Box::new(self.clone())
187    }
188
189    fn outputs_eq(&self, other: &dyn ResourceOutputsDefinition) -> bool {
190        other.as_any().downcast_ref::<ServiceAccountOutputs>() == Some(self)
191    }
192}