Skip to main content

alien_core/
permissions.rs

1//! Defines core permission types and structures used across Alien Infra.
2
3use indexmap::IndexMap;
4use serde::{Deserialize, Serialize};
5
6#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
7#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
8pub enum AwsPermissionEffect {
9    #[default]
10    Allow,
11    Deny,
12}
13
14impl AwsPermissionEffect {
15    pub fn as_str(&self) -> &'static str {
16        match self {
17            Self::Allow => "Allow",
18            Self::Deny => "Deny",
19        }
20    }
21
22    pub fn is_allow(&self) -> bool {
23        matches!(self, Self::Allow)
24    }
25}
26
27/// Grant permissions for a specific cloud platform
28#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
29#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
30#[serde(rename_all = "camelCase", deny_unknown_fields)]
31pub struct PermissionGrant {
32    /// AWS IAM actions (only for AWS)
33    #[serde(skip_serializing_if = "Option::is_none")]
34    pub actions: Option<Vec<String>>,
35    /// GCP permissions that require an exact residual custom role.
36    #[serde(skip_serializing_if = "Option::is_none")]
37    pub permissions: Option<Vec<String>>,
38    /// Provider predefined roles to bind directly.
39    #[serde(skip_serializing_if = "Option::is_none")]
40    pub predefined_roles: Option<Vec<String>>,
41    /// GCP residual custom permissions to pair with predefined roles.
42    #[serde(skip_serializing_if = "Option::is_none")]
43    pub residual_permissions: Option<Vec<String>>,
44    /// Azure actions (only for Azure)
45    #[serde(skip_serializing_if = "Option::is_none")]
46    pub data_actions: Option<Vec<String>>,
47}
48
49/// AWS-specific binding specification
50#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
51#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
52#[serde(rename_all = "camelCase", deny_unknown_fields)]
53pub struct AwsBindingSpec {
54    /// Resource ARNs to bind to
55    pub resources: Vec<String>,
56    /// Optional condition for additional filtering (rare)
57    #[serde(skip_serializing_if = "Option::is_none")]
58    pub condition: Option<IndexMap<String, IndexMap<String, String>>>,
59}
60
61/// GCP-specific binding specification
62#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
63#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
64#[serde(rename_all = "camelCase", deny_unknown_fields)]
65pub struct GcpBindingSpec {
66    /// Scope (project/resource level)
67    pub scope: String,
68    /// Optional condition for filtering resources
69    #[serde(skip_serializing_if = "Option::is_none")]
70    pub condition: Option<GcpCondition>,
71}
72
73/// Azure-specific binding specification
74#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
75#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
76#[serde(rename_all = "camelCase", deny_unknown_fields)]
77pub struct AzureBindingSpec {
78    /// Scope (subscription/resource group/resource level)
79    pub scope: String,
80}
81
82/// Generic binding configuration for permissions
83#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
84#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
85#[serde(rename_all = "camelCase", deny_unknown_fields)]
86pub struct BindingConfiguration<T> {
87    /// Stack-level binding
88    #[serde(skip_serializing_if = "Option::is_none")]
89    pub stack: Option<T>,
90    /// Resource-level binding
91    #[serde(skip_serializing_if = "Option::is_none")]
92    pub resource: Option<T>,
93}
94
95impl<T> BindingConfiguration<T> {
96    /// Check if the binding configuration is empty (no stack or resource bindings)
97    pub fn is_empty(&self) -> bool {
98        self.stack.is_none() && self.resource.is_none()
99    }
100}
101
102/// GCP IAM condition
103#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
104#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
105#[serde(rename_all = "camelCase", deny_unknown_fields)]
106pub struct GcpCondition {
107    pub title: String,
108    pub expression: String,
109}
110
111/// AWS-specific platform permission configuration
112#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
113#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
114#[serde(rename_all = "camelCase", deny_unknown_fields)]
115pub struct AwsPlatformPermission {
116    /// Stable admin-facing label for this permission entry.
117    #[serde(skip_serializing_if = "Option::is_none")]
118    pub label: Option<String>,
119    /// Short admin-facing description of why this entry exists.
120    #[serde(skip_serializing_if = "Option::is_none")]
121    pub description: Option<String>,
122    /// IAM effect. Defaults to Allow.
123    #[serde(default, skip_serializing_if = "AwsPermissionEffect::is_allow")]
124    pub effect: AwsPermissionEffect,
125    /// What permissions to grant
126    pub grant: PermissionGrant,
127    /// How to bind the permissions (stack vs resource scope)
128    pub binding: BindingConfiguration<AwsBindingSpec>,
129}
130
131/// GCP-specific platform permission configuration
132#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
133#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
134#[serde(rename_all = "camelCase", deny_unknown_fields)]
135pub struct GcpPlatformPermission {
136    /// Stable admin-facing label for this permission entry.
137    #[serde(skip_serializing_if = "Option::is_none")]
138    pub label: Option<String>,
139    /// Short admin-facing description of why this entry exists.
140    #[serde(skip_serializing_if = "Option::is_none")]
141    pub description: Option<String>,
142    /// What permissions to grant
143    pub grant: PermissionGrant,
144    /// How to bind the permissions (stack vs resource scope)
145    pub binding: BindingConfiguration<GcpBindingSpec>,
146}
147
148/// Azure-specific platform permission configuration
149#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
150#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
151#[serde(rename_all = "camelCase", deny_unknown_fields)]
152pub struct AzurePlatformPermission {
153    /// Stable admin-facing label for this permission entry.
154    #[serde(skip_serializing_if = "Option::is_none")]
155    pub label: Option<String>,
156    /// Short admin-facing description of why this entry exists.
157    #[serde(skip_serializing_if = "Option::is_none")]
158    pub description: Option<String>,
159    /// What permissions to grant
160    pub grant: PermissionGrant,
161    /// How to bind the permissions (stack vs resource scope)
162    pub binding: BindingConfiguration<AzureBindingSpec>,
163}
164
165/// Platform-specific permission configurations
166#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
167#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
168#[serde(rename_all = "camelCase", deny_unknown_fields)]
169pub struct PlatformPermissions {
170    /// AWS permission configurations
171    #[serde(skip_serializing_if = "Option::is_none")]
172    pub aws: Option<Vec<AwsPlatformPermission>>,
173    /// GCP permission configurations
174    #[serde(skip_serializing_if = "Option::is_none")]
175    pub gcp: Option<Vec<GcpPlatformPermission>>,
176    /// Azure permission configurations
177    #[serde(skip_serializing_if = "Option::is_none")]
178    pub azure: Option<Vec<AzurePlatformPermission>>,
179}
180
181/// A permission set that can be applied across different cloud platforms
182#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
183#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
184#[serde(rename_all = "camelCase", deny_unknown_fields)]
185pub struct PermissionSet {
186    /// Unique identifier for the permission set (e.g., "storage/data-read")
187    pub id: String,
188    /// Human-readable description of what this permission set allows
189    pub description: String,
190    /// Platform-specific permission configurations
191    pub platforms: PlatformPermissions,
192}
193
194/// Reference to a permission set - either by name or inline definition
195#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
196#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
197#[serde(untagged)]
198pub enum PermissionSetReference {
199    /// Reference to a built-in permission set by name (e.g., "storage/data-read")
200    Name(String),
201    /// Inline permission set definition
202    Inline(PermissionSet),
203}
204
205impl PermissionSetReference {
206    /// Get the ID of the permission set, whether it's a reference or inline
207    pub fn id(&self) -> &str {
208        match self {
209            PermissionSetReference::Name(name) => name,
210            PermissionSetReference::Inline(permission_set) => &permission_set.id,
211        }
212    }
213
214    /// Create a permission set reference from a name
215    pub fn from_name(name: impl Into<String>) -> Self {
216        PermissionSetReference::Name(name.into())
217    }
218
219    /// Create a permission set reference from an inline permission set
220    pub fn from_inline(permission_set: PermissionSet) -> Self {
221        PermissionSetReference::Inline(permission_set)
222    }
223
224    /// Resolve this reference to a concrete PermissionSet
225    /// Takes a resolver function for built-in permission sets
226    pub fn resolve(
227        &self,
228        resolver: impl Fn(&str) -> Option<PermissionSet>,
229    ) -> Option<PermissionSet> {
230        match self {
231            PermissionSetReference::Name(name) => resolver(name),
232            PermissionSetReference::Inline(permission_set) => Some(permission_set.clone()),
233        }
234    }
235}
236
237/// Permission profile that maps resources to permission sets
238/// Key can be "*" for all resources or resource name for specific resource
239#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
240#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
241#[serde(transparent)]
242pub struct PermissionProfile(pub IndexMap<String, Vec<PermissionSetReference>>);
243
244impl PermissionProfile {
245    /// Create a new permission profile
246    pub fn new() -> Self {
247        Self(IndexMap::new())
248    }
249
250    /// Add global permissions (applies to all resources)
251    pub fn global<I>(mut self, permission_sets: I) -> Self
252    where
253        I: IntoIterator,
254        I::Item: Into<PermissionSetReference>,
255    {
256        let permission_list: Vec<PermissionSetReference> =
257            permission_sets.into_iter().map(|s| s.into()).collect();
258        self.0.insert("*".to_string(), permission_list);
259        self
260    }
261
262    /// Add resource-scoped permissions
263    pub fn resource<I>(mut self, resource_name: impl Into<String>, permission_sets: I) -> Self
264    where
265        I: IntoIterator,
266        I::Item: Into<PermissionSetReference>,
267    {
268        let permission_list: Vec<PermissionSetReference> =
269            permission_sets.into_iter().map(|s| s.into()).collect();
270        self.0.insert(resource_name.into(), permission_list);
271        self
272    }
273}
274
275impl Default for PermissionProfile {
276    fn default() -> Self {
277        Self::new()
278    }
279}
280
281impl From<String> for PermissionSetReference {
282    fn from(name: String) -> Self {
283        PermissionSetReference::Name(name)
284    }
285}
286
287impl From<&str> for PermissionSetReference {
288    fn from(name: &str) -> Self {
289        PermissionSetReference::Name(name.to_string())
290    }
291}
292
293impl From<PermissionSet> for PermissionSetReference {
294    fn from(permission_set: PermissionSet) -> Self {
295        PermissionSetReference::Inline(permission_set)
296    }
297}
298
299/// Management permissions configuration for stack management access
300#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
301#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
302#[serde(rename_all = "camelCase", deny_unknown_fields)]
303pub enum ManagementPermissions {
304    /// Auto-derived permissions only (default)
305    /// Uses resource lifecycles to determine management permissions:
306    /// - Frozen resources: `<type>/management`
307    /// - Live resources: `<type>/provision`
308    Auto,
309
310    /// Add permissions to auto-derived baseline
311    Extend(PermissionProfile),
312
313    /// Replace auto-derived permissions entirely
314    Override(PermissionProfile),
315}
316
317impl Default for ManagementPermissions {
318    fn default() -> Self {
319        ManagementPermissions::Auto
320    }
321}
322
323/// Combined permissions configuration that contains both profiles and management
324#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
325#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
326#[serde(rename_all = "camelCase", deny_unknown_fields)]
327pub struct PermissionsConfig {
328    /// Permission profiles that define access control for compute services
329    /// Key is the profile name, value is the permission configuration
330    pub profiles: IndexMap<String, PermissionProfile>,
331    /// Management permissions configuration for stack management access
332    #[serde(default)]
333    pub management: ManagementPermissions,
334}
335
336impl PermissionsConfig {
337    /// Create a new permissions config with auto management
338    pub fn new() -> Self {
339        Self {
340            profiles: IndexMap::new(),
341            management: ManagementPermissions::Auto,
342        }
343    }
344
345    /// Add a permission profile
346    pub fn with_profile(mut self, name: impl Into<String>, profile: PermissionProfile) -> Self {
347        self.profiles.insert(name.into(), profile);
348        self
349    }
350
351    /// Set management permissions
352    pub fn with_management(mut self, management: ManagementPermissions) -> Self {
353        self.management = management;
354        self
355    }
356}
357
358impl Default for PermissionsConfig {
359    fn default() -> Self {
360        Self::new()
361    }
362}
363
364impl ManagementPermissions {
365    /// Create auto-derived management permissions
366    pub fn auto() -> Self {
367        ManagementPermissions::Auto
368    }
369
370    /// Create management permissions that extend auto-derived baseline
371    pub fn extend(profile: PermissionProfile) -> Self {
372        ManagementPermissions::Extend(profile)
373    }
374
375    /// Create management permissions that override auto-derived permissions
376    pub fn override_(profile: PermissionProfile) -> Self {
377        ManagementPermissions::Override(profile)
378    }
379
380    /// Get the permission profile if present (for Extend/Override variants)
381    pub fn profile(&self) -> Option<&PermissionProfile> {
382        match self {
383            ManagementPermissions::Auto => None,
384            ManagementPermissions::Extend(profile) => Some(profile),
385            ManagementPermissions::Override(profile) => Some(profile),
386        }
387    }
388
389    /// Check if this is the auto variant
390    pub fn is_auto(&self) -> bool {
391        matches!(self, ManagementPermissions::Auto)
392    }
393
394    /// Check if this extends auto-derived permissions
395    pub fn is_extend(&self) -> bool {
396        matches!(self, ManagementPermissions::Extend(_))
397    }
398
399    /// Check if this overrides auto-derived permissions
400    pub fn is_override(&self) -> bool {
401        matches!(self, ManagementPermissions::Override(_))
402    }
403}