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
323pub enum Effect {
324    Allow,
325    Deny,
326}
327
328impl From<Effect> for String {
329    fn from(value: Effect) -> Self {
330        match value {
331            Effect::Allow => "Allow".to_string(),
332            Effect::Deny => "Deny".to_string(),
333        }
334    }
335}
336
337pub trait StatementState {}
338pub struct StatementStartState {}
339impl StatementState for StatementStartState {}
340
341/// Builder for IAM policy statements.
342///
343/// # Example
344///
345/// ```rust
346/// use rusty_cdk_core::iam::{StatementBuilder, Effect, PrincipalBuilder};
347/// use rusty_cdk_core::wrappers::*;
348/// use serde_json::Value;
349/// use rusty_cdk_macros::iam_action;
350///
351/// let statement = StatementBuilder::new(
352///     vec![iam_action!("s3:GetObject"), iam_action!("s3:PutObject")],
353///     Effect::Allow
354/// )
355/// .resources(vec![Value::String("arn:aws:s3:::my-bucket/*".to_string())])
356/// .principal(PrincipalBuilder::new().service("lambda.amazonaws.com").build())
357/// .build();
358/// ```
359pub struct StatementBuilder {
360    action: Vec<String>,
361    effect: Effect,
362    principal: Option<Principal>,
363    resource: Option<Vec<Value>>,
364    condition: Option<Value>
365}
366
367impl StatementBuilder {
368    pub(crate) fn internal_new(action: Vec<String>, effect: Effect) -> Self {
369        Self {
370            action,
371            effect,
372            principal: None,
373            resource: None,
374            condition: None,
375        }
376    }
377
378    pub fn new(actions: Vec<IamAction>, effect: Effect) -> Self {
379        Self::internal_new(actions.into_iter().map(|a| a.0).collect(), effect)
380    }
381
382    pub fn principal(self, principal: Principal) -> Self {
383        Self {
384            principal: Some(principal),
385            ..self
386        }
387    }
388    
389    pub fn condition(self, condition: Value) -> Self {
390        Self {
391            condition: Some(condition),
392            ..self
393        }
394    }
395
396    pub fn resources(self, resources: Vec<Value>) -> Self {
397        Self {
398            resource: Some(resources),
399            ..self
400        }
401    }
402
403    pub fn all_resources(self) -> Self {
404        Self {
405            resource: Some(vec![Value::String("*".to_string())]),
406            ..self
407        }
408    }
409
410    #[must_use]
411    pub fn build(self) -> Statement {
412        Statement {
413            action: self.action,
414            effect: self.effect.into(),
415            principal: self.principal,
416            resource: self.resource,
417            condition: self.condition,
418        }
419    }
420}
421
422pub struct CustomPermission {
423    id: String,
424    statement: Statement,
425}
426
427impl CustomPermission {
428    /// Creates a new custom IAM permission.
429    ///
430    /// # Arguments
431    /// * `id` - Unique identifier for the permission
432    /// * `statement` - IAM policy statement defining the permission
433    pub fn new(id: &str, statement: Statement) -> Self {
434        Self {
435            id: id.to_string(),
436            statement,
437        }
438    }
439}
440
441pub enum Permission<'a> {
442    AppConfigRead(&'a ApplicationRef, &'a EnvironmentRef, &'a ConfigurationProfileRef),
443    DynamoDBRead(&'a TableRef),
444    DynamoDBReadWrite(&'a TableRef),
445    SecretsManagerRead(&'a SecretRef),
446    SqsRead(&'a QueueRef),
447    S3ReadWrite(&'a BucketRef),
448    Custom(CustomPermission),
449}
450
451impl Permission<'_> {
452    pub(crate) fn into_policy(self) -> Policy {
453        match self {
454            Permission::DynamoDBRead(table) => {
455                let id = table.get_resource_id();
456                let statement = Statement {
457                    action: vec![
458                        "dynamodb:Get*".to_string(),
459                        "dynamodb:DescribeTable".to_string(),
460                        "dynamodb:BatchGetItem".to_string(),
461                        "dynamodb:ConditionCheckItem".to_string(),
462                        "dynamodb:Query".to_string(),
463                        "dynamodb:Scan".to_string(),
464                    ],
465                    effect: "Allow".to_string(),
466                    resource: Some(vec![get_arn(id)]),
467                    principal: None,
468                    condition: None,
469                };
470                let policy_document = PolicyDocumentBuilder::new(vec![statement]).build();
471                PolicyBuilder::new(format!("{}Read", id), policy_document).build()
472            }
473            Permission::DynamoDBReadWrite(table) => {
474                let id = table.get_resource_id();
475                let statement = Statement {
476                    action: vec![
477                        "dynamodb:Get*".to_string(),
478                        "dynamodb:DescribeTable".to_string(),
479                        "dynamodb:BatchGetItem".to_string(),
480                        "dynamodb:BatchWriteItem".to_string(),
481                        "dynamodb:ConditionCheckItem".to_string(),
482                        "dynamodb:Query".to_string(),
483                        "dynamodb:Scan".to_string(),
484                        "dynamodb:DeleteItem".to_string(),
485                        "dynamodb:PutItem".to_string(),
486                        "dynamodb:UpdateItem".to_string(),
487                    ],
488                    effect: "Allow".to_string(),
489                    resource: Some(vec![get_arn(id)]),
490                    principal: None,
491                    condition: None,
492                };
493                let policy_document = PolicyDocumentBuilder::new(vec![statement]).build();
494                PolicyBuilder::new(format!("{}ReadWrite", id), policy_document).build()
495            }
496            Permission::SqsRead(queue) => {
497                let id = queue.get_resource_id();
498                let sqs_permissions_statement = StatementBuilder::internal_new(
499                    vec![
500                        "sqs:ChangeMessageVisibility".to_string(),
501                        "sqs:DeleteMessage".to_string(),
502                        "sqs:GetQueueAttributes".to_string(),
503                        "sqs:GetQueueUrl".to_string(),
504                        "sqs:ReceiveMessage".to_string(),
505                    ],
506                    Effect::Allow,
507                )
508                .resources(vec![get_arn(id)])
509                .build();
510                let policy_document = PolicyDocumentBuilder::new(vec![sqs_permissions_statement]).build();
511                PolicyBuilder::new(format!("{}Read", id), policy_document).build()
512            }
513            Permission::S3ReadWrite(bucket) => {
514                let id = bucket.get_resource_id();
515                let arn = get_arn(id);
516                let s3_permissions_statement = StatementBuilder::internal_new(
517                    vec![
518                        "s3:Abort*".to_string(),
519                        "s3:DeleteObject*".to_string(),
520                        "s3:GetBucket*".to_string(),
521                        "s3:GetObject*".to_string(),
522                        "s3:List*".to_string(),
523                        "s3:PutObject".to_string(),
524                        "s3:PutObjectLegalHold".to_string(),
525                        "s3:PutObjectRetention".to_string(),
526                        "s3:PutObjectTagging".to_string(),
527                        "s3:PutObjectVersionTagging".to_string(),
528                    ],
529                    Effect::Allow,
530                )
531                .resources(vec![arn.clone(), join("/", vec![arn, Value::String("*".to_string())])])
532                .build();
533
534                let policy_document = PolicyDocumentBuilder::new(vec![s3_permissions_statement]).build();
535                PolicyBuilder::new(format!("{}ReadWrite", id), policy_document).build()
536            }
537            Permission::SecretsManagerRead(secret) => {
538                let id = secret.get_resource_id();
539                let statement = StatementBuilder::internal_new(vec!["secretsmanager:GetSecretValue".to_string()], Effect::Allow)
540                    .resources(vec![secret.get_ref()])
541                    .build();
542                let policy_document = PolicyDocumentBuilder::new(vec![statement]).build();
543                PolicyBuilder::new(format!("{}Read", id), policy_document).build()
544            }
545            Permission::AppConfigRead(app, env, profile) => {
546                let id = app.get_resource_id();
547                let resource = join(
548                    "",
549                    vec![
550                        Value::String("arn:aws:appconfig:*:".to_string()),
551                        get_ref(AWS_ACCOUNT_PSEUDO_PARAM),
552                        Value::String(":application/".to_string()),
553                        app.get_ref(),
554                        Value::String("/environment/".to_string()),
555                        env.get_ref(),
556                        Value::String("/configuration/".to_string()),
557                        profile.get_ref(),
558                    ],
559                );
560                
561                let statement = StatementBuilder::internal_new(vec![
562                    "appconfig:StartConfigurationSession".to_string(),
563                    "appconfig:GetLatestConfiguration".to_string(),
564                ], Effect::Allow)
565                    .resources(vec![resource])
566                    .build();
567                let policy_document = PolicyDocumentBuilder::new(vec![statement]).build();
568                PolicyBuilder::new(format!("{}Read", id), policy_document).build()
569            }
570            Permission::Custom(CustomPermission { id, statement }) => {
571                let policy_document = PolicyDocumentBuilder::new(vec![statement]).build();
572                PolicyBuilder::new(id, policy_document).build()
573            }
574        }
575    }
576}