rusty_cdk_core/iam/
builder.rs

1use crate::dynamodb::TableRef;
2use crate::iam::{AWSPrincipal, AssumeRolePolicyDocument, IamRoleProperties, Policy, PolicyDocument, Principal, Role, RoleRef, ServicePrincipal, Statement};
3use crate::intrinsic::{get_arn, get_ref, join, AWS_ACCOUNT_PSEUDO_PARAM};
4use crate::s3::BucketRef;
5use crate::shared::Id;
6use crate::sqs::QueueRef;
7use crate::stack::{Resource, StackBuilder};
8use crate::wrappers::{IamAction, PolicyName};
9use serde_json::Value;
10use std::marker::PhantomData;
11use std::vec;
12use crate::appconfig::{ApplicationRef, ConfigurationProfileRef, EnvironmentRef};
13use crate::secretsmanager::SecretRef;
14use crate::type_state;
15
16type_state!(
17    PrincipalState,
18    StartState,
19    ChosenState,
20);
21
22/// Builder for IAM principals (service, AWS, or custom).
23///
24/// # Example
25///
26/// ```rust
27/// use rusty_cdk_core::iam::PrincipalBuilder;
28///
29/// // Service principal
30/// let service_principal = PrincipalBuilder::new()
31///     .service("lambda.amazonaws.com")
32///     .build();
33///
34/// // Custom principal
35/// let custom_principal = PrincipalBuilder::new()
36///     .normal("*")
37///     .build();
38/// ```
39pub struct PrincipalBuilder<T: PrincipalState> {
40    phantom_data: PhantomData<T>,
41    service: Option<String>,
42    aws: Option<String>,
43    normal: Option<String>
44}
45
46impl Default for PrincipalBuilder<StartState> {
47    fn default() -> Self {
48        Self::new()
49    }
50}
51
52impl PrincipalBuilder<StartState> {
53    pub fn new() -> PrincipalBuilder<StartState> {
54        PrincipalBuilder {
55            phantom_data: Default::default(),
56            service: None,
57            aws: None,
58            normal: None,
59        }
60    }
61    
62    pub fn service<T: Into<String>>(self, service: T) -> PrincipalBuilder<ChosenState> {
63        PrincipalBuilder {
64            phantom_data: Default::default(),
65            service: Some(service.into()),
66            aws: self.aws,
67            normal: self.normal
68        }
69    }
70
71    pub fn aws<T: Into<String>>(self, aws: T) -> PrincipalBuilder<ChosenState> {
72        PrincipalBuilder {
73            phantom_data: Default::default(),
74            aws: Some(aws.into()),
75            service: self.service,
76            normal: self.normal
77        }
78    }
79
80    pub fn normal<T: Into<String>>(self, normal: T) -> PrincipalBuilder<ChosenState> {
81        PrincipalBuilder {
82            phantom_data: Default::default(),
83            normal: Some(normal.into()),
84            service: self.service,
85            aws: self.aws,
86        }
87    }
88}
89
90impl PrincipalBuilder<ChosenState> {
91    pub fn build(self) -> Principal {
92        if let Some(aws) = self.aws {
93            Principal::AWS(AWSPrincipal {
94                aws,
95            })
96        } else if let Some(service) = self.service {
97            Principal::Service(ServicePrincipal {
98                service,
99            })
100        } else if let Some(normal) = self.normal {
101            Principal::Custom(normal)
102        } else {
103            unreachable!("can only reach build state when one of the above is present")
104        }
105    }
106}
107
108/// Builder for IAM roles.
109///
110/// # Example
111///
112/// ```rust
113/// use rusty_cdk_core::stack::StackBuilder;
114/// use rusty_cdk_core::iam::{RoleBuilder, RolePropertiesBuilder, AssumeRolePolicyDocumentBuilder, StatementBuilder, PrincipalBuilder, Effect};
115/// use rusty_cdk_core::stack::Resource;
116/// use rusty_cdk_macros::iam_action;
117/// use rusty_cdk_core::wrappers::IamAction;
118///
119/// let mut stack_builder = StackBuilder::new();
120///
121/// let assume_role_statement = StatementBuilder::new(
122///     vec![iam_action!("sts:AssumeRole")],
123///     Effect::Allow
124/// )
125/// .principal(PrincipalBuilder::new().service("lambda.amazonaws.com").build())
126/// .build();
127///
128/// let assume_role_policy = AssumeRolePolicyDocumentBuilder::new(vec![assume_role_statement]).build();
129///
130/// let properties = RolePropertiesBuilder::new(assume_role_policy, vec![])
131///     .build();
132///
133/// let role = RoleBuilder::new("my-role", properties)
134///     .build(&mut stack_builder);
135/// ```
136pub struct RoleBuilder {
137    id: Id,
138    resource_id: Option<String>,
139    properties: IamRoleProperties,
140    potentially_missing: Vec<String>
141}
142
143impl RoleBuilder {
144    /// Creates a new IAM role builder.
145    ///
146    /// # Arguments
147    /// * `id` - Unique identifier for the role
148    /// * `properties` - IAM role properties including policies and trust relationships
149    pub fn new(id: &str, properties: IamRoleProperties) -> RoleBuilder {
150        RoleBuilder {
151            id: Id(id.to_string()),
152            resource_id: None,
153            properties,
154            potentially_missing: vec![],
155        }
156    }
157
158    pub(crate) fn new_with_info_on_missing(id: &str, resource_id: &str, properties: IamRoleProperties, potentially_missing: Vec<String>) -> RoleBuilder {
159        Self {
160            id: Id(id.to_string()),
161            resource_id: Some(resource_id.to_string()),
162            properties,
163            potentially_missing,
164        }
165    }
166    
167    pub fn build(self, stack_builder: &mut StackBuilder) -> RoleRef {
168        let resource_id = self.resource_id.unwrap_or_else(|| Resource::generate_id("Role"));
169        
170        stack_builder.add_resource(Role {
171            id: self.id,
172            resource_id: resource_id.clone(),
173            potentially_missing_services: self.potentially_missing,
174            r#type: "AWS::IAM::Role".to_string(),
175            properties: self.properties,
176        });
177        RoleRef::new(resource_id)
178    }
179}
180
181/// Builder for IAM role properties.
182pub struct RolePropertiesBuilder {
183    assumed_role_policy_document: AssumeRolePolicyDocument,
184    managed_policy_arns: Vec<Value>,
185    policies: Option<Vec<Policy>>,
186    role_name: Option<String>,
187}
188
189impl RolePropertiesBuilder {
190    pub fn new(assumed_role_policy_document: AssumeRolePolicyDocument, managed_policy_arns: Vec<Value>) -> RolePropertiesBuilder {
191        RolePropertiesBuilder {
192            assumed_role_policy_document,
193            managed_policy_arns,
194            policies: None,
195            role_name: None,
196        }
197    }
198
199    pub fn policies(self, policies: Vec<Policy>) -> RolePropertiesBuilder {
200        Self {
201            policies: Some(policies),
202            ..self
203        }
204    }
205
206    pub fn role_name<T: Into<String>>(self, role_name: T) -> RolePropertiesBuilder {
207        Self {
208            role_name: Some(role_name.into()),
209            ..self
210        }
211    }
212
213    #[must_use]
214    pub fn build(self) -> IamRoleProperties {
215        IamRoleProperties {
216            assumed_role_policy_document: self.assumed_role_policy_document,
217            managed_policy_arns: self.managed_policy_arns,
218            policies: self.policies,
219            role_name: self.role_name,
220        }
221    }
222}
223
224/// Builder for IAM policies.
225///
226/// # Example
227///
228/// ```rust
229/// use rusty_cdk_core::iam::{PolicyBuilder, PolicyDocumentBuilder, StatementBuilder, Effect};
230/// use rusty_cdk_core::wrappers::*;
231/// use rusty_cdk_macros::{iam_action, policy_name};
232///
233/// let statement = StatementBuilder::new(
234///     vec![iam_action!("s3:GetObject")],
235///     Effect::Allow
236/// )
237/// .all_resources()
238/// .build();
239///
240/// let policy_doc = PolicyDocumentBuilder::new(vec![statement]).build();
241/// let policy = PolicyBuilder::new(policy_name!("MyPolicy"), policy_doc).build();
242/// ```
243pub struct PolicyBuilder {
244    policy_name: String,
245    policy_document: PolicyDocument,
246}
247
248impl PolicyBuilder {
249    pub fn new(policy_name: PolicyName, policy_document: PolicyDocument) -> Self {
250        PolicyBuilder {
251            policy_name: policy_name.0,
252            policy_document,
253        }
254    }
255
256    #[must_use]
257    pub fn build(self) -> Policy {
258        Policy {
259            policy_name: self.policy_name,
260            policy_document: self.policy_document,
261        }
262    }
263}
264
265/// Builder for IAM policy documents.
266///
267/// # Example
268///
269/// ```rust
270/// use rusty_cdk_core::iam::{PolicyDocumentBuilder, StatementBuilder, Effect};
271/// use rusty_cdk_core::wrappers::*;
272/// use rusty_cdk_macros::iam_action;
273///
274/// let statement = StatementBuilder::new(
275///     vec![iam_action!("dynamodb:GetItem"), iam_action!("dynamodb:Query")],
276///     Effect::Allow
277/// )
278/// .all_resources()
279/// .build();
280///
281/// let policy_doc = PolicyDocumentBuilder::new(vec![statement]).build();
282/// ```
283pub struct PolicyDocumentBuilder {
284    statements: Vec<Statement>
285}
286
287impl PolicyDocumentBuilder {
288    pub fn new(statements: Vec<Statement>) -> PolicyDocumentBuilder {
289        Self {
290            statements
291        }
292    }
293    
294    pub fn build(self) -> PolicyDocument {
295        PolicyDocument {
296            version: "2012-10-17".to_string(),
297            statements: self.statements,
298        }
299    }
300}
301
302pub struct AssumeRolePolicyDocumentBuilder {
303    statements: Vec<Statement>
304}
305
306impl AssumeRolePolicyDocumentBuilder {
307    pub fn new(statements: Vec<Statement>) -> Self {
308        Self {
309            statements,
310        }
311    }
312    
313    pub fn build(self) -> AssumeRolePolicyDocument {
314        AssumeRolePolicyDocument {
315            version: "2012-10-17".to_string(),
316            statements: self.statements,
317        }
318    }
319}
320
321#[derive(Debug, Clone)]
322pub enum Effect {
323    Allow,
324    Deny,
325}
326
327impl From<Effect> for String {
328    fn from(value: Effect) -> Self {
329        match value {
330            Effect::Allow => "Allow".to_string(),
331            Effect::Deny => "Deny".to_string(),
332        }
333    }
334}
335
336pub trait StatementState {}
337pub struct StatementStartState {}
338impl StatementState for StatementStartState {}
339
340/// Builder for IAM policy statements.
341///
342/// # Example
343///
344/// ```rust
345/// use rusty_cdk_core::iam::{StatementBuilder, Effect, PrincipalBuilder};
346/// use rusty_cdk_core::wrappers::*;
347/// use serde_json::Value;
348/// use rusty_cdk_macros::iam_action;
349///
350/// let statement = StatementBuilder::new(
351///     vec![iam_action!("s3:GetObject"), iam_action!("s3:PutObject")],
352///     Effect::Allow
353/// )
354/// .resources(vec![Value::String("arn:aws:s3:::my-bucket/*".to_string())])
355/// .principal(PrincipalBuilder::new().service("lambda.amazonaws.com").build())
356/// .build();
357/// ```
358pub struct StatementBuilder {
359    action: Vec<String>,
360    effect: Effect,
361    principal: Option<Principal>,
362    resource: Option<Vec<Value>>,
363    condition: Option<Value>
364}
365
366impl StatementBuilder {
367    pub(crate) fn internal_new(action: Vec<String>, effect: Effect) -> Self {
368        Self {
369            action,
370            effect,
371            principal: None,
372            resource: None,
373            condition: None,
374        }
375    }
376
377    pub fn new(actions: Vec<IamAction>, effect: Effect) -> Self {
378        Self::internal_new(actions.into_iter().map(|a| a.0).collect(), effect)
379    }
380
381    pub fn principal(self, principal: Principal) -> Self {
382        Self {
383            principal: Some(principal),
384            ..self
385        }
386    }
387    
388    pub fn condition(self, condition: Value) -> Self {
389        Self {
390            condition: Some(condition),
391            ..self
392        }
393    }
394
395    pub fn resources(self, resources: Vec<Value>) -> Self {
396        Self {
397            resource: Some(resources),
398            ..self
399        }
400    }
401
402    pub fn all_resources(self) -> Self {
403        Self {
404            resource: Some(vec![Value::String("*".to_string())]),
405            ..self
406        }
407    }
408
409    #[must_use]
410    pub fn build(self) -> Statement {
411        Statement {
412            action: self.action,
413            effect: self.effect.into(),
414            principal: self.principal,
415            resource: self.resource,
416            condition: self.condition,
417        }
418    }
419}
420
421pub struct CustomPermission {
422    id: PolicyName,
423    statement: Statement,
424}
425
426impl CustomPermission {
427    /// Creates a new custom IAM permission.
428    ///
429    /// # Arguments
430    /// * `id` - Unique name for the permission
431    /// * `statement` - IAM policy statement defining the permission
432    pub fn new(id: PolicyName, statement: Statement) -> Self {
433        Self {
434            id,
435            statement,
436        }
437    }
438}
439
440pub enum Permission<'a> {
441    AppConfigRead(&'a ApplicationRef, &'a EnvironmentRef, &'a ConfigurationProfileRef),
442    DynamoDBRead(&'a TableRef),
443    DynamoDBReadWrite(&'a TableRef),
444    SecretsManagerRead(&'a SecretRef),
445    SqsRead(&'a QueueRef),
446    S3ReadWrite(&'a BucketRef),
447    Custom(CustomPermission),
448}
449
450impl Permission<'_> {
451    pub(crate) fn into_policy(self) -> Policy {
452        match self {
453            Permission::DynamoDBRead(table) => {
454                let id = table.get_resource_id();
455                let statement = Statement {
456                    action: vec![
457                        "dynamodb:Get*".to_string(),
458                        "dynamodb:DescribeTable".to_string(),
459                        "dynamodb:BatchGetItem".to_string(),
460                        "dynamodb:ConditionCheckItem".to_string(),
461                        "dynamodb:Query".to_string(),
462                        "dynamodb:Scan".to_string(),
463                    ],
464                    effect: "Allow".to_string(),
465                    resource: Some(vec![get_arn(id)]),
466                    principal: None,
467                    condition: None,
468                };
469                let policy_document = PolicyDocumentBuilder::new(vec![statement]).build();
470                PolicyBuilder::new(PolicyName(format!("{}Read", id)), policy_document).build()
471            }
472            Permission::DynamoDBReadWrite(table) => {
473                let id = table.get_resource_id();
474                let statement = Statement {
475                    action: vec![
476                        "dynamodb:Get*".to_string(),
477                        "dynamodb:DescribeTable".to_string(),
478                        "dynamodb:BatchGetItem".to_string(),
479                        "dynamodb:BatchWriteItem".to_string(),
480                        "dynamodb:ConditionCheckItem".to_string(),
481                        "dynamodb:Query".to_string(),
482                        "dynamodb:Scan".to_string(),
483                        "dynamodb:DeleteItem".to_string(),
484                        "dynamodb:PutItem".to_string(),
485                        "dynamodb:UpdateItem".to_string(),
486                    ],
487                    effect: "Allow".to_string(),
488                    resource: Some(vec![get_arn(id)]),
489                    principal: None,
490                    condition: None,
491                };
492                let policy_document = PolicyDocumentBuilder::new(vec![statement]).build();
493                PolicyBuilder::new(PolicyName(format!("{}ReadWrite", id)), policy_document).build()
494            }
495            Permission::SqsRead(queue) => {
496                let id = queue.get_resource_id();
497                let sqs_permissions_statement = StatementBuilder::internal_new(
498                    vec![
499                        "sqs:ChangeMessageVisibility".to_string(),
500                        "sqs:DeleteMessage".to_string(),
501                        "sqs:GetQueueAttributes".to_string(),
502                        "sqs:GetQueueUrl".to_string(),
503                        "sqs:ReceiveMessage".to_string(),
504                    ],
505                    Effect::Allow,
506                )
507                .resources(vec![get_arn(id)])
508                .build();
509                let policy_document = PolicyDocumentBuilder::new(vec![sqs_permissions_statement]).build();
510                PolicyBuilder::new(PolicyName(format!("{}Read", id)), policy_document).build()
511            }
512            Permission::S3ReadWrite(bucket) => {
513                let id = bucket.get_resource_id();
514                let arn = get_arn(id);
515                let s3_permissions_statement = StatementBuilder::internal_new(
516                    vec![
517                        "s3:Abort*".to_string(),
518                        "s3:DeleteObject*".to_string(),
519                        "s3:GetBucket*".to_string(),
520                        "s3:GetObject*".to_string(),
521                        "s3:List*".to_string(),
522                        "s3:PutObject".to_string(),
523                        "s3:PutObjectLegalHold".to_string(),
524                        "s3:PutObjectRetention".to_string(),
525                        "s3:PutObjectTagging".to_string(),
526                        "s3:PutObjectVersionTagging".to_string(),
527                    ],
528                    Effect::Allow,
529                )
530                .resources(vec![arn.clone(), join("/", vec![arn, Value::String("*".to_string())])])
531                .build();
532
533                let policy_document = PolicyDocumentBuilder::new(vec![s3_permissions_statement]).build();
534                PolicyBuilder::new(PolicyName(format!("{}ReadWrite", id)), policy_document).build()
535            }
536            Permission::SecretsManagerRead(secret) => {
537                let id = secret.get_resource_id();
538                let statement = StatementBuilder::internal_new(vec!["secretsmanager:GetSecretValue".to_string()], Effect::Allow)
539                    .resources(vec![secret.get_ref()])
540                    .build();
541                let policy_document = PolicyDocumentBuilder::new(vec![statement]).build();
542                PolicyBuilder::new(PolicyName(format!("{}Read", id)), policy_document).build()
543            }
544            Permission::AppConfigRead(app, env, profile) => {
545                let id = app.get_resource_id();
546                let resource = join(
547                    "",
548                    vec![
549                        Value::String("arn:aws:appconfig:*:".to_string()),
550                        get_ref(AWS_ACCOUNT_PSEUDO_PARAM),
551                        Value::String(":application/".to_string()),
552                        app.get_ref(),
553                        Value::String("/environment/".to_string()),
554                        env.get_ref(),
555                        Value::String("/configuration/".to_string()),
556                        profile.get_ref(),
557                    ],
558                );
559                
560                let statement = StatementBuilder::internal_new(vec![
561                    "appconfig:StartConfigurationSession".to_string(),
562                    "appconfig:GetLatestConfiguration".to_string(),
563                ], Effect::Allow)
564                    .resources(vec![resource])
565                    .build();
566                let policy_document = PolicyDocumentBuilder::new(vec![statement]).build();
567                PolicyBuilder::new(PolicyName(format!("{}Read", id)), policy_document).build()
568            }
569            Permission::Custom(CustomPermission { id, statement }) => {
570                let policy_document = PolicyDocumentBuilder::new(vec![statement]).build();
571                PolicyBuilder::new(id, policy_document).build()
572            }
573        }
574    }
575}