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;
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;
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("MyPolicy", policy_doc).build();
242/// ```
243pub struct PolicyBuilder {
244    policy_name: String,
245    policy_document: PolicyDocument,
246}
247
248impl PolicyBuilder {
249    // TODO policy name characters consisting of upper and lowercase alphanumeric characters with no spaces + any of the following characters: _+=,.@-; 1 - 128 chars
250    //  (but most of the time used indirectly, so not very urgent)
251    pub fn new<T: Into<String>>(policy_name: T, policy_document: PolicyDocument) -> Self {
252        PolicyBuilder {
253            policy_name: policy_name.into(),
254            policy_document,
255        }
256    }
257
258    #[must_use]
259    pub fn build(self) -> Policy {
260        Policy {
261            policy_name: self.policy_name,
262            policy_document: self.policy_document,
263        }
264    }
265}
266
267/// Builder for IAM policy documents.
268///
269/// # Example
270///
271/// ```rust
272/// use rusty_cdk_core::iam::{PolicyDocumentBuilder, StatementBuilder, Effect};
273/// use rusty_cdk_core::wrappers::*;
274/// use rusty_cdk_macros::iam_action;
275///
276/// let statement = StatementBuilder::new(
277///     vec![iam_action!("dynamodb:GetItem"), iam_action!("dynamodb:Query")],
278///     Effect::Allow
279/// )
280/// .all_resources()
281/// .build();
282///
283/// let policy_doc = PolicyDocumentBuilder::new(vec![statement]).build();
284/// ```
285pub struct PolicyDocumentBuilder {
286    statements: Vec<Statement>
287}
288
289impl PolicyDocumentBuilder {
290    pub fn new(statements: Vec<Statement>) -> PolicyDocumentBuilder {
291        Self {
292            statements
293        }
294    }
295    
296    pub fn build(self) -> PolicyDocument {
297        PolicyDocument {
298            version: "2012-10-17".to_string(),
299            statements: self.statements,
300        }
301    }
302}
303
304pub struct AssumeRolePolicyDocumentBuilder {
305    statements: Vec<Statement>
306}
307
308impl AssumeRolePolicyDocumentBuilder {
309    pub fn new(statements: Vec<Statement>) -> Self {
310        Self {
311            statements,
312        }
313    }
314    
315    pub fn build(self) -> AssumeRolePolicyDocument {
316        AssumeRolePolicyDocument {
317            version: "2012-10-17".to_string(),
318            statements: self.statements,
319        }
320    }
321}
322
323#[derive(Debug, Clone)]
324pub enum Effect {
325    Allow,
326    Deny,
327}
328
329impl From<Effect> for String {
330    fn from(value: Effect) -> Self {
331        match value {
332            Effect::Allow => "Allow".to_string(),
333            Effect::Deny => "Deny".to_string(),
334        }
335    }
336}
337
338pub trait StatementState {}
339pub struct StatementStartState {}
340impl StatementState for StatementStartState {}
341
342/// Builder for IAM policy statements.
343///
344/// # Example
345///
346/// ```rust
347/// use rusty_cdk_core::iam::{StatementBuilder, Effect, PrincipalBuilder};
348/// use rusty_cdk_core::wrappers::*;
349/// use serde_json::Value;
350/// use rusty_cdk_macros::iam_action;
351///
352/// let statement = StatementBuilder::new(
353///     vec![iam_action!("s3:GetObject"), iam_action!("s3:PutObject")],
354///     Effect::Allow
355/// )
356/// .resources(vec![Value::String("arn:aws:s3:::my-bucket/*".to_string())])
357/// .principal(PrincipalBuilder::new().service("lambda.amazonaws.com").build())
358/// .build();
359/// ```
360pub struct StatementBuilder {
361    action: Vec<String>,
362    effect: Effect,
363    principal: Option<Principal>,
364    resource: Option<Vec<Value>>,
365    condition: Option<Value>
366}
367
368impl StatementBuilder {
369    pub(crate) fn internal_new(action: Vec<String>, effect: Effect) -> Self {
370        Self {
371            action,
372            effect,
373            principal: None,
374            resource: None,
375            condition: None,
376        }
377    }
378
379    pub fn new(actions: Vec<IamAction>, effect: Effect) -> Self {
380        Self::internal_new(actions.into_iter().map(|a| a.0).collect(), effect)
381    }
382
383    pub fn principal(self, principal: Principal) -> Self {
384        Self {
385            principal: Some(principal),
386            ..self
387        }
388    }
389    
390    pub fn condition(self, condition: Value) -> Self {
391        Self {
392            condition: Some(condition),
393            ..self
394        }
395    }
396
397    pub fn resources(self, resources: Vec<Value>) -> Self {
398        Self {
399            resource: Some(resources),
400            ..self
401        }
402    }
403
404    pub fn all_resources(self) -> Self {
405        Self {
406            resource: Some(vec![Value::String("*".to_string())]),
407            ..self
408        }
409    }
410
411    #[must_use]
412    pub fn build(self) -> Statement {
413        Statement {
414            action: self.action,
415            effect: self.effect.into(),
416            principal: self.principal,
417            resource: self.resource,
418            condition: self.condition,
419        }
420    }
421}
422
423pub struct CustomPermission {
424    id: String,
425    statement: Statement,
426}
427
428impl CustomPermission {
429    /// Creates a new custom IAM permission.
430    ///
431    /// # Arguments
432    /// * `id` - Unique identifier for the permission
433    /// * `statement` - IAM policy statement defining the permission
434    pub fn new(id: &str, statement: Statement) -> Self {
435        Self {
436            id: id.to_string(),
437            statement,
438        }
439    }
440}
441
442pub enum Permission<'a> {
443    AppConfigRead(&'a ApplicationRef, &'a EnvironmentRef, &'a ConfigurationProfileRef),
444    DynamoDBRead(&'a TableRef),
445    DynamoDBReadWrite(&'a TableRef),
446    SecretsManagerRead(&'a SecretRef),
447    SqsRead(&'a QueueRef),
448    S3ReadWrite(&'a BucketRef),
449    Custom(CustomPermission),
450}
451
452impl Permission<'_> {
453    pub(crate) fn into_policy(self) -> Policy {
454        match self {
455            Permission::DynamoDBRead(table) => {
456                let id = table.get_resource_id();
457                let statement = Statement {
458                    action: vec![
459                        "dynamodb:Get*".to_string(),
460                        "dynamodb:DescribeTable".to_string(),
461                        "dynamodb:BatchGetItem".to_string(),
462                        "dynamodb:ConditionCheckItem".to_string(),
463                        "dynamodb:Query".to_string(),
464                        "dynamodb:Scan".to_string(),
465                    ],
466                    effect: "Allow".to_string(),
467                    resource: Some(vec![get_arn(id)]),
468                    principal: None,
469                    condition: None,
470                };
471                let policy_document = PolicyDocumentBuilder::new(vec![statement]).build();
472                PolicyBuilder::new(format!("{}Read", id), policy_document).build()
473            }
474            Permission::DynamoDBReadWrite(table) => {
475                let id = table.get_resource_id();
476                let statement = Statement {
477                    action: vec![
478                        "dynamodb:Get*".to_string(),
479                        "dynamodb:DescribeTable".to_string(),
480                        "dynamodb:BatchGetItem".to_string(),
481                        "dynamodb:BatchWriteItem".to_string(),
482                        "dynamodb:ConditionCheckItem".to_string(),
483                        "dynamodb:Query".to_string(),
484                        "dynamodb:Scan".to_string(),
485                        "dynamodb:DeleteItem".to_string(),
486                        "dynamodb:PutItem".to_string(),
487                        "dynamodb:UpdateItem".to_string(),
488                    ],
489                    effect: "Allow".to_string(),
490                    resource: Some(vec![get_arn(id)]),
491                    principal: None,
492                    condition: None,
493                };
494                let policy_document = PolicyDocumentBuilder::new(vec![statement]).build();
495                PolicyBuilder::new(format!("{}ReadWrite", id), policy_document).build()
496            }
497            Permission::SqsRead(queue) => {
498                let id = queue.get_resource_id();
499                let sqs_permissions_statement = StatementBuilder::internal_new(
500                    vec![
501                        "sqs:ChangeMessageVisibility".to_string(),
502                        "sqs:DeleteMessage".to_string(),
503                        "sqs:GetQueueAttributes".to_string(),
504                        "sqs:GetQueueUrl".to_string(),
505                        "sqs:ReceiveMessage".to_string(),
506                    ],
507                    Effect::Allow,
508                )
509                .resources(vec![get_arn(id)])
510                .build();
511                let policy_document = PolicyDocumentBuilder::new(vec![sqs_permissions_statement]).build();
512                PolicyBuilder::new(format!("{}Read", id), policy_document).build()
513            }
514            Permission::S3ReadWrite(bucket) => {
515                let id = bucket.get_resource_id();
516                let arn = get_arn(id);
517                let s3_permissions_statement = StatementBuilder::internal_new(
518                    vec![
519                        "s3:Abort*".to_string(),
520                        "s3:DeleteObject*".to_string(),
521                        "s3:GetBucket*".to_string(),
522                        "s3:GetObject*".to_string(),
523                        "s3:List*".to_string(),
524                        "s3:PutObject".to_string(),
525                        "s3:PutObjectLegalHold".to_string(),
526                        "s3:PutObjectRetention".to_string(),
527                        "s3:PutObjectTagging".to_string(),
528                        "s3:PutObjectVersionTagging".to_string(),
529                    ],
530                    Effect::Allow,
531                )
532                .resources(vec![arn.clone(), join("/", vec![arn, Value::String("*".to_string())])])
533                .build();
534
535                let policy_document = PolicyDocumentBuilder::new(vec![s3_permissions_statement]).build();
536                PolicyBuilder::new(format!("{}ReadWrite", id), policy_document).build()
537            }
538            Permission::SecretsManagerRead(secret) => {
539                let id = secret.get_resource_id();
540                let statement = StatementBuilder::internal_new(vec!["secretsmanager:GetSecretValue".to_string()], Effect::Allow)
541                    .resources(vec![secret.get_ref()])
542                    .build();
543                let policy_document = PolicyDocumentBuilder::new(vec![statement]).build();
544                PolicyBuilder::new(format!("{}Read", id), policy_document).build()
545            }
546            Permission::AppConfigRead(app, env, profile) => {
547                let id = app.get_resource_id();
548                let resource = join(
549                    "",
550                    vec![
551                        Value::String("arn:aws:appconfig:*:".to_string()),
552                        get_ref(AWS_ACCOUNT_PSEUDO_PARAM),
553                        Value::String(":application/".to_string()),
554                        app.get_ref(),
555                        Value::String("/environment/".to_string()),
556                        env.get_ref(),
557                        Value::String("/configuration/".to_string()),
558                        profile.get_ref(),
559                    ],
560                );
561                
562                let statement = StatementBuilder::internal_new(vec![
563                    "appconfig:StartConfigurationSession".to_string(),
564                    "appconfig:GetLatestConfiguration".to_string(),
565                ], Effect::Allow)
566                    .resources(vec![resource])
567                    .build();
568                let policy_document = PolicyDocumentBuilder::new(vec![statement]).build();
569                PolicyBuilder::new(format!("{}Read", id), policy_document).build()
570            }
571            Permission::Custom(CustomPermission { id, statement }) => {
572                let policy_document = PolicyDocumentBuilder::new(vec![statement]).build();
573                PolicyBuilder::new(id, policy_document).build()
574            }
575        }
576    }
577}