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}