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