iam_rs/policy/
statement.rs

1use serde::{Deserialize, Serialize};
2
3use super::ConditionBlock;
4use crate::{
5    core::{Action, Effect, Operator, Principal, Resource},
6    validation::{Validate, ValidationContext, ValidationError, ValidationResult, helpers},
7};
8
9/// Represents a single statement in an IAM policy
10///
11/// https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_statement.html
12#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
13pub struct IAMStatement {
14    /// Optional statement ID
15    ///
16    /// You can provide a `Sid` (statement ID) as an optional identifier for the policy statement.
17    /// You can assign a `Sid` value to each statement in a statement array.
18    /// You can use the `Sid` value as a description for the policy statement.
19    ///
20    /// In services that let you specify an ID element, such as AWS SQS and AWS SNS, the `Sid` value is just a sub-ID of the policy document ID.
21    /// In IAM, the `Sid` value must be unique within a JSON policy.
22    ///
23    /// The Sid element supports ASCII uppercase letters (A-Z), lowercase letters (a-z), and numbers (0-9).
24    ///
25    /// https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_sid.html
26    #[serde(rename = "Sid", skip_serializing_if = "Option::is_none")]
27    pub sid: Option<String>,
28
29    /// The effect of the statement (Allow or Deny)
30    ///
31    /// The `Effect` element is required and specifies whether the statement results in an allow or an explicit deny.
32    /// Valid values for Effect are **Allow** and **Deny**.
33    /// The Effect value is case sensitive.
34    ///
35    /// By default, access to resources is denied.
36    /// To allow access to a resource, you must set the Effect element to Allow.
37    /// To override an allow (for example, to override an allow that is otherwise in force), you set the Effect element to Deny.
38    ///
39    /// https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_effect.html
40    #[serde(rename = "Effect")]
41    pub effect: Effect,
42
43    /// Optional principal(s) - who the statement applies to
44    ///
45    /// Use the `Principal` element in a resource-based JSON policy to specify the principal that is allowed or denied access to a resource.
46    ///
47    /// You must use the `Principal` element in resource-based policies.
48    /// You cannot use the `Principal` element in an identity-based policy.
49    ///
50    /// Identity-based policies are permissions policies that you attach to IAM identities (users, groups, or roles).
51    /// In those cases, the principal is implicitly the identity where the policy is attached.
52    ///
53    /// https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_principal.html
54    #[serde(rename = "Principal", skip_serializing_if = "Option::is_none")]
55    pub principal: Option<Principal>,
56
57    /// Optional not principal(s) - who the statement does not apply to
58    ///
59    /// The `NotPrincipal` element uses "Effect":"Deny" to deny access to all principals except the principal specified in the NotPrincipal element.
60    /// A principal can usually be a user, federated user, role, assumed role, account, service, or other principal type.
61    ///
62    /// `NotPrincipal` must be used with `"Effect":"Deny"`. Using it with `"Effect":"Allow"` is not supported.
63    ///
64    /// https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_notprincipal.html
65    #[serde(rename = "NotPrincipal", skip_serializing_if = "Option::is_none")]
66    pub not_principal: Option<Principal>,
67
68    /// Optional action(s) - what actions are allowed/denied
69    ///
70    /// The `Action` element describes the specific action or actions that will be allowed or denied.
71    /// Statements must include either an `Action` or `NotAction` element.
72    /// Each service has its own set of actions that describe tasks that you can perform with that service.
73    ///
74    /// For example:
75    /// * the list of actions for Amazon S3 can be found at Specifying Permissions in a Policy in the *Amazon Simple Storage Service User Guide*
76    /// * the list of actions for Amazon EC2 can be found in the [Amazon EC2 API Reference](https://docs.aws.amazon.com/AWSEC2/latest/APIReference/query-apis.html)
77    /// * the list of actions for AWS Identity and Access Management can be found in the [IAM API Reference](https://docs.aws.amazon.com/IAM/latest/APIReference/API_Operations.html)
78    ///
79    /// To find the list of actions for other AWS services, consult the [API reference](http://aws.amazon.com/documentation) documentation for the service.
80    /// For non-AWS services, consult the service documentation for the actions that are supported by that service.
81    ///
82    /// You specify a value using a service namespace as an action prefix (`iam`, `ec2`, `sqs`, `sns`, `s3`, etc.) followed by the name of the action to allow or deny.
83    /// The name must match an action that is supported by the service.
84    /// The prefix and the action name are case insensitive.
85    /// For example, `iam:ListAccessKeys` is the same as `IAM:listaccesskeys`.
86    ///
87    /// The following examples show Action elements for different services:
88    /// * `Action: "sqs:SendMessage"` - allows the `SendMessage` action on SQS.
89    /// * `Action: "ec2:StartInstances"` - allows the `StartInstances` action on EC2.
90    /// * `Action: "iam:ChangePassword"` - allows the `ChangePassword` action on IAM.
91    /// * `Action: "s3:GetObject"` - allows the `GetObject` action on S3.
92    ///
93    /// You can specify multiple values for the Action element:
94    /// * `Action: [ "sqs:SendMessage", "sqs:ReceiveMessage", "ec2:StartInstances", "iam:ChangePassword", "s3:GetObject" ]`
95    ///
96    /// You can use wildcards to match multiple actions:
97    /// * `Action: "s3:*"` - allows all actions on S3.
98    ///
99    /// You can also use wildcards (`*` or `?`) as part of the action name. For example, the following Action element applies to all IAM actions that include the string `AccessKey`, including `CreateAccessKey`, `DeleteAccessKey`, `ListAccessKeys`, and `UpdateAccessKey`:
100    ///
101    /// `"Action": "iam:*AccessKey*"`
102    ///
103    /// Some services let you limit the actions that are available.
104    /// For example, Amazon SQS lets you make available just a subset of all the possible Amazon SQS actions.
105    /// In that case, the `*` wildcard doesn't allow complete control of the queue; it allows only the subset of actions that you've shared.
106    ///
107    /// https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_action.html
108    #[serde(rename = "Action", skip_serializing_if = "Option::is_none")]
109    pub action: Option<Action>,
110
111    /// Optional not action(s) - what actions are not covered
112    ///
113    /// `NotAction` is an advanced policy element that explicitly matches everything except the specified list of actions.
114    /// Using `NotAction` can result in a shorter policy by listing only a few actions that should not match, rather than including a long list of actions that will match.
115    ///
116    /// Actions specified in `NotAction` are not impacted by the Allow or Deny effect in a policy statement.
117    /// This, in turn, means that all of the applicable actions or services that are not listed are allowed if you use the Allow effect.
118    /// In addition, such unlisted actions or services are denied if you use the Deny effect.
119    ///
120    /// When you use `NotAction` with the Resource element, you provide scope for the policy.
121    /// This is how AWS determines which actions or services are applicable.
122    ///
123    /// For more information, see the following example policy.
124    ///
125    /// # NotAction with Allow
126    ///
127    /// You can use the NotAction element in a statement with `"Effect": "Allow"` to provide access to all of the actions in an AWS service, except for the actions specified in NotAction.
128    /// You can use it with the Resource element to provide scope for the policy, limiting the allowed actions to the actions that can be performed on the specified resource.
129    ///
130    /// Example: Allow all S3 actions except deleting a bucket:
131    /// ```json
132    /// "Effect": "Allow",
133    /// "NotAction": "s3:DeleteBucket",
134    /// "Resource": "arn:aws:s3:::*"
135    /// ```
136    ///
137    /// Example: Allow all actions except IAM:
138    /// ```json
139    /// "Effect": "Allow",
140    /// "NotAction": "iam:*",
141    /// "Resource": "*"
142    /// ```
143    ///
144    /// Be careful using NotAction with `"Effect": "Allow"` as it could grant more permissions than intended.
145    ///
146    /// # NotAction with Deny
147    ///
148    /// You can use the NotAction element in a statement with `"Effect": "Deny"` to deny access to all of the listed resources except for the actions specified in NotAction.
149    /// This combination does not allow the listed items, but instead explicitly denies the actions not listed.
150    ///
151    /// Example: Deny all actions except IAM actions if not using MFA:
152    /// ```json
153    /// {
154    ///     "Sid": "DenyAllUsersNotUsingMFA",
155    ///     "Effect": "Deny",
156    ///     "NotAction": "iam:*",
157    ///     "Resource": "*",
158    ///     "Condition": {"BoolIfExists": {"aws:MultiFactorAuthPresent": "false"}}
159    /// }
160    /// ```
161    ///
162    /// https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_notaction.html
163    #[serde(rename = "NotAction", skip_serializing_if = "Option::is_none")]
164    pub not_action: Option<Action>,
165
166    /// Optional resource(s) - what resources the statement applies to
167    ///
168    /// The `Resource` element specifies the object(s) that the statement applies to.
169    ///
170    /// You must include either a `Resource` or a `NotResource` element in a statement.
171    ///
172    /// You specify a resource using an Amazon Resource Name (ARN). The ARN format depends on the AWS service and the specific resource.
173    /// For more information about ARNs, see: https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_identifiers.html#identifiers-arns
174    ///
175    /// Some AWS services do not support resource-level permissions. In those cases, use the wildcard character (`*`) in the Resource element.
176    ///
177    /// Examples:
178    /// * Specific SQS queue:
179    ///   `"Resource": "arn:aws:sqs:us-east-2:account-ID-without-hyphens:queue1"`
180    /// * Specific IAM user (user name is case sensitive):
181    ///   `"Resource": "arn:aws:iam::account-ID-without-hyphens:user/Bob"`
182    ///
183    /// # Using wildcards in resource ARNs
184    ///
185    /// You can use wildcard characters (`*` and `?`) within the individual segments of an ARN (the parts separated by colons) to represent:
186    /// - Any combination of characters (`*`)
187    /// - Any single character (`?`)
188    ///
189    /// You can use multiple `*` or `?` characters in each segment.
190    /// If the `*` wildcard is the last character of a resource ARN segment, it can expand to match beyond the colon boundaries.
191    /// It is recommended to use wildcards within ARN segments separated by a colon.
192    ///
193    /// **Note:** You can't use a wildcard in the service segment that identifies the AWS product.
194    ///
195    /// ## Examples
196    ///
197    /// All IAM users whose path is `/accounting`:
198    /// ```text
199    /// "Resource": "arn:aws:iam::account-ID-without-hyphens:user/accounting/*"
200    /// ```
201    ///
202    /// All items within a specific Amazon S3 bucket:
203    /// ```text
204    /// "Resource": "arn:aws:s3:::amzn-s3-demo-bucket/*"
205    /// ```
206    ///
207    /// Wildcards can match across slashes and other characters:
208    /// ```text
209    /// "Resource": "arn:aws:s3:::amzn-s3-demo-bucket/*/test/*"
210    /// ```
211    /// This matches:
212    /// - amzn-s3-demo-bucket/1/test/object.jpg
213    /// - amzn-s3-demo-bucket/1/2/test/object.jpg
214    /// - amzn-s3-demo-bucket/1/2/test/3/object.jpg
215    /// - amzn-s3-demo-bucket/1/2/3/test/4/object.jpg
216    /// - amzn-s3-demo-bucket/1///test///object.jpg
217    /// - amzn-s3-demo-bucket/1/test/.jpg
218    /// - amzn-s3-demo-bucket//test/object.jpg
219    /// - amzn-s3-demo-bucket/1/test/
220    ///
221    /// But does **not** match:
222    /// - amzn-s3-demo-bucket/1-test/object.jpg
223    /// - amzn-s3-demo-bucket/test/object.jpg
224    /// - amzn-s3-demo-bucket/1/2/test.jpg
225    ///
226    /// ## Specifying multiple resources
227    ///
228    /// You can specify multiple resources in the `Resource` element by using an array of ARNs:
229    /// ```json
230    /// "Resource": [
231    ///     "arn:aws:dynamodb:us-east-2:account-ID-without-hyphens:table/books_table",
232    ///     "arn:aws:dynamodb:us-east-2:account-ID-without-hyphens:table/magazines_table"
233    /// ]
234    /// ```
235    ///
236    /// ## Using policy variables in resource ARNs
237    ///
238    /// You can use JSON policy variables in the part of the ARN that identifies the specific resource. For example:
239    /// ```text
240    /// "Resource": "arn:aws:dynamodb:us-east-2:account-id:table/${aws:username}"
241    /// ```
242    /// This allows access to a DynamoDB table that matches the current user's name.
243    ///
244    /// For more information about JSON policy variables, see [IAM policy elements: Variables and tags](https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_variables.html).
245    ///
246    /// https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_resource.html
247    #[serde(rename = "Resource", skip_serializing_if = "Option::is_none")]
248    pub resource: Option<Resource>,
249
250    /// Optional not resource(s) - what resources are not covered
251    ///
252    /// NotResource is an advanced policy element that explicitly matches every resource except those specified.
253    /// Using NotResource can result in a shorter policy by listing only a few resources that should not match, rather than including a long list of resources that will match.
254    /// This is particularly useful for policies that apply within a single AWS service.
255    ///
256    /// https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_notresource.html
257    #[serde(rename = "NotResource", skip_serializing_if = "Option::is_none")]
258    pub not_resource: Option<Resource>,
259
260    /// Optional conditions for the statement
261    ///
262    /// The `Condition` element (or Condition block) lets you specify conditions for when a policy is in effect. The Condition element is optional.
263    ///
264    /// In the Condition element, you build expressions in which you use condition operators (equal, less than, and others) to match the context keys and values in the policy against keys and values in the request context.
265    /// To learn more about the request context, see [Components of a request](https://docs.aws.amazon.com/IAM/latest/UserGuide/intro-structure.html#intro-structure-request).
266    ///
267    /// ```json
268    /// "Condition" : { "{condition-operator}" : { "{condition-key}" : "{condition-value}" }}
269    /// ```
270    ///
271    /// The context key that you specify in a policy condition can be a global condition context key or a service-specific context key.
272    /// * Global condition context keys have the aws: prefix.
273    /// * Service-specific context keys have the service's prefix.
274    /// For example, Amazon EC2 lets you write a condition using the ec2:InstanceType context key, which is unique to that service.
275    ///
276    /// Context key names are not case-sensitive.
277    /// For example, including the aws:SourceIP context key is equivalent to testing for `AWS:SourceIp`.
278    /// Case-sensitivity of context key values depends on the condition operator that you use.
279    /// For example, the following condition includes the `StringEquals` operator to make sure that only requests made by john match.
280    /// Users named John are denied access.
281    ///
282    /// ```json
283    /// "Condition" : { "StringEquals" : { "aws:username" : "john" }}
284    /// ```
285    /// The following condition uses the `StringEqualsIgnoreCase` operator to match users named john or John.
286    /// ```json
287    /// "Condition" : { "StringEqualsIgnoreCase" : { "aws:username" : "john" }}
288    /// ```
289    ///
290    /// https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_condition.html
291    #[serde(rename = "Condition", skip_serializing_if = "Option::is_none")]
292    pub condition: Option<ConditionBlock>,
293}
294
295impl IAMStatement {
296    /// Creates a new IAM statement with the specified effect
297    pub fn new(effect: Effect) -> Self {
298        Self {
299            sid: None,
300            effect,
301            principal: None,
302            not_principal: None,
303            action: None,
304            not_action: None,
305            resource: None,
306            not_resource: None,
307            condition: None,
308        }
309    }
310
311    /// Sets the statement ID
312    pub fn with_sid<S: Into<String>>(mut self, sid: S) -> Self {
313        self.sid = Some(sid.into());
314        self
315    }
316
317    /// Sets the principal
318    pub fn with_principal(mut self, principal: Principal) -> Self {
319        self.principal = Some(principal);
320        self
321    }
322
323    /// Sets the action
324    pub fn with_action(mut self, action: Action) -> Self {
325        self.action = Some(action);
326        self
327    }
328
329    /// Sets the resource
330    pub fn with_resource(mut self, resource: Resource) -> Self {
331        self.resource = Some(resource);
332        self
333    }
334
335    /// Adds a condition to the statement
336    pub fn with_condition(
337        mut self,
338        operator: Operator,
339        key: String,
340        value: serde_json::Value,
341    ) -> Self {
342        let condition_block = self.condition.get_or_insert_with(ConditionBlock::new);
343        let condition = super::Condition::new(operator, key, value);
344        condition_block.add_condition(condition);
345        self
346    }
347
348    /// Adds a condition using the Condition struct
349    pub fn with_condition_struct(mut self, condition: super::Condition) -> Self {
350        let condition_block = self.condition.get_or_insert_with(ConditionBlock::new);
351        condition_block.add_condition(condition);
352        self
353    }
354}
355
356impl Validate for IAMStatement {
357    fn validate(&self, context: &mut ValidationContext) -> ValidationResult {
358        context.with_segment("Statement", |ctx| {
359            let mut results = Vec::new();
360
361            // Check that either Action or NotAction is present
362            match (&self.action, &self.not_action) {
363                (None, None) => {
364                    results.push(Err(ValidationError::MissingField {
365                        field: "Action or NotAction".to_string(),
366                        context: ctx.current_path(),
367                    }));
368                }
369                (Some(_), Some(_)) => {
370                    results.push(Err(ValidationError::LogicalError {
371                        message: "Statement cannot have both Action and NotAction".to_string(),
372                    }));
373                }
374                _ => {} // Valid: exactly one is present
375            }
376
377            // Check that either Resource or NotResource is present
378            match (&self.resource, &self.not_resource) {
379                (None, None) => {
380                    results.push(Err(ValidationError::MissingField {
381                        field: "Resource or NotResource".to_string(),
382                        context: ctx.current_path(),
383                    }));
384                }
385                (Some(_), Some(_)) => {
386                    results.push(Err(ValidationError::LogicalError {
387                        message: "Statement cannot have both Resource and NotResource".to_string(),
388                    }));
389                }
390                _ => {} // Valid: exactly one is present
391            }
392
393            // Check logical constraints on Principal/NotPrincipal
394            if let (Some(_), Some(_)) = (&self.principal, &self.not_principal) {
395                results.push(Err(ValidationError::LogicalError {
396                    message: "Statement cannot have both Principal and NotPrincipal".to_string(),
397                }));
398            }
399
400            // Validate NotPrincipal only used with Deny effect
401            if self.not_principal.is_some() && self.effect != Effect::Deny {
402                results.push(Err(ValidationError::LogicalError {
403                    message: "NotPrincipal must only be used with Effect: Deny".to_string(),
404                }));
405            }
406
407            // Validate individual components if present
408            if let Some(ref action) = self.action {
409                results.push(action.validate(ctx));
410            }
411            if let Some(ref not_action) = self.not_action {
412                results.push(not_action.validate(ctx));
413            }
414            if let Some(ref resource) = self.resource {
415                results.push(resource.validate(ctx));
416            }
417            if let Some(ref not_resource) = self.not_resource {
418                results.push(not_resource.validate(ctx));
419            }
420            if let Some(ref principal) = self.principal {
421                results.push(principal.validate(ctx));
422            }
423            if let Some(ref not_principal) = self.not_principal {
424                results.push(not_principal.validate(ctx));
425            }
426            if let Some(ref condition) = self.condition {
427                results.push(condition.validate(ctx));
428            }
429
430            // Validate Sid format if present
431            if let Some(ref sid) = self.sid {
432                if !sid.chars().all(|c| c.is_ascii_alphanumeric()) {
433                    results.push(Err(ValidationError::InvalidValue {
434                        field: "Sid".to_string(),
435                        value: sid.clone(),
436                        reason: "Sid must contain only ASCII alphanumeric characters".to_string(),
437                    }));
438                }
439            }
440
441            helpers::collect_errors(results)
442        })
443    }
444}
445
446#[cfg(test)]
447mod tests {
448    use super::*;
449
450    #[test]
451    fn test_statement_validation() {
452        // Valid statement
453        let valid_statement = IAMStatement::new(Effect::Allow)
454            .with_action(Action::Single("s3:GetObject".to_string()))
455            .with_resource(Resource::Single("arn:aws:s3:::bucket/*".to_string()));
456        assert!(valid_statement.is_valid());
457
458        // Missing action and resource
459        let invalid_statement = IAMStatement::new(Effect::Allow);
460        assert!(!invalid_statement.is_valid());
461
462        // Both Action and NotAction
463        let mut conflicting_statement = IAMStatement::new(Effect::Allow);
464        conflicting_statement.action = Some(Action::Single("s3:GetObject".to_string()));
465        conflicting_statement.not_action = Some(Action::Single("s3:PutObject".to_string()));
466        conflicting_statement.resource = Some(Resource::Single("*".to_string()));
467        assert!(!conflicting_statement.is_valid());
468
469        // Both Resource and NotResource
470        let mut conflicting_resource = IAMStatement::new(Effect::Allow);
471        conflicting_resource.action = Some(Action::Single("s3:GetObject".to_string()));
472        conflicting_resource.resource = Some(Resource::Single("*".to_string()));
473        conflicting_resource.not_resource =
474            Some(Resource::Single("arn:aws:s3:::bucket/*".to_string()));
475        assert!(!conflicting_resource.is_valid());
476    }
477
478    #[test]
479    fn test_statement_principal_validation() {
480        // NotPrincipal with Allow effect (invalid)
481        let mut invalid_not_principal = IAMStatement::new(Effect::Allow);
482        invalid_not_principal.action = Some(Action::Single("s3:GetObject".to_string()));
483        invalid_not_principal.resource = Some(Resource::Single("*".to_string()));
484        let mut principal_map = std::collections::HashMap::new();
485        principal_map.insert(
486            crate::core::PrincipalType::Aws,
487            serde_json::json!("arn:aws:iam::123456789012:user/test"),
488        );
489        invalid_not_principal.not_principal = Some(Principal::Mapped(principal_map));
490        assert!(!invalid_not_principal.is_valid());
491
492        // NotPrincipal with Deny effect (valid)
493        let mut valid_not_principal = IAMStatement::new(Effect::Deny);
494        valid_not_principal.action = Some(Action::Single("s3:GetObject".to_string()));
495        valid_not_principal.resource = Some(Resource::Single("*".to_string()));
496        let mut principal_map = std::collections::HashMap::new();
497        principal_map.insert(
498            crate::core::PrincipalType::Aws,
499            serde_json::json!("arn:aws:iam::123456789012:user/test"),
500        );
501        valid_not_principal.not_principal = Some(Principal::Mapped(principal_map));
502        assert!(valid_not_principal.is_valid());
503
504        // Both Principal and NotPrincipal (invalid)
505        let mut conflicting_principal = IAMStatement::new(Effect::Deny);
506        conflicting_principal.action = Some(Action::Single("s3:GetObject".to_string()));
507        conflicting_principal.resource = Some(Resource::Single("*".to_string()));
508        let mut principal_map1 = std::collections::HashMap::new();
509        principal_map1.insert(
510            crate::core::PrincipalType::Aws,
511            serde_json::json!("arn:aws:iam::123456789012:user/test"),
512        );
513        conflicting_principal.principal = Some(Principal::Mapped(principal_map1));
514        let mut principal_map2 = std::collections::HashMap::new();
515        principal_map2.insert(
516            crate::core::PrincipalType::Aws,
517            serde_json::json!("arn:aws:iam::123456789012:user/other"),
518        );
519        conflicting_principal.not_principal = Some(Principal::Mapped(principal_map2));
520        assert!(!conflicting_principal.is_valid());
521    }
522
523    #[test]
524    fn test_full_statement_with_complex_conditions() {
525        let statement = IAMStatement::new(Effect::Allow)
526            .with_sid("ComplexConditionExample")
527            .with_action(Action::Multiple(vec![
528                "s3:GetObject".to_string(),
529                "s3:PutObject".to_string(),
530            ]))
531            .with_resource(Resource::Single("arn:aws:s3:::my-bucket/*".to_string()))
532            .with_condition(
533                Operator::StringEquals,
534                "aws:PrincipalTag/department".to_string(),
535                serde_json::json!(["finance", "hr", "legal"]),
536            )
537            .with_condition(
538                Operator::ArnLike,
539                "aws:PrincipalArn".to_string(),
540                serde_json::json!([
541                    "arn:aws:iam::222222222222:user/Ana",
542                    "arn:aws:iam::222222222222:user/Mary"
543                ]),
544            );
545
546        // Verify the conditions are properly structured
547        assert!(statement.condition.is_some());
548        let condition_block = statement.condition.as_ref().unwrap();
549
550        assert!(
551            condition_block.has_condition(&Operator::StringEquals, "aws:PrincipalTag/department")
552        );
553        assert!(condition_block.has_condition(&Operator::ArnLike, "aws:PrincipalArn"));
554    }
555
556    #[test]
557    fn test_condition_handling() {
558        let statement = IAMStatement::new(Effect::Allow)
559            .with_action(Action::Single("s3:GetObject".to_string()))
560            .with_condition(
561                Operator::StringEquals,
562                "s3:prefix".to_string(),
563                serde_json::json!("uploads/"),
564            );
565
566        assert!(statement.condition.is_some());
567        let condition_block = statement.condition.unwrap();
568        assert!(condition_block.has_condition(&Operator::StringEquals, "s3:prefix"));
569    }
570
571    #[test]
572    fn test_statement_logical_validation() {
573        // Test NotPrincipal with Allow (should fail)
574        let mut invalid_not_principal = IAMStatement::new(Effect::Allow);
575        invalid_not_principal.action = Some(Action::Single("s3:GetObject".to_string()));
576        invalid_not_principal.resource = Some(Resource::Single("*".to_string()));
577        let mut principal_map = std::collections::HashMap::new();
578        principal_map.insert(
579            crate::core::PrincipalType::Aws,
580            serde_json::json!("arn:aws:iam::123456789012:user/test"),
581        );
582        invalid_not_principal.not_principal = Some(Principal::Mapped(principal_map));
583
584        assert!(!invalid_not_principal.is_valid());
585
586        // Test both Action and NotAction (should fail)
587        let mut conflicting_actions = IAMStatement::new(Effect::Allow);
588        conflicting_actions.action = Some(Action::Single("s3:GetObject".to_string()));
589        conflicting_actions.not_action = Some(Action::Single("s3:PutObject".to_string()));
590        conflicting_actions.resource = Some(Resource::Single("*".to_string()));
591
592        assert!(!conflicting_actions.is_valid());
593
594        // Test valid NotPrincipal with Deny
595        let mut valid_not_principal = IAMStatement::new(Effect::Deny);
596        valid_not_principal.action = Some(Action::Single("*".to_string()));
597        valid_not_principal.resource = Some(Resource::Single("*".to_string()));
598        let mut principal_map = std::collections::HashMap::new();
599        principal_map.insert(
600            crate::core::PrincipalType::Aws,
601            serde_json::json!("arn:aws:iam::123456789012:user/admin"),
602        );
603        valid_not_principal.not_principal = Some(Principal::Mapped(principal_map));
604
605        assert!(valid_not_principal.is_valid());
606    }
607
608    #[test]
609    fn test_statement_sid_validation() {
610        // Valid Sid
611        let valid_sid = IAMStatement::new(Effect::Allow)
612            .with_sid("ValidSid123")
613            .with_action(Action::Single("s3:GetObject".to_string()))
614            .with_resource(Resource::Single("*".to_string()));
615        assert!(valid_sid.is_valid());
616
617        // Invalid Sid with special characters
618        let mut invalid_sid = IAMStatement::new(Effect::Allow);
619        invalid_sid.sid = Some("Invalid-Sid!".to_string());
620        invalid_sid.action = Some(Action::Single("s3:GetObject".to_string()));
621        invalid_sid.resource = Some(Resource::Single("*".to_string()));
622        assert!(!invalid_sid.is_valid());
623    }
624}