1use {
2 crate::{
3 display_json, from_str_json, serutil::MapList, ActionList, AspenError, Condition, Context, Decision, Effect,
4 PolicyVersion, Principal, ResourceList,
5 },
6 derive_builder::Builder,
7 serde::{
8 de::{Deserializer, MapAccess, Visitor},
9 Deserialize, Serialize,
10 },
11 std::fmt::{Formatter, Result as FmtResult},
12};
13
14#[derive(Builder, Clone, Debug, Eq, PartialEq, Serialize)]
18#[builder(build_fn(validate = "Self::validate"))]
19#[serde(deny_unknown_fields, rename_all = "PascalCase")]
20pub struct Statement {
21 #[builder(setter(into, strip_option), default)]
23 #[serde(skip_serializing_if = "Option::is_none")]
24 sid: Option<String>,
25
26 effect: Effect,
28
29 #[builder(setter(into, strip_option), default)]
31 #[serde(skip_serializing_if = "Option::is_none")]
32 action: Option<ActionList>,
33
34 #[builder(setter(into, strip_option), default)]
36 #[serde(skip_serializing_if = "Option::is_none")]
37 not_action: Option<ActionList>,
38
39 #[builder(setter(into, strip_option), default)]
41 #[serde(skip_serializing_if = "Option::is_none")]
42 resource: Option<ResourceList>,
43
44 #[builder(setter(into, strip_option), default)]
46 #[serde(skip_serializing_if = "Option::is_none")]
47 not_resource: Option<ResourceList>,
48
49 #[builder(setter(into, strip_option), default)]
51 #[serde(skip_serializing_if = "Option::is_none")]
52 principal: Option<Principal>,
53
54 #[builder(setter(into, strip_option), default)]
56 #[serde(skip_serializing_if = "Option::is_none")]
57 not_principal: Option<Principal>,
58
59 #[builder(setter(into, strip_option), default)]
61 #[serde(skip_serializing_if = "Option::is_none")]
62 condition: Option<Condition>,
63}
64
65impl Statement {
66 pub fn builder() -> StatementBuilder {
68 StatementBuilder::default()
69 }
70
71 #[inline]
73 pub fn sid(&self) -> Option<&str> {
74 self.sid.as_deref()
75 }
76
77 #[inline]
79 pub fn effect(&self) -> &Effect {
80 &self.effect
81 }
82
83 #[inline]
85 pub fn action(&self) -> Option<&ActionList> {
86 self.action.as_ref()
87 }
88
89 #[inline]
91 pub fn not_action(&self) -> Option<&ActionList> {
92 self.not_action.as_ref()
93 }
94
95 #[inline]
97 pub fn resource(&self) -> Option<&ResourceList> {
98 self.resource.as_ref()
99 }
100
101 #[inline]
103 pub fn not_resource(&self) -> Option<&ResourceList> {
104 self.not_resource.as_ref()
105 }
106
107 #[inline]
109 pub fn principal(&self) -> Option<&Principal> {
110 self.principal.as_ref()
111 }
112
113 #[inline]
115 pub fn not_principal(&self) -> Option<&Principal> {
116 self.not_principal.as_ref()
117 }
118
119 #[inline]
121 pub fn condition(&self) -> Option<&Condition> {
122 self.condition.as_ref()
123 }
124
125 pub fn evaluate(&self, context: &Context, pv: PolicyVersion) -> Result<Decision, AspenError> {
152 if let Some(actions) = self.action() {
154 let mut matched = false;
155 for action in actions.iter() {
156 if action.matches(context.service(), context.api()) {
157 matched = true;
158 break;
159 }
160 }
161
162 if !matched {
163 return Ok(Decision::DefaultDeny);
164 }
165 } else if let Some(actions) = self.not_action() {
166 let mut matched = false;
167 for action in actions.iter() {
168 if action.matches(context.service(), context.api()) {
169 matched = true;
170 break;
171 }
172 }
173
174 if matched {
175 return Ok(Decision::DefaultDeny);
176 }
177 } else {
178 unreachable!("Statement must have either an Action or NotAction");
179 }
180
181 if let Some(resources) = self.resource() {
183 let candidates = context.resources();
184 if candidates.is_empty() {
185 if !resources.iter().any(|r| r.is_any()) {
187 return Ok(Decision::DefaultDeny);
188 }
189 } else {
190 for candidate in candidates {
191 let mut candidate_matched = false;
192
193 for resource in resources.iter() {
194 if resource.matches(context, pv, candidate)? {
195 candidate_matched = true;
196 break;
197 }
198 }
199
200 if !candidate_matched {
201 return Ok(Decision::DefaultDeny);
202 }
203 }
204 }
205 } else if let Some(resources) = self.not_resource() {
206 let candidates = context.resources();
207 log::trace!("NotResource: candidates = {:?}", candidates);
208 if candidates.is_empty() {
209 if resources.iter().any(|r| r.is_any()) {
211 return Ok(Decision::DefaultDeny);
212 }
213 } else {
214 for candidate in candidates {
215 log::trace!("NotResource: candidate = {:?}", candidate);
216 for resource in resources.iter() {
217 if resource.matches(context, pv, candidate)? {
218 log::trace!("NotResource: candidate {:?} matched resource {:?}", candidate, resource);
219 return Ok(Decision::DefaultDeny);
220 }
221 }
222 }
223
224 log::trace!("NotResource: no matches");
225 }
226 }
227 if let Some(principal) = self.principal() {
231 if !principal.matches(context.actor()) {
232 return Ok(Decision::DefaultDeny);
233 }
234 } else if let Some(principal) = self.not_principal() {
235 if principal.matches(context.actor()) {
236 return Ok(Decision::DefaultDeny);
237 }
238 }
239 if let Some(conditions) = self.condition() {
243 for (key, values) in conditions.iter() {
244 if !key.matches(values, context, pv)? {
245 return Ok(Decision::DefaultDeny);
246 }
247 }
248 }
249
250 match self.effect() {
252 Effect::Allow => Ok(Decision::Allow),
253 Effect::Deny => Ok(Decision::Deny),
254 }
255 }
256}
257
258display_json!(Statement);
259from_str_json!(Statement);
260
261impl<'de> Deserialize<'de> for Statement {
262 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
263 deserializer.deserialize_map(StatementVisitor {})
264 }
265}
266
267struct StatementVisitor;
268impl<'de> Visitor<'de> for StatementVisitor {
269 type Value = Statement;
270
271 fn expecting(&self, formatter: &mut Formatter) -> FmtResult {
272 formatter.write_str("a map of statement properties")
273 }
274
275 fn visit_map<A: MapAccess<'de>>(self, mut access: A) -> Result<Statement, A::Error> {
276 let mut builder = Statement::builder();
277 let mut sid_seen = false;
278 let mut effect_seen = false;
279 let mut action_seen = false;
280 let mut not_action_seen = false;
281 let mut resource_seen = false;
282 let mut not_resource_seen = false;
283 let mut principal_seen = false;
284 let mut not_principal_seen = false;
285 let mut condition_seen = false;
286
287 while let Some(key) = access.next_key::<&str>()? {
288 match key {
289 "Sid" => {
290 if sid_seen {
291 return Err(serde::de::Error::duplicate_field("Sid"));
292 }
293
294 sid_seen = true;
295 builder.sid(access.next_value::<&str>()?);
296 }
297 "Effect" => {
298 if effect_seen {
299 return Err(serde::de::Error::duplicate_field("Effect"));
300 }
301
302 effect_seen = true;
303 builder.effect(access.next_value::<Effect>()?);
304 }
305 "Action" => {
306 if action_seen {
307 return Err(serde::de::Error::duplicate_field("Action"));
308 }
309
310 action_seen = true;
311 builder.action(access.next_value::<ActionList>()?);
312 }
313 "NotAction" => {
314 if not_action_seen {
315 return Err(serde::de::Error::duplicate_field("NotAction"));
316 }
317
318 not_action_seen = true;
319 builder.not_action(access.next_value::<ActionList>()?);
320 }
321 "Resource" => {
322 if resource_seen {
323 return Err(serde::de::Error::duplicate_field("Resource"));
324 }
325
326 resource_seen = true;
327 builder.resource(access.next_value::<ResourceList>()?);
328 }
329 "NotResource" => {
330 if not_resource_seen {
331 return Err(serde::de::Error::duplicate_field("NotResource"));
332 }
333
334 not_resource_seen = true;
335 builder.not_resource(access.next_value::<ResourceList>()?);
336 }
337 "Principal" => {
338 if principal_seen {
339 return Err(serde::de::Error::duplicate_field("Principal"));
340 }
341
342 principal_seen = true;
343 builder.principal(access.next_value::<Principal>()?);
344 }
345 "NotPrincipal" => {
346 if not_principal_seen {
347 return Err(serde::de::Error::duplicate_field("NotPrincipal"));
348 }
349
350 not_principal_seen = true;
351 builder.not_principal(access.next_value::<Principal>()?);
352 }
353 "Condition" => {
354 if condition_seen {
355 return Err(serde::de::Error::duplicate_field("Condition"));
356 }
357
358 condition_seen = true;
359 builder.condition(access.next_value::<Condition>()?);
360 }
361 _ => {
362 return Err(serde::de::Error::unknown_field(
363 key,
364 &[
365 "Sid",
366 "Effect",
367 "Action",
368 "NotAction",
369 "Resource",
370 "NotResource",
371 "Principal",
372 "NotPrincipal",
373 "Condition",
374 ],
375 ));
376 }
377 }
378 }
379
380 builder.build().map_err(|e| match e {
381 StatementBuilderError::ValidationError(s) => {
382 let msg2 = s.replace('.', ";").trim_end_matches(|c| c == ';').to_string();
383 serde::de::Error::custom(StatementBuilderError::ValidationError(msg2))
384 }
385 _ => serde::de::Error::custom(e),
386 })
387 }
388}
389
390impl StatementBuilder {
391 fn validate(&self) -> Result<(), StatementBuilderError> {
392 let mut errors = Vec::with_capacity(5);
393 if self.effect.is_none() {
394 errors.push("Effect must be set.");
395 }
396
397 match (&self.action, &self.not_action) {
398 (Some(_), Some(_)) => errors.push("Action and NotAction cannot both be set."),
399 (None, None) => errors.push("Either Action or NotAction must be set."),
400 _ => (),
401 }
402
403 match (&self.resource, &self.not_resource) {
404 (Some(_), Some(_)) => errors.push("Resource and NotResource cannot both be set."),
405 (None, None) => errors.push("Either Resource or NotResource must be set."),
406 _ => (),
407 }
408
409 if let (Some(_), Some(_)) = (&self.principal, &self.not_principal) {
410 errors.push("Principal and NotPrincipal cannot both be set.");
411 }
412
413 if errors.is_empty() {
414 Ok(())
415 } else {
416 Err(StatementBuilderError::ValidationError(errors.join(" ")))
417 }
418 }
419}
420
421pub type StatementList = MapList<Statement>;
423
424#[cfg(test)]
425mod tests {
426 use {
427 crate::{
428 Action, AwsPrincipal, Context, Decision, Effect, Policy, PolicyVersion, Principal, Resource,
429 SpecifiedPrincipal, Statement,
430 },
431 indoc::indoc,
432 pretty_assertions::assert_eq,
433 scratchstack_aws_principal::{Principal as PrincipalActor, PrincipalIdentity, SessionData, User},
434 std::str::FromStr,
435 };
436
437 #[test_log::test]
438 fn test_blank_policy_import() {
439 let policy = Policy::from_str(indoc! { r#"
440 {
441 "Version": "2012-10-17",
442 "Statement": []
443 }"# })
444 .unwrap();
445 assert_eq!(policy.version(), PolicyVersion::V2012_10_17);
446 assert!(policy.id().is_none());
447
448 let policy_str = policy.to_string();
449 assert_eq!(
450 policy_str,
451 indoc! { r#"
452 {
453 "Version": "2012-10-17",
454 "Statement": []
455 }"#}
456 );
457 }
458
459 #[test_log::test]
460 fn test_builder() {
461 let err = Statement::builder().build().unwrap_err();
462 assert_eq!(
463 err.to_string(),
464 "Effect must be set. Either Action or NotAction must be set. Either Resource or NotResource must be set."
465 );
466
467 let err = Statement::builder().effect(Effect::Allow).build().unwrap_err();
468 assert_eq!(
469 err.to_string(),
470 "Either Action or NotAction must be set. Either Resource or NotResource must be set."
471 );
472
473 let err = Statement::builder()
474 .effect(Effect::Allow)
475 .action(Action::from_str("ec2:RunInstances").unwrap())
476 .build()
477 .unwrap_err();
478 assert_eq!(err.to_string(), "Either Resource or NotResource must be set.");
479
480 let err = Statement::builder()
481 .effect(Effect::Allow)
482 .action(Action::from_str("ec2:RunInstances").unwrap())
483 .resource(Resource::from_str("arn:aws:ec2:us-east-1:123456789012:instance/i-01234567890abcdef").unwrap())
484 .principal(
485 SpecifiedPrincipal::builder().aws(AwsPrincipal::from_str("123456789012").unwrap()).build().unwrap(),
486 )
487 .not_principal(
488 SpecifiedPrincipal::builder().aws(AwsPrincipal::from_str("123456789012").unwrap()).build().unwrap(),
489 )
490 .build()
491 .unwrap_err();
492 assert_eq!(err.to_string(), "Principal and NotPrincipal cannot both be set.");
493
494 let s = Statement::builder()
495 .sid("sid1")
496 .effect(Effect::Allow)
497 .action(Action::from_str("ec2:RunInstances").unwrap())
498 .resource(Resource::from_str("arn:aws:ec2:us-east-1:123456789012:instance/i-01234567890abcdef").unwrap())
499 .principal(
500 SpecifiedPrincipal::builder().aws(AwsPrincipal::from_str("123456789012").unwrap()).build().unwrap(),
501 )
502 .build()
503 .unwrap();
504
505 assert_eq!(s.sid(), Some("sid1"));
506 assert_eq!(s.effect(), &Effect::Allow);
507 assert_eq!(s.action().unwrap().len(), 1);
508 assert_eq!(s.action().unwrap()[0].to_string(), "ec2:RunInstances");
509
510 let s2 = s.clone();
511 assert_eq!(s, s2);
512
513 let s = Statement::builder()
514 .sid("sid1")
515 .effect(Effect::Allow)
516 .action(Action::from_str("ec2:RunInstances").unwrap())
517 .not_resource(vec![
518 Resource::from_str("arn:aws:ec2:us-east-1:123456789012:instance/i-01234567890abcdef").unwrap(),
519 Resource::from_str("arn:aws:ec2:us-west-1:123456789012:instance/i-01234567890abcdef").unwrap(),
520 ])
521 .not_principal(
522 SpecifiedPrincipal::builder().aws(AwsPrincipal::from_str("123456789012").unwrap()).build().unwrap(),
523 )
524 .build()
525 .unwrap();
526
527 assert_eq!(s.not_resource().unwrap().len(), 2);
528 assert_eq!(
529 s.not_resource().unwrap()[0].to_string(),
530 "arn:aws:ec2:us-east-1:123456789012:instance/i-01234567890abcdef"
531 );
532 let principal = s.not_principal().unwrap();
533 if let Principal::Specified(specified) = principal {
534 assert_eq!(specified.aws().unwrap().len(), 1);
535 assert_eq!(specified.aws().unwrap()[0].to_string(), "123456789012");
536 } else {
537 panic!("not_principal is not SpecifiedPrincipal");
538 }
539 }
540
541 #[test_log::test]
542 fn test_context_without_resources() {
543 let mut sb = Statement::builder();
544 sb.effect(Effect::Allow).action(Action::Any).resource(Resource::Any);
545
546 let s = sb.build().unwrap();
547 let actor = PrincipalActor::from(vec![PrincipalIdentity::from(
548 User::new("aws", "123456789012", "/", "MyUser").unwrap(),
549 )]);
550 let sd = SessionData::new();
551 let context =
552 Context::builder().api("DescribeInstances").actor(actor).service("ec2").session_data(sd).build().unwrap();
553
554 assert_eq!(s.evaluate(&context, PolicyVersion::None).unwrap(), Decision::Allow);
555
556 sb.resource(Resource::from_str("arn:aws:ec2:us-east-1:123456789012:instance/i-01234567890abcdef").unwrap());
557 let s = sb.build().unwrap();
558 assert_eq!(s.evaluate(&context, PolicyVersion::None).unwrap(), Decision::DefaultDeny);
559
560 let mut sb = Statement::builder();
561 sb.effect(Effect::Allow).action(Action::Any).not_resource(Resource::Any);
562 let s = sb.build().unwrap();
563 assert_eq!(s.evaluate(&context, PolicyVersion::None).unwrap(), Decision::DefaultDeny);
564 }
565
566 #[test_log::test]
567 fn test_bad_actions() {
568 let policy_str = indoc! { r#"
569 {
570 "Version": "2012-10-17",
571 "Statement": {
572 "Effect": "Allow",
573 "Action": ["ec2:"],
574 "Resource": "*",
575 "Principal": {
576 "AWS": ["arn:aws:"]
577 }
578 }
579 }"# };
580 let e = Policy::from_str(policy_str).unwrap_err();
581 assert_eq!(e.to_string(), r#"Invalid action: ec2: at line 5 column 26"#);
582 }
583
584 #[test_log::test]
585 fn test_bad_principals() {
586 let policy_str = indoc! { r#"
587 {
588 "Version": "2012-10-17",
589 "Statement": {
590 "Effect": "Allow",
591 "Action": "*",
592 "Resource": "*",
593 "Principal": {
594 "AWS": ["arn:aws:"]
595 }
596 }
597 }"# };
598 let e = Policy::from_str(policy_str).unwrap_err();
599 assert_eq!(e.to_string(), r#"Invalid principal: arn:aws: at line 8 column 31"#);
600 }
601
602 #[test_log::test]
603 fn test_bad_resources() {
604 let policy_str = indoc! { r#"
605 {
606 "Version": "2012-10-17",
607 "Statement": {
608 "Effect": "Allow",
609 "Action": "*",
610 "Resource": [2],
611 "Principal": "*"
612 }
613 }"# };
614 let e = Policy::from_str(policy_str).unwrap_err();
615 assert_eq!(
616 e.to_string(),
617 r#"invalid value: sequence, expected Resource or list of Resource at line 6 column 23"#
618 );
619
620 let policy_str = indoc! { r#"
621 {
622 "Version": "2012-10-17",
623 "Statement": {
624 "Effect": "Allow",
625 "Action": "*",
626 "Resource": ["foo-bar-baz"],
627 "Principal": "*"
628 }
629 }"# };
630 let e = Policy::from_str(policy_str).unwrap_err();
631 assert_eq!(e.to_string(), r#"Invalid resource: foo-bar-baz at line 6 column 35"#);
632 }
633}