scratchstack_aspen/
policy.rs

1use {
2    crate::{display_json, from_str_json, AspenError, Context, Decision, StatementList},
3    derive_builder::Builder,
4    serde::{
5        de,
6        de::{Deserializer, MapAccess, Visitor},
7        ser::{SerializeMap, Serializer},
8        Deserialize, Serialize,
9    },
10    std::{
11        fmt::{Display, Formatter, Result as FmtResult},
12        str::FromStr,
13    },
14};
15
16/// Aspen policy versions as represented in an Aspen policy document.
17#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
18pub enum PolicyVersion {
19    /// No policy version specified. Equivalent to [PolicyVersion::V2008_10_17], but is not serialized in the policy
20    /// document.
21    None,
22
23    /// Aspen policy version 2008-10-17. This is the default version. It does not support policy variables.
24    V2008_10_17,
25
26    /// Aspen policy version 2012-10-17. This version supports policy variables.
27    V2012_10_17,
28}
29
30impl PolicyVersion {
31    /// Indicates if no policy version was specified.
32    #[inline]
33    pub fn is_none(&self) -> bool {
34        matches!(self, Self::None)
35    }
36
37    /// Indicates if a policy version was specified.
38    #[inline]
39    pub fn is_some(&self) -> bool {
40        !self.is_none()
41    }
42}
43
44impl Default for PolicyVersion {
45    fn default() -> Self {
46        Self::None
47    }
48}
49
50impl Display for PolicyVersion {
51    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
52        match self {
53            Self::None => Ok(()),
54            Self::V2008_10_17 => f.write_str("2008-10-17"),
55            Self::V2012_10_17 => f.write_str("2012-10-17"),
56        }
57    }
58}
59
60impl<'de> Deserialize<'de> for PolicyVersion {
61    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
62        let value = String::deserialize(deserializer)?;
63        match PolicyVersion::from_str(&value) {
64            Ok(v) => Ok(v),
65            Err(e) => Err(serde::de::Error::custom(e)),
66        }
67    }
68}
69
70impl FromStr for PolicyVersion {
71    type Err = AspenError;
72
73    fn from_str(s: &str) -> Result<Self, Self::Err> {
74        match s {
75            "2008-10-17" => Ok(Self::V2008_10_17),
76            "2012-10-17" => Ok(Self::V2012_10_17),
77            _ => Err(AspenError::InvalidPolicyVersion(s.to_string())),
78        }
79    }
80}
81
82impl Serialize for PolicyVersion {
83    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
84        serializer.serialize_str(self.to_string().as_str())
85    }
86}
87
88/// The top-level structure for holding an Aspen policy.
89///
90/// This does not directly derive Deserialize/Serialize to prevent serde from allowing this to be represented as an
91/// array.
92///
93/// Policy structures are immutable after creation.
94#[derive(Builder, Clone, Debug, Eq, PartialEq)]
95pub struct Policy {
96    /// The version of the policy. Currently allowed values are `2008-10-17` and `2012-10-17`. Features such as
97    /// policy variables are only available with version `2012-10-17` (or later, should a newer version be published).
98    /// If omitted, this is equivalent to `2008-10-17`.
99    #[builder(setter(into, strip_option), default)]
100    version: PolicyVersion,
101
102    /// An optional identifier for the policy. Some services may require this element and have uniqueness requirements.
103    #[builder(setter(into, strip_option), default)]
104    id: Option<String>,
105
106    /// One or more statements describing the policy. Aspen allows single statements to be encoded directly as a map
107    /// instead of being enclosed in a list.
108    #[builder(setter(into))]
109    statement: StatementList,
110}
111
112impl Policy {
113    #[inline]
114    /// Returns a builder for a [Policy].
115    pub fn builder() -> PolicyBuilder {
116        PolicyBuilder::default()
117    }
118
119    /// Returns the policy version.
120    pub fn version(&self) -> PolicyVersion {
121        self.version
122    }
123
124    /// Returns the user-specified identifier for the policy, or None if no identifier was specified.
125    #[inline]
126    pub fn id(&self) -> Option<&str> {
127        self.id.as_deref()
128    }
129
130    /// Returns the policy statements associated with the policy.
131    #[inline]
132    pub fn statement(&self) -> &StatementList {
133        &self.statement
134    }
135
136    /// Evaluates the policy against the request [Context].
137    ///
138    /// Returns [Decision::Deny] if the policy denies the request, [Decision::Allow] if the policy allows the request,
139    /// or [Decision::DefaultDeny] if the policy does not explicitly allow or deny the request.
140    ///
141    /// # Example
142    /// ```
143    /// # use scratchstack_aspen::{Action, Context, Decision, Effect, Policy, Resource, Statement, StatementList};
144    /// # use scratchstack_aws_principal::{Principal, SessionData, User};
145    /// # use std::str::FromStr;
146    /// let action = Action::from_str("s3:ListBucket").unwrap();
147    /// let resource = Resource::from_str("arn:aws:s3:::examplebucket").unwrap();
148    /// let statement = Statement::builder().effect(Effect::Allow).action(action).resource(resource).build().unwrap();
149    /// let policy = Policy::builder().statement(statement).build().unwrap();
150    ///
151    /// let actor = Principal::from(vec![User::from_str("arn:aws:iam::123456789012:user/exampleuser").unwrap().into()]);
152    /// let context = Context::builder().service("s3").api("ListBucket").actor(actor)
153    ///     .session_data(SessionData::new()).build().unwrap();
154    /// policy.evaluate(&context);
155    /// ```
156    pub fn evaluate(&self, context: &Context) -> Result<Decision, crate::AspenError> {
157        for statement in self.statement.iter() {
158            match statement.evaluate(context, self.version()) {
159                Ok(Decision::Allow) => return Ok(Decision::Allow),
160                Ok(Decision::Deny) => return Ok(Decision::Deny),
161                Ok(Decision::DefaultDeny) => (),
162                Err(err) => return Err(err),
163            }
164        }
165        Ok(Decision::DefaultDeny)
166    }
167}
168
169display_json!(Policy);
170from_str_json!(Policy);
171
172impl<'de> Visitor<'de> for PolicyBuilder {
173    type Value = Policy;
174
175    fn expecting(&self, formatter: &mut Formatter<'_>) -> FmtResult {
176        formatter.write_str("policy")
177    }
178
179    fn visit_map<A: MapAccess<'de>>(mut self, mut access: A) -> Result<Self::Value, A::Error> {
180        let builder = &mut self;
181        let mut version_seen = false;
182        let mut id_seen = false;
183        let mut statement_seen = false;
184
185        while let Some(key) = access.next_key()? {
186            match key {
187                "Version" => {
188                    if version_seen {
189                        return Err(de::Error::duplicate_field("Version"));
190                    }
191                    version_seen = true;
192                    builder.version(access.next_value::<PolicyVersion>()?);
193                }
194                "Id" => {
195                    if id_seen {
196                        return Err(de::Error::duplicate_field("Id"));
197                    }
198                    id_seen = true;
199                    builder.id(access.next_value::<String>()?);
200                }
201                "Statement" => {
202                    if statement_seen {
203                        return Err(de::Error::duplicate_field("Statement"));
204                    }
205                    statement_seen = true;
206                    builder.statement(access.next_value::<StatementList>()?);
207                }
208                _ => return Err(de::Error::unknown_field(key, &["Version", "Id", "Statement"])),
209            }
210        }
211
212        if !statement_seen {
213            return Err(de::Error::missing_field("Statement"));
214        }
215
216        self.build().map_err(de::Error::custom)
217    }
218}
219
220impl<'de> Deserialize<'de> for Policy {
221    fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Policy, D::Error> {
222        d.deserialize_map(PolicyBuilder::default())
223    }
224}
225
226impl Serialize for Policy {
227    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
228        let mut state = serializer.serialize_map(None)?;
229        if self.version.is_some() {
230            state.serialize_entry("Version", &self.version)?;
231        }
232        if let Some(id) = &self.id {
233            state.serialize_entry("Id", id)?;
234        }
235        state.serialize_entry("Statement", &self.statement)?;
236        state.end()
237    }
238}
239
240#[cfg(test)]
241mod tests {
242    use {
243        crate::{
244            serutil::JsonRep, Action, AspenError, AwsPrincipal, Context, Decision, Effect, Policy, PolicyBuilderError,
245            PolicyVersion, Principal, Resource, SpecifiedPrincipal, Statement,
246        },
247        indoc::indoc,
248        pretty_assertions::{assert_eq, assert_ne},
249        scratchstack_arn::Arn,
250        scratchstack_aws_principal::{Principal as PrincipalActor, Service, SessionData, SessionValue, User},
251        std::{
252            collections::hash_map::DefaultHasher,
253            hash::{Hash, Hasher},
254            str::FromStr,
255        },
256    };
257
258    #[test_log::test]
259    fn test_typical_policy_import() {
260        let policy_str = indoc! { r#"
261        {
262            "Version": "2012-10-17",
263            "Id": "PolicyId",
264            "Statement": [
265                {
266                    "Sid": "1",
267                    "Effect": "Allow",
268                    "Action": [
269                        "ec2:Get*",
270                        "ecs:*"
271                    ],
272                    "Resource": "*",
273                    "Principal": {
274                        "AWS": "123456789012"
275                    },
276                    "Condition": {
277                        "StringEquals": {
278                            "ec2:Region": [
279                                "us-west-2",
280                                "us-west-1",
281                                "us-east-2",
282                                "us-east-1"
283                            ]
284                        }
285                    }
286                },
287                {
288                    "Sid": "2",
289                    "Effect": "Deny",
290                    "Action": "*",
291                    "Resource": [
292                        "arn:aws:s3:::my-bucket",
293                        "arn:aws:s3:::my-bucket/*"
294                    ],
295                    "Principal": "*"
296                }
297            ]
298        }"# };
299        let policy = Policy::from_str(policy_str).unwrap();
300
301        assert_eq!(policy.version(), PolicyVersion::V2012_10_17);
302        assert_eq!(policy.id(), Some("PolicyId"));
303
304        assert_eq!(policy.statement().len(), 2);
305        let s = &policy.statement()[0];
306        assert_eq!(*s.effect(), Effect::Allow);
307        match &s.action() {
308            None => panic!("Expected a list of actions"),
309            Some(a_list) => {
310                assert_eq!(a_list.kind(), JsonRep::List);
311                assert_eq!(a_list[0].specific(), Some(("ec2", "Get*")));
312                assert_eq!(a_list[1].specific(), Some(("ecs", "*")));
313            }
314        }
315        assert!(s.condition().is_some());
316        let c = s.condition().unwrap();
317        let se = c.get("StringEquals");
318        assert!(se.is_some());
319
320        let new_policy_str = policy.to_string();
321        assert_eq!(new_policy_str, policy_str);
322    }
323
324    #[test_log::test]
325    fn test_bad_condition_variable() {
326        let policy_str = indoc! { r#"
327        {
328            "Version": "2012-10-17",
329            "Statement": {
330                "Effect": "Allow",
331                "Action": "*",
332                "Resource": "*",
333                "Condition": {
334                    "StringEquals": {
335                        "aws:username": "${"
336                    }
337                }
338            }
339        }"# };
340
341        let policy = Policy::from_str(policy_str).unwrap();
342        let actor = PrincipalActor::from(User::new("aws", "123456789012", "/", "MyUser").unwrap());
343        let mut sd = SessionData::new();
344        sd.insert("aws:username", SessionValue::from("MyUser"));
345        let context = Context::builder()
346            .api("DescribeSecurityGroups")
347            .actor(actor)
348            .session_data(sd)
349            .service("ec2")
350            .build()
351            .unwrap();
352
353        assert_eq!(policy.evaluate(&context).unwrap_err().to_string(), "Invalid variable substitution: ${");
354    }
355
356    #[test_log::test]
357    fn test_bad_field_types() {
358        let policy_str = indoc! { r#"
359        {
360            "Version": "2012-10-17",
361            "Id": "PolicyId",
362            "Statement": "Deny"
363            }
364        }"# };
365        let e = Policy::from_str(policy_str).unwrap_err();
366        assert_eq!(
367            e.to_string(),
368            r#"invalid type: string "Deny", expected Statement or list of Statement at line 4 column 23"#
369        );
370
371        let policy_str = indoc! { r#"
372        {
373            "Version": "2012-10-17",
374            "Id": "PolicyId",
375            "Statement": {
376                3: "Deny"
377            }
378        }"# };
379        let e = Policy::from_str(policy_str).unwrap_err();
380        assert_eq!(e.to_string(), r#"key must be a string at line 5 column 9"#);
381
382        let policy_str = indoc! { r#"
383        {
384            "Version": "2012-10-17",
385            "Id": "PolicyId",
386            "Statement": {
387                "Sid": 1,
388                "Effect": "Allow",
389                "Action": [
390                    "ec2:Get*",
391                    "ecs:*"
392                ],
393                "Resource": "*",
394                "Principal": {
395                    "AWS": "123456789012"
396                },
397                "Condition": {
398                    "StringEquals": {
399                        "ec2:Region": [
400                            "us-west-2"
401                        ]
402                    }
403                }
404            }
405        }"# };
406        let e = Policy::from_str(policy_str).unwrap_err();
407        assert_eq!(e.to_string(), "invalid type: integer `1`, expected a borrowed string at line 5 column 16");
408
409        let policy_str = indoc! { r#"
410        {
411            "Version": "2012-10-17",
412            "Id": "PolicyId",
413            "Statement": {
414                "Sid": "1",
415                "Effect": ["Allow"],
416                "Action": [
417                    "ec2:Get*",
418                    "ecs:*"
419                ],
420                "Resource": "*",
421                "Principal": {
422                    "AWS": "123456789012"
423                },
424                "Condition": {
425                    "StringEquals": {
426                        "ec2:Region": [
427                            "us-west-2"
428                        ]
429                    }
430                }
431            }
432        }"# };
433        let e = Policy::from_str(policy_str).unwrap_err();
434        assert_eq!(e.to_string(), "expected value at line 6 column 19");
435
436        let policy_str = indoc! { r#"
437        {
438            "Version": "2012-10-17",
439            "Id": "PolicyId",
440            "Statement": {
441                "Sid": "1",
442                "Effect": "Allow",
443                "Action": {
444                    "ec2": "RunInstances"
445                },
446                "Resource": "*",
447                "Principal": {
448                    "AWS": "123456789012"
449                },
450                "Condition": {
451                    "StringEquals": {
452                        "ec2:Region": [
453                            "us-west-2"
454                        ]
455                    }
456                }
457            }
458        }"# };
459        let e = Policy::from_str(policy_str).unwrap_err();
460        assert_eq!(e.to_string(), "invalid type: map, expected Action or list of Action at line 8 column 12");
461
462        let policy_str = indoc! { r#"
463        {
464            "Version": "2012-10-17",
465            "Id": "PolicyId",
466            "Statement": {
467                "Sid": "1",
468                "Effect": "Allow",
469                "NotAction": {
470                    "ec2": "RunInstances"
471                },
472                "Resource": "*",
473                "Principal": {
474                    "AWS": "123456789012"
475                },
476                "Condition": {
477                    "StringEquals": {
478                        "ec2:Region": [
479                            "us-west-2"
480                        ]
481                    }
482                }
483            }
484        }"# };
485        let e = Policy::from_str(policy_str).unwrap_err();
486        assert_eq!(e.to_string(), "invalid type: map, expected Action or list of Action at line 8 column 12");
487
488        let policy_str = indoc! { r#"
489        {
490            "Version": "2012-10-17",
491            "Id": "PolicyId",
492            "Statement": {
493                "Sid": "1",
494                "Effect": "Allow",
495                "Action": "ec2:RunInstances",
496                "Resource": {"ec2": "Instance"},
497                "Principal": {
498                    "AWS": "123456789012"
499                },
500                "Condition": {
501                    "StringEquals": {
502                        "ec2:Region": [
503                            "us-west-2"
504                        ]
505                    }
506                }
507            }
508        }"# };
509        let e = Policy::from_str(policy_str).unwrap_err();
510        assert_eq!(e.to_string(), "invalid type: map, expected Resource or list of Resource at line 8 column 21");
511
512        let policy_str = indoc! { r#"
513        {
514            "Version": "2012-10-17",
515            "Id": "PolicyId",
516            "Statement": {
517                "Sid": "1",
518                "Effect": "Allow",
519                "Action": "ec2:RunInstances",
520                "NotResource": {"ec2": "Instance"},
521                "Principal": {
522                    "AWS": "123456789012"
523                },
524                "Condition": {
525                    "StringEquals": {
526                        "ec2:Region": [
527                            "us-west-2"
528                        ]
529                    }
530                }
531            }
532        }"# };
533        let e = Policy::from_str(policy_str).unwrap_err();
534        assert_eq!(e.to_string(), "invalid type: map, expected Resource or list of Resource at line 8 column 24");
535
536        let policy_str = indoc! { r#"
537        {
538            "Version": "2012-10-17",
539            "Id": "PolicyId",
540            "Statement": {
541                "Sid": "1",
542                "Effect": "Allow",
543                "Action": "ec2:RunInstances",
544                "Resource": "*",
545                "Principal": "123456789012",
546                "Condition": {
547                    "StringEquals": {
548                        "ec2:Region": [
549                            "us-west-2"
550                        ]
551                    }
552                }
553            }
554        }"# };
555        let e = Policy::from_str(policy_str).unwrap_err();
556        assert_eq!(
557            e.to_string(),
558            r#"invalid value: string "123456789012", expected map of principal types to values or "*" at line 9 column 35"#
559        );
560
561        let policy_str = indoc! { r#"
562        {
563            "Version": "2012-10-17",
564            "Id": "PolicyId",
565            "Statement": {
566                "Sid": "1",
567                "Effect": "Allow",
568                "Action": "ec2:RunInstances",
569                "Resource": "*",
570                "NotPrincipal": "123456789012",
571                "Condition": {
572                    "StringEquals": {
573                        "ec2:Region": [
574                            "us-west-2"
575                        ]
576                    }
577                }
578            }
579        }"# };
580        let e = Policy::from_str(policy_str).unwrap_err();
581        assert_eq!(
582            e.to_string(),
583            r#"invalid value: string "123456789012", expected map of principal types to values or "*" at line 9 column 38"#
584        );
585
586        let policy_str = indoc! { r#"
587        {
588            "Version": "2012-10-17",
589            "Id": "PolicyId",
590            "Statement": {
591                "Sid": "1",
592                "Effect": "Allow",
593                "Action": "ec2:RunInstances",
594                "Resource": "*",
595                "Principal": {"AWS": "123456789012"},
596                "Condition": {
597                    "Foo": {
598                        "ec2:Region": [
599                            "us-west-2"
600                        ]
601                    }
602                }
603            }
604        }"# };
605        let e = Policy::from_str(policy_str).unwrap_err();
606        assert_eq!(e.to_string(), r#"Invalid condition operator: Foo at line 11 column 17"#);
607
608        let policy_str = indoc! { r#"
609        {
610            "Version": "2012-10-17",
611            "Id": "PolicyId",
612            "Statement": {
613                "Sid": "1",
614                "Effect": "Allow",
615                "Action": "ec2:RunInstances",
616                "Resource": "*",
617                "Principal": {"AWS": "123456789012"},
618                "Condition": {
619                    ["1"]: {
620                        "ec2:Region": "us-west-2"
621                    }
622                }
623            }
624        }"# };
625        let e = Policy::from_str(policy_str).unwrap_err();
626        assert_eq!(e.to_string(), r#"key must be a string at line 11 column 13"#);
627    }
628
629    #[test_log::test]
630    fn test_bad_from_str() {
631        let e = Policy::from_str("{}").unwrap_err();
632        assert_eq!(e.to_string(), "missing field `Statement` at line 1 column 2");
633    }
634
635    #[test_log::test]
636    fn test_bad_types() {
637        let e = Policy::from_str("3").unwrap_err();
638        assert_eq!(e.to_string(), "invalid type: integer `3`, expected policy at line 1 column 1");
639
640        let e = Policy::from_str(r#"[1, 2]"#).unwrap_err();
641        assert_eq!(e.to_string(), "invalid type: sequence, expected policy at line 1 column 0");
642
643        let e = Policy::from_str(r#"{1: 1}"#).unwrap_err();
644        assert_eq!(e.to_string(), "key must be a string at line 1 column 2");
645    }
646
647    #[test_log::test]
648    #[allow(clippy::redundant_clone)]
649    fn test_builder() {
650        let e = Policy::builder().clone().build().unwrap_err();
651        assert_eq!(e.to_string(), "`statement` must be initialized");
652        assert_eq!(format!("{e}"), "`statement` must be initialized");
653        assert_eq!(format!("{e:?}"), r#"UninitializedField("statement")"#);
654        assert_eq!(format!("{}", PolicyBuilderError::from("Oops".to_string())), "Oops");
655
656        let s = Statement::builder()
657            .effect(Effect::Allow)
658            .action(Action::from_str("ec2:RunInstances").unwrap())
659            .resource(Resource::from_str("arn:aws:ec2:us-east-1:123456789012:instance/i-01234567890abcdef").unwrap())
660            .principal(
661                SpecifiedPrincipal::builder().aws(AwsPrincipal::from_str("123456789012").unwrap()).build().unwrap(),
662            )
663            .build()
664            .unwrap();
665        let p1a = Policy::builder().statement(s.clone()).build().unwrap();
666        let p1b = Policy::builder().statement(s.clone()).build().unwrap();
667        let p2 = Policy::builder().version(PolicyVersion::V2012_10_17).id("test").statement(s).build().unwrap();
668
669        assert_eq!(p1a, p1b);
670        assert_eq!(p1a, p1a.clone());
671        assert_ne!(p1a, p2);
672
673        let _ = format!("{p1a:?}");
674        let json = format!("{p2}");
675
676        assert_eq!(
677            json,
678            indoc! {r#"
679            {
680                "Version": "2012-10-17",
681                "Id": "test",
682                "Statement": {
683                    "Effect": "Allow",
684                    "Action": "ec2:RunInstances",
685                    "Resource": "arn:aws:ec2:us-east-1:123456789012:instance/i-01234567890abcdef",
686                    "Principal": {
687                        "AWS": "123456789012"
688                    }
689                }
690            }"#}
691        );
692
693        let s = Statement::builder()
694            .effect(Effect::Allow)
695            .action(Action::from_str("ec2:RunInstances").unwrap())
696            .resource(Resource::from_str("arn:aws:ec2:us-east-1:123456789012:instance/i-01234567890abcdef").unwrap())
697            .principal(
698                SpecifiedPrincipal::builder().aws(AwsPrincipal::from_str("123456789012").unwrap()).build().unwrap(),
699            )
700            .build()
701            .unwrap();
702        let p1a = Policy::builder().statement(s.clone()).build().unwrap();
703        let p1b = Policy::builder().statement(s.clone()).build().unwrap();
704        let p2 = Policy::builder().version(PolicyVersion::None).id("test").statement(s).build().unwrap();
705
706        assert_eq!(p1a, p1b);
707        assert_eq!(p1a, p1a.clone());
708        assert_ne!(p1a, p2);
709
710        let _ = format!("{p1a:?}");
711        let json = format!("{p2}");
712
713        assert_eq!(
714            json,
715            indoc! {r#"
716            {
717                "Id": "test",
718                "Statement": {
719                    "Effect": "Allow",
720                    "Action": "ec2:RunInstances",
721                    "Resource": "arn:aws:ec2:us-east-1:123456789012:instance/i-01234567890abcdef",
722                    "Principal": {
723                        "AWS": "123456789012"
724                    }
725                }
726            }"#}
727        );
728    }
729
730    #[test_log::test]
731    fn test_conflicting_blocks() {
732        let policy_str = indoc! { r#"
733        {
734            "Version": "2012-10-17",
735            "Id": "PolicyId",
736            "Statement": {
737                "Sid": "1",
738                "Effect": "Allow",
739                "Action": [
740                    "ec2:Get*",
741                    "ecs:*"
742                ],
743                "NotAction": [
744                    "rds:*"
745                ],
746                "Resource": "*",
747                "Principal": {
748                    "AWS": "123456789012"
749                },
750                "Condition": {
751                    "StringEquals": {
752                        "ec2:Region": [
753                            "us-west-2"
754                        ]
755                    }
756                }
757            }
758        }"# };
759        let e = Policy::from_str(policy_str).unwrap_err();
760        assert_eq!(e.to_string(), "Action and NotAction cannot both be set at line 25 column 5");
761
762        let policy_str = indoc! { r#"
763        {
764            "Version": "2012-10-17",
765            "Id": "PolicyId",
766            "Statement": {
767                "Sid": "1",
768                "Effect": "Allow",
769                "Action": [
770                    "ec2:Get*",
771                    "ecs:*"
772                ],
773                "Resource": "*",
774                "NotResource": "*",
775                "Principal": {
776                    "AWS": "123456789012"
777                },
778                "Condition": {
779                    "StringEquals": {
780                        "ec2:Region": [
781                            "us-west-2"
782                        ]
783                    }
784                }
785            }
786        }"# };
787        let e = Policy::from_str(policy_str).unwrap_err();
788        assert_eq!(e.to_string(), "Resource and NotResource cannot both be set at line 23 column 5");
789
790        let policy_str = indoc! { r#"
791        {
792            "Version": "2012-10-17",
793            "Id": "PolicyId",
794            "Statement": {
795                "Sid": "1",
796                "Effect": "Allow",
797                "Action": [
798                    "ec2:Get*",
799                    "ecs:*"
800                ],
801                "Resource": "*",
802                "Principal": {
803                    "AWS": "123456789012"
804                },
805                "NotPrincipal": {
806                    "CanonicalUser": "abcd"
807                },
808                "Condition": {
809                    "StringEquals": {
810                        "ec2:Region": [
811                            "us-west-2"
812                        ]
813                    }
814                }
815            }
816        }"# };
817        let e = Policy::from_str(policy_str).unwrap_err();
818        assert_eq!(e.to_string(), "Principal and NotPrincipal cannot both be set at line 25 column 5");
819
820        let policy_str = indoc! { r#"
821        {
822            "Version": "2012-10-17",
823            "Id": "PolicyId",
824            "Statement": {
825                "Sid": "1",
826                "Effect": "Allow",
827                "Action": [
828                    "ec2:Get*",
829                    "ecs:*"
830                ],
831                "Resource": "*",
832                "NotResource": [
833                    "arn:aws:s3:::my-bucket"
834                ],
835                "Principal": {
836                    "AWS": "123456789012"
837                },
838                "Condition": {
839                    "StringEquals": {
840                        "ec2:Region": [
841                            "us-west-2"
842                        ]
843                    }
844                }
845            }
846        }"# };
847        let e = Policy::from_str(policy_str).unwrap_err();
848        assert_eq!(e.to_string(), "Resource and NotResource cannot both be set at line 25 column 5");
849
850        let policy_str = indoc! { r#"
851        {
852            "Version": "2012-10-17",
853            "Id": "PolicyId",
854            "Statement": {
855                "Sid": "1",
856                "Sid": "2",
857                "Effect": "Allow",
858                "Action": [
859                    "ec2:Get*",
860                    "ecs:*"
861                ],
862                "Resource": "*",
863                "Principal": {
864                    "AWS": "123456789012"
865                },
866                "Condition": {
867                    "StringEquals": {
868                        "ec2:Region": [
869                            "us-west-2"
870                        ]
871                    }
872                }
873            }
874        }"# };
875        let e = Policy::from_str(policy_str).unwrap_err();
876        assert_eq!(e.to_string(), "duplicate field `Sid` at line 6 column 13");
877
878        let policy_str = indoc! { r#"
879        {
880            "Version": "2012-10-17",
881            "Id": "PolicyId",
882            "Statement": {
883                "Sid": "1",
884                "Effect": "Allow",
885                "Effect": "Deny",
886                "Action": [
887                    "ec2:Get*",
888                    "ecs:*"
889                ],
890                "Resource": "*",
891                "Principal": {
892                    "AWS": "123456789012"
893                },
894                "Condition": {
895                    "StringEquals": {
896                        "ec2:Region": [
897                            "us-west-2"
898                        ]
899                    }
900                }
901            }
902        }"# };
903        let e = Policy::from_str(policy_str).unwrap_err();
904        assert_eq!(e.to_string(), "duplicate field `Effect` at line 7 column 16");
905
906        let policy_str = indoc! { r#"
907        {
908            "Version": "2012-10-17",
909            "Id": "PolicyId",
910            "Statement": {
911                "Sid": "1",
912                "Effect": "Allow",
913                "Action": [
914                    "ec2:Get*",
915                    "ecs:*"
916                ],
917                "Action": [
918                    "rds:*"
919                ],
920                "Resource": "*",
921                "Principal": {
922                    "AWS": "123456789012"
923                },
924                "Condition": {
925                    "StringEquals": {
926                        "ec2:Region": [
927                            "us-west-2"
928                        ]
929                    }
930                }
931            }
932        }"# };
933        let e = Policy::from_str(policy_str).unwrap_err();
934        assert_eq!(e.to_string(), "duplicate field `Action` at line 11 column 16");
935
936        let policy_str = indoc! { r#"
937        {
938            "Version": "2012-10-17",
939            "Id": "PolicyId",
940            "Statement": {
941                "Sid": "1",
942                "Effect": "Allow",
943                "NotAction": [
944                    "ec2:Get*",
945                    "ecs:*"
946                ],
947                "NotAction": [
948                    "rds:*"
949                ],
950                "Resource": "*",
951                "Principal": {
952                    "AWS": "123456789012"
953                },
954                "Condition": {
955                    "StringEquals": {
956                        "ec2:Region": [
957                            "us-west-2"
958                        ]
959                    }
960                }
961            }
962        }"# };
963        let e = Policy::from_str(policy_str).unwrap_err();
964        assert_eq!(e.to_string(), "duplicate field `NotAction` at line 11 column 19");
965
966        let policy_str = indoc! { r#"
967        {
968            "Version": "2012-10-17",
969            "Id": "PolicyId",
970            "Statement": {
971                "Sid": "1",
972                "Effect": "Allow",
973                "Action": [
974                    "ec2:Get*",
975                    "ecs:*"
976                ],
977                "Resource": "*",
978                "Resource": [
979                    "arn:aws:s3:::my-bucket"
980                ],
981                "Principal": {
982                    "AWS": "123456789012"
983                },
984                "Condition": {
985                    "StringEquals": {
986                        "ec2:Region": [
987                            "us-west-2"
988                        ]
989                    }
990                }
991            }
992        }"# };
993        let e = Policy::from_str(policy_str).unwrap_err();
994        assert_eq!(e.to_string(), "duplicate field `Resource` at line 12 column 18");
995
996        let policy_str = indoc! { r#"
997        {
998            "Version": "2012-10-17",
999            "Id": "PolicyId",
1000            "Statement": {
1001                "Sid": "1",
1002                "Effect": "Allow",
1003                "Action": [
1004                    "ec2:Get*",
1005                    "ecs:*"
1006                ],
1007                "NotResource": "*",
1008                "NotResource": [
1009                    "arn:aws:s3:::my-bucket"
1010                ],
1011                "Principal": {
1012                    "AWS": "123456789012"
1013                },
1014                "Condition": {
1015                    "StringEquals": {
1016                        "ec2:Region": [
1017                            "us-west-2"
1018                        ]
1019                    }
1020                }
1021            }
1022        }"# };
1023        let e = Policy::from_str(policy_str).unwrap_err();
1024        assert_eq!(e.to_string(), "duplicate field `NotResource` at line 12 column 21");
1025
1026        let policy_str = indoc! { r#"
1027        {
1028            "Version": "2012-10-17",
1029            "Id": "PolicyId",
1030            "Statement": {
1031                "Sid": "1",
1032                "Effect": "Allow",
1033                "Action": [
1034                    "ec2:Get*",
1035                    "ecs:*"
1036                ],
1037                "Resource": "*",
1038                "Principal": {
1039                    "AWS": "123456789012"
1040                },
1041                "Principal": {
1042                    "AWS": "123456789012"
1043                },
1044                "Condition": {
1045                    "StringEquals": {
1046                        "ec2:Region": [
1047                            "us-west-2"
1048                        ]
1049                    }
1050                }
1051            }
1052        }"# };
1053        let e = Policy::from_str(policy_str).unwrap_err();
1054        assert_eq!(e.to_string(), "duplicate field `Principal` at line 15 column 19");
1055
1056        let policy_str = indoc! { r#"
1057        {
1058            "Version": "2012-10-17",
1059            "Id": "PolicyId",
1060            "Statement": {
1061                "Sid": "1",
1062                "Effect": "Allow",
1063                "Action": [
1064                    "ec2:Get*",
1065                    "ecs:*"
1066                ],
1067                "Resource": "*",
1068                "NotPrincipal": {
1069                    "AWS": "123456789012"
1070                },
1071                "NotPrincipal": {
1072                    "AWS": "123456789012"
1073                },
1074                "Condition": {
1075                    "StringEquals": {
1076                        "ec2:Region": [
1077                            "us-west-2"
1078                        ]
1079                    }
1080                }
1081            }
1082        }"# };
1083        let e = Policy::from_str(policy_str).unwrap_err();
1084        assert_eq!(e.to_string(), "duplicate field `NotPrincipal` at line 15 column 22");
1085
1086        let policy_str = indoc! { r#"
1087        {
1088            "Version": "2012-10-17",
1089            "Id": "PolicyId",
1090            "Statement": {
1091                "Sid": "1",
1092                "Effect": "Allow",
1093                "Action": [
1094                    "ec2:Get*",
1095                    "ecs:*"
1096                ],
1097                "Resource": "*",
1098                "NotPrincipal": {
1099                    "AWS": "123456789012"
1100                },
1101                "Condition": {
1102                    "StringEquals": {
1103                        "ec2:Region": [
1104                            "us-west-2"
1105                        ]
1106                    }
1107                },
1108                "Condition": {
1109                }
1110            }
1111        }"# };
1112        let e = Policy::from_str(policy_str).unwrap_err();
1113        assert_eq!(e.to_string(), "duplicate field `Condition` at line 22 column 19");
1114    }
1115
1116    #[test_log::test]
1117    fn test_duplicate_fields() {
1118        let policy_str = indoc! { r#"
1119    {
1120        "Version": "2012-10-17",
1121        "Id": "PolicyId",
1122        "Statement": {
1123            "Effect": "Allow",
1124            "Action": "*",
1125            "Resource": "*"
1126        },
1127        "Version": "2012-10-17"
1128    }"# };
1129        let e = Policy::from_str(policy_str).unwrap_err();
1130        assert_eq!(e.to_string(), "duplicate field `Version` at line 9 column 13");
1131
1132        let policy_str = indoc! { r#"
1133    {
1134        "Version": "2012-10-17",
1135        "Id": "PolicyId",
1136        "Statement": {
1137            "Effect": "Allow",
1138            "Action": "*",
1139            "Resource": "*"
1140        },
1141        "Id": "2012-10-17"
1142    }"# };
1143        let e = Policy::from_str(policy_str).unwrap_err();
1144        assert_eq!(e.to_string(), "duplicate field `Id` at line 9 column 8");
1145
1146        let policy_str = indoc! { r#"
1147        {
1148            "Version": "2012-10-17",
1149            "Id": "PolicyId",
1150            "Statement": {
1151                "Effect": "Allow",
1152                "Action": "*",
1153                "Resource": "*"
1154            },
1155            "Statement": {
1156                "Effect": "Allow",
1157                "Action": "*",
1158                "Resource": "*"
1159            }
1160        }"# };
1161        let e = Policy::from_str(policy_str).unwrap_err();
1162        assert_eq!(e.to_string(), "duplicate field `Statement` at line 9 column 15");
1163    }
1164
1165    #[test_log::test]
1166    fn test_ec2_describe_bug() {
1167        let policy = Policy::from_str(indoc! {r#"
1168        {
1169            "Version": "2012-10-17",
1170            "Statement": [
1171                {
1172                    "Effect": "Allow",
1173                    "Action": [
1174                        "ec2:Describe*"
1175                    ],
1176                    "Resource": "*"
1177                }
1178            ]
1179        }
1180    "#})
1181        .unwrap();
1182        let actor = PrincipalActor::from(User::new("aws", "123456789012", "/", "MyUser").unwrap());
1183        let mut sd = SessionData::new();
1184        sd.insert("aws:username", SessionValue::from("MyUser"));
1185        let context = Context::builder()
1186            .api("DescribeSecurityGroups")
1187            .actor(actor)
1188            .session_data(sd)
1189            .service("ec2")
1190            .build()
1191            .unwrap();
1192
1193        assert_eq!(policy.evaluate(&context).unwrap(), Decision::Allow);
1194    }
1195
1196    #[test_log::test]
1197    fn test_not_action() {
1198        let policy = Policy::from_str(indoc! {r#"
1199        {
1200            "Version": "2012-10-17",
1201            "Statement": [
1202                {
1203                    "Effect": "Allow",
1204                    "NotAction": [
1205                        "ec2:Describe*"
1206                    ],
1207                    "Resource": "*"
1208                }
1209            ]
1210        }"# })
1211        .unwrap();
1212        let actor = PrincipalActor::from(User::new("aws", "123456789012", "/", "MyUser").unwrap());
1213        let sd = SessionData::new();
1214        let context = Context::builder()
1215            .api("DescribeSecurityGroups")
1216            .actor(actor.clone())
1217            .service("ec2")
1218            .session_data(sd.clone())
1219            .build()
1220            .unwrap();
1221        assert_eq!(policy.evaluate(&context).unwrap(), Decision::DefaultDeny);
1222        let context =
1223            Context::builder().api("RunInstances").actor(actor).service("ec2").session_data(sd).build().unwrap();
1224        assert_eq!(policy.evaluate(&context).unwrap(), Decision::Allow);
1225    }
1226
1227    #[test_log::test]
1228    fn test_not_resource() {
1229        let policy = Policy::from_str(indoc! {r#"
1230        {
1231            "Version": "2012-10-17",
1232            "Statement": [
1233                {
1234                    "Effect": "Allow",
1235                    "Action": [
1236                        "ec2:TerminateInstances"
1237                    ],
1238                    "NotResource": [
1239                        "arn:aws:ec2:*:*:instance/i-012*",
1240                        "arn:aws:ec2:*:*:network-interface/eni-012*"
1241                    ]
1242                }
1243            ]
1244        }"# })
1245        .unwrap();
1246
1247        let matching_instance =
1248            Arn::from_str("arn:aws:ec2:us-east-1:123456789012:instance/i-0123456789abcdef0").unwrap();
1249        let matching_eni =
1250            Arn::from_str("arn:aws:ec2:us-east-1:123456789012:network-interface/eni-0123456789abcdef0").unwrap();
1251        let nonmatching_instance =
1252            Arn::from_str("arn:aws:ec2:us-east-1:123456789012:instance/i-0223456789abcdef0").unwrap();
1253        let nonmatching_eni =
1254            Arn::from_str("arn:aws:ec2:us-east-1:123456789012:network-interface/eni-0223456789abcdef0").unwrap();
1255
1256        let actor = PrincipalActor::from(User::from_str("arn:aws:iam::123456789012:user/MyUser").unwrap());
1257        let sd = SessionData::new();
1258        let mut context_builder = Context::builder();
1259        context_builder.api("TerminateInstances").actor(actor).service("ec2").session_data(sd);
1260
1261        context_builder.resources(vec![matching_instance.clone(), matching_eni.clone()]);
1262        let context = context_builder.build().unwrap();
1263        assert_eq!(policy.evaluate(&context).unwrap(), Decision::DefaultDeny);
1264
1265        context_builder.resources(vec![matching_instance, nonmatching_eni.clone()]);
1266        let context = context_builder.build().unwrap();
1267        assert!(!context.resources().is_empty());
1268        assert_eq!(policy.evaluate(&context).unwrap(), Decision::DefaultDeny);
1269
1270        context_builder.resources(vec![nonmatching_instance.clone(), matching_eni]);
1271        let context = context_builder.build().unwrap();
1272        assert_eq!(policy.evaluate(&context).unwrap(), Decision::DefaultDeny);
1273
1274        context_builder.resources(vec![nonmatching_instance, nonmatching_eni]);
1275        let context = context_builder.build().unwrap();
1276        assert_eq!(policy.evaluate(&context).unwrap(), Decision::Allow);
1277
1278        context_builder.resources(vec![]);
1279        let context = context_builder.build().unwrap();
1280        assert_eq!(policy.evaluate(&context).unwrap(), Decision::Allow);
1281
1282        let policy = Policy::from_str(indoc! {r#"
1283        {
1284            "Version": "2012-10-17",
1285            "Statement": [
1286                {
1287                    "Effect": "Allow",
1288                    "Action": [
1289                        "ec2:TerminateInstances"
1290                    ],
1291                    "NotResource": "*"
1292                }
1293            ]
1294        }"# })
1295        .unwrap();
1296
1297        let matching_instance =
1298            Arn::from_str("arn:aws:ec2:us-east-1:123456789012:instance/i-0123456789abcdef0").unwrap();
1299        let matching_eni =
1300            Arn::from_str("arn:aws:ec2:us-east-1:123456789012:network-interface/eni-0123456789abcdef0").unwrap();
1301        context_builder.resources(vec![matching_instance, matching_eni]);
1302        let context = context_builder.build().unwrap();
1303        assert_eq!(policy.evaluate(&context).unwrap(), Decision::DefaultDeny);
1304
1305        context_builder.resources(vec![]);
1306        let context = context_builder.build().unwrap();
1307        assert_eq!(policy.evaluate(&context).unwrap(), Decision::DefaultDeny);
1308    }
1309
1310    #[test_log::test]
1311    fn test_policy_version() {
1312        assert_eq!(PolicyVersion::default(), PolicyVersion::None);
1313
1314        assert_eq!(format!("{}", PolicyVersion::None), "");
1315        assert_eq!(format!("{}", PolicyVersion::V2008_10_17), "2008-10-17");
1316        assert_eq!(format!("{}", PolicyVersion::V2012_10_17), "2012-10-17");
1317
1318        assert_eq!(format!("{:?}", PolicyVersion::None), "None");
1319        assert_eq!(format!("{:?}", PolicyVersion::V2008_10_17), "V2008_10_17");
1320        assert_eq!(format!("{:?}", PolicyVersion::V2012_10_17), "V2012_10_17");
1321
1322        let mut h1 = DefaultHasher::new();
1323        let mut h2 = DefaultHasher::new();
1324        let mut h3 = DefaultHasher::new();
1325        PolicyVersion::None.hash(&mut h1);
1326        PolicyVersion::V2008_10_17.hash(&mut h2);
1327        PolicyVersion::V2012_10_17.hash(&mut h3);
1328        let h1 = h1.finish();
1329        let h2 = h2.finish();
1330        let h3 = h3.finish();
1331
1332        assert_ne!(h1, h2);
1333        assert_ne!(h1, h3);
1334        assert_ne!(h2, h3);
1335
1336        assert_eq!(PolicyVersion::from_str("2008-10-17").unwrap(), PolicyVersion::V2008_10_17);
1337        assert_eq!(PolicyVersion::from_str("2012-10-17").unwrap(), PolicyVersion::V2012_10_17);
1338        assert_eq!(
1339            PolicyVersion::from_str("2012-10-18").unwrap_err(),
1340            AspenError::InvalidPolicyVersion("2012-10-18".to_string())
1341        );
1342
1343        let e = serde_json::from_str::<PolicyVersion>(r#""2012-10-18""#).unwrap_err();
1344        assert_eq!(e.to_string(), "Invalid policy version: 2012-10-18");
1345
1346        let e = serde_json::from_str::<PolicyVersion>(r#"2012"#).unwrap_err();
1347        assert_eq!(e.to_string(), "invalid type: integer `2012`, expected a string at line 1 column 4");
1348    }
1349
1350    #[test_log::test]
1351    fn test_principals() {
1352        let policy_str = indoc! { r#"
1353            {
1354                "Version": "2012-10-17",
1355                "Statement": {
1356                    "Effect": "Allow",
1357                    "Action": "*",
1358                    "Resource": "*",
1359                    "Principal": {
1360                        "AWS": "*"
1361                    }
1362                }
1363            }"# };
1364        let policy = Policy::from_str(policy_str).unwrap();
1365        let statement = policy.statement();
1366        assert_eq!(statement.len(), 1);
1367        assert_eq!(statement.to_vec().len(), 1);
1368        let principal = statement[0].principal().unwrap();
1369        if let Principal::Specified(specified) = principal {
1370            let aws = specified.aws().unwrap();
1371            assert_eq!(aws.len(), 1);
1372            assert_eq!(aws[0], AwsPrincipal::Any);
1373            assert_eq!(format!("{}", aws[0]), "*");
1374            assert_eq!(aws.to_vec(), vec![&AwsPrincipal::Any]);
1375        } else {
1376            panic!("principal is not SpecifiedPrincipal");
1377        }
1378
1379        assert_eq!(
1380            format!("{}", statement[0]),
1381            indoc! {r#"
1382            {
1383                "Effect": "Allow",
1384                "Action": "*",
1385                "Resource": "*",
1386                "Principal": {
1387                    "AWS": "*"
1388                }
1389            }"#}
1390        );
1391
1392        let actor = PrincipalActor::from(User::from_str("arn:aws:iam::123456789012:user/MyUser").unwrap());
1393        let instance = Arn::from_str("arn:aws:ec2:us-east-1:123456789012:instance/i-0123456789abcdef0").unwrap();
1394        let eni = Arn::from_str("arn:aws:ec2:us-east-1:123456789012:network-interface/eni-0123456789abcdef0").unwrap();
1395
1396        let sd = SessionData::new();
1397        let mut context_builder = Context::builder();
1398        context_builder
1399            .api("TerminateInstances")
1400            .actor(actor.clone())
1401            .service("ec2")
1402            .session_data(sd)
1403            .resources(vec![instance, eni]);
1404        let context = context_builder.build().unwrap();
1405        assert_eq!(policy.evaluate(&context).unwrap(), Decision::Allow);
1406
1407        let policy_str = indoc! { r#"
1408            {
1409                "Version": "2012-10-17",
1410                "Statement": {
1411                    "Effect": "Allow",
1412                    "Action": "*",
1413                    "Resource": "*",
1414                    "Principal": {
1415                        "AWS": "arn:aws:iam::123456789012:root"
1416                    }
1417                }
1418            }"# };
1419        let policy = Policy::from_str(policy_str).unwrap();
1420        let principal = policy.statement()[0].principal().unwrap();
1421        let specified = principal.specified().unwrap();
1422        let aws = specified.aws().unwrap();
1423        assert_eq!(aws.len(), 1);
1424        assert_eq!(aws[0], AwsPrincipal::Arn(Arn::from_str("arn:aws:iam::123456789012:root").unwrap()));
1425        assert_eq!(format!("{}", aws[0]), "arn:aws:iam::123456789012:root");
1426        assert_eq!(policy.evaluate(&context).unwrap(), Decision::Allow);
1427
1428        context_builder.actor(PrincipalActor::from(Service::new("ec2", None, "amazonaws.com").unwrap()));
1429        let context = context_builder.build().unwrap();
1430        assert_eq!(policy.evaluate(&context).unwrap(), Decision::DefaultDeny);
1431
1432        let policy_str = indoc! { r#"
1433            {
1434                "Version": "2012-10-17",
1435                "Statement": {
1436                    "Effect": "Allow",
1437                    "Action": "*",
1438                    "Resource": "*",
1439                    "NotPrincipal": {
1440                        "AWS": "arn:aws:iam::123456789012:root"
1441                    }
1442                }
1443            }"# };
1444        let policy = Policy::from_str(policy_str).unwrap();
1445        context_builder.actor(actor.clone());
1446        let context = context_builder.build().unwrap();
1447        assert_eq!(policy.evaluate(&context).unwrap(), Decision::DefaultDeny);
1448
1449        context_builder.actor(PrincipalActor::from(Service::new("ec2", None, "amazonaws.com").unwrap()));
1450        let context = context_builder.build().unwrap();
1451        assert_eq!(policy.evaluate(&context).unwrap(), Decision::Allow);
1452
1453        let policy_str = indoc! { r#"
1454            {
1455                "Version": "2012-10-17",
1456                "Statement": {
1457                    "Effect": "Allow",
1458                    "Action": "*",
1459                    "Resource": "*",
1460                    "Principal": "*"
1461                }
1462            }"# };
1463        let policy = Policy::from_str(policy_str).unwrap();
1464        context_builder.actor(actor);
1465        let context = context_builder.build().unwrap();
1466        assert_eq!(policy.evaluate(&context).unwrap(), Decision::Allow);
1467
1468        context_builder.actor(PrincipalActor::from(Service::new("ec2", None, "amazonaws.com").unwrap()));
1469        let context = context_builder.build().unwrap();
1470        assert_eq!(policy.evaluate(&context).unwrap(), Decision::Allow);
1471    }
1472
1473    #[test_log::test]
1474    fn test_serialization() {
1475        let p1_str = include_str!("test-policy-1.json");
1476        let p2_str = include_str!("test-policy-2.json");
1477        let p1 = Policy::from_str(p1_str).unwrap();
1478        let p2 = Policy::from_str(p2_str).unwrap();
1479
1480        let statements = p1.statement.to_vec();
1481        assert_eq!(statements.len(), 2);
1482        let actions = statements[0].action().unwrap().to_vec();
1483        assert_eq!(actions.len(), 1);
1484        assert_eq!(actions[0], &Action::new("s3", "ListBucket").unwrap());
1485
1486        assert_eq!(*statements[1].effect(), Effect::Deny);
1487
1488        let actions = statements[1].not_action().unwrap().to_vec();
1489        assert_eq!(actions.len(), 3);
1490        assert_eq!(actions[0], &Action::new("ec2", "*").unwrap());
1491        assert_eq!(actions[1], &Action::new("s3", "*").unwrap());
1492        assert_eq!(actions[2], &Action::new("rds", "*").unwrap());
1493
1494        let resources = statements[1].resource().unwrap().to_vec();
1495        assert_eq!(resources.len(), 3);
1496        assert_eq!(*resources[0], Resource::from_str("arn:aws:ec2:*:*:instance/*").unwrap());
1497        assert_eq!(*resources[1], Resource::from_str("arn:aws:s3:*:*:bucket/*").unwrap());
1498        assert_eq!(*resources[2], Resource::from_str("arn:aws:rds:*:*:db/*").unwrap());
1499
1500        let principals = statements[1].principal().unwrap();
1501        if let Principal::Specified(ref specified) = principals {
1502            let aws = specified.aws().unwrap().to_vec();
1503            assert_eq!(aws.len(), 2);
1504            assert_eq!(*aws[0], AwsPrincipal::from_str("arn:aws:iam::123456789012:root").unwrap());
1505            assert_eq!(*aws[1], AwsPrincipal::from_str("arn:aws:iam::123456789012:user/*").unwrap());
1506
1507            let canonical_users = specified.canonical_user().unwrap().to_vec();
1508            assert_eq!(canonical_users.len(), 2);
1509            assert_eq!(canonical_users[0], "d04207a7d9311e77f5837e0e4f4b025322bf2f626f0872c85be8c6bb1290c88b");
1510            assert_eq!(canonical_users[1], "2cdb0173470eb5b200f82c8e1b51a88562924cda12e2ccce60d7f00e1567ee7c");
1511
1512            let federated = specified.federated().unwrap().to_vec();
1513            assert_eq!(federated.len(), 1);
1514            assert_eq!(federated[0], "dacut@kanga.org");
1515
1516            let service = specified.service().unwrap().to_vec();
1517            assert_eq!(service.len(), 3);
1518            assert_eq!(service[0], "ec2.amazonaws.com");
1519            assert_eq!(service[1], "edgelambda.amazonaws.com");
1520            assert_eq!(service[2], "lambda.amazonaws.com");
1521        } else {
1522            panic!("Expected SpecifiedPrincipal");
1523        }
1524
1525        let json = serde_json::to_string_pretty(&p1).unwrap();
1526        assert_eq!(json, p1_str);
1527
1528        assert_ne!(p1, p2);
1529    }
1530
1531    #[test_log::test]
1532    fn test_unknown_field() {
1533        let policy_str = indoc! { r#"
1534        {
1535            "Version": "2012-10-17",
1536            "Id": "PolicyId",
1537            "Statement": {
1538                "Sid": "1",
1539                "Effect": "Allow",
1540                "Action": [
1541                    "ec2:Get*",
1542                    "ecs:*"
1543                ],
1544                "Instance": [
1545                    "i-0123456789abcdef0",
1546                ],
1547                "Resource": "*",
1548                "Principal": {
1549                    "AWS": "123456789012"
1550                },
1551                "Condition": {
1552                    "StringEquals": {
1553                        "ec2:Region": [
1554                            "us-west-2"
1555                        ]
1556                    }
1557                }
1558            }
1559        }"# };
1560        let e = Policy::from_str(policy_str).unwrap_err();
1561        assert_eq!(e.to_string(), "unknown field `Instance`, expected one of `Sid`, `Effect`, `Action`, `NotAction`, `Resource`, `NotResource`, `Principal`, `NotPrincipal`, `Condition` at line 11 column 18");
1562
1563        let policy_str = indoc! { r#"
1564        {
1565            "Version": "2012-10-17",
1566            "Id": "PolicyId",
1567            "Statement": {
1568                "Effect": "Allow",
1569                "Action": "*",
1570                "Resource": "*"
1571            },
1572            "Test": true
1573        }"# };
1574        let e = Policy::from_str(policy_str).unwrap_err();
1575        assert_eq!(
1576            e.to_string(),
1577            "unknown field `Test`, expected one of `Version`, `Id`, `Statement` at line 9 column 10"
1578        );
1579    }
1580}