scratchstack_aspen/resource/
mod.rs

1mod arn;
2
3use {
4    crate::{serutil::StringLikeList, AspenError, Context, PolicyVersion},
5    scratchstack_arn::Arn,
6    std::{
7        fmt::{Display, Formatter, Result as FmtResult},
8        str::FromStr,
9    },
10};
11
12pub use arn::ResourceArn;
13
14/// A list of resources. In JSON, this may be a string or an array of strings.
15pub type ResourceList = StringLikeList<Resource>;
16
17/// A resource in an Aspen policy.
18///
19/// Resource enums are immutable.
20#[derive(Clone, Debug, Eq, PartialEq)]
21pub enum Resource {
22    /// Any resource. This is specified by the wildcard character `*`.
23    Any,
24
25    /// A resource specified by an ARN.
26    Arn(ResourceArn),
27}
28
29impl Resource {
30    /// If this is [Resource::Any], returns true.
31    #[inline]
32    pub fn is_any(&self) -> bool {
33        matches!(self, Self::Any)
34    }
35
36    /// Indicates whether this [Resource] matches the candidate [Arn], given the request [Context] ad using variable
37    /// substitution rules according to the specified [PolicyVersion].
38    /// # Example
39    /// ```
40    /// # use scratchstack_aspen::{Context, PolicyVersion, Resource, ResourceArn};
41    /// # use scratchstack_arn::Arn;
42    /// # use scratchstack_aws_principal::{Principal, User, SessionData, SessionValue};
43    /// # use std::str::FromStr;
44    /// let actor = Principal::from(vec![User::from_str("arn:aws:iam::123456789012:user/exampleuser").unwrap().into()]);
45    /// let s3_object_arn = Arn::from_str("arn:aws:s3:::examplebucket/exampleuser/my-object").unwrap();
46    /// let resources = vec![s3_object_arn.clone()];
47    /// let session_data = SessionData::from([("aws:username", SessionValue::from("exampleuser"))]);
48    /// let context = Context::builder()
49    ///     .service("s3").api("GetObject").actor(actor).resources(resources)
50    ///     .session_data(session_data).build().unwrap();
51    /// let r1 = Resource::Arn(ResourceArn::new("aws", "s3", "", "", "examplebucket/${aws:username}/*"));
52    /// let r2 = Resource::Any;
53    /// assert!(r1.matches(&context, PolicyVersion::V2012_10_17, &s3_object_arn).unwrap());
54    /// assert!(r2.matches(&context, PolicyVersion::V2012_10_17, &s3_object_arn).unwrap());
55    ///
56    /// let bad_s3_object_arn = Arn::from_str("arn:aws:s3:::examplebucket/other-user/object").unwrap();
57    /// assert!(!r1.matches(&context, PolicyVersion::V2012_10_17, &bad_s3_object_arn).unwrap());
58    /// assert!(r2.matches(&context, PolicyVersion::V2012_10_17, &bad_s3_object_arn).unwrap());
59    /// ```
60    pub fn matches(&self, context: &Context, pv: PolicyVersion, candidate: &Arn) -> Result<bool, AspenError> {
61        match self {
62            Self::Any => Ok(true),
63            Self::Arn(pattern) => pattern.matches(context, pv, candidate),
64        }
65    }
66}
67
68impl FromStr for Resource {
69    type Err = AspenError;
70
71    fn from_str(s: &str) -> Result<Self, Self::Err> {
72        if s == "*" {
73            return Ok(Self::Any);
74        }
75
76        let pattern = ResourceArn::from_str(s)?;
77        Ok(Self::Arn(pattern))
78    }
79}
80
81impl Display for Resource {
82    fn fmt(&self, f: &mut Formatter) -> FmtResult {
83        match self {
84            Self::Any => f.write_str("*"),
85            Self::Arn(arn_pattern) => f.write_str(&arn_pattern.to_string()),
86        }
87    }
88}
89
90#[cfg(test)]
91mod tests {
92    use {
93        crate::{serutil::JsonRep, Resource, ResourceArn, ResourceList},
94        indoc::indoc,
95        pretty_assertions::assert_eq,
96        std::{panic::catch_unwind, str::FromStr},
97    };
98
99    #[test_log::test]
100    fn deserialize_resource_list_star() {
101        let resource_list: ResourceList = serde_json::from_str("\"*\"").unwrap();
102        assert_eq!(resource_list.kind(), JsonRep::Single);
103        let v = resource_list.to_vec();
104        assert_eq!(v, vec![&Resource::Any]);
105        assert!(!v.is_empty());
106    }
107
108    #[test_log::test]
109    fn check_from() {
110        let ap = ResourceArn::from_str("arn:*:ec*:us-*-2:123?56789012:instance/*").unwrap();
111        let rl1: ResourceList = Resource::Arn(ap.clone()).into();
112        let rl2: ResourceList = Resource::Arn(ap.clone()).into();
113        let rl3: ResourceList = vec![Resource::Arn(ap.clone())].into();
114        let rl4: ResourceList = vec![Resource::Arn(ap.clone())].into();
115
116        assert_eq!(rl1, rl2);
117        assert_eq!(rl1, rl3);
118        assert_eq!(rl1, rl4);
119        assert_eq!(rl2, rl3);
120        assert_eq!(rl2, rl4);
121        assert_eq!(rl3, rl4);
122        assert_eq!(rl2, rl1);
123        assert_eq!(rl3, rl1);
124        assert_eq!(rl4, rl1);
125        assert_eq!(rl3, rl2);
126        assert_eq!(rl4, rl2);
127        assert_eq!(rl4, rl3);
128
129        assert!(!rl1.is_empty());
130        assert!(!rl2.is_empty());
131        assert!(!rl3.is_empty());
132        assert!(!rl4.is_empty());
133        assert_eq!(rl1.len(), 1);
134        assert_eq!(rl2.len(), 1);
135        assert_eq!(rl3.len(), 1);
136        assert_eq!(rl4.len(), 1);
137
138        assert_eq!(rl1[0], Resource::Arn(ap.clone()));
139        assert_eq!(rl2[0], Resource::Arn(ap));
140
141        let e = catch_unwind(|| {
142            println!("This will not print: {}", rl1[1]);
143        })
144        .unwrap_err();
145        assert_eq!(*e.downcast::<String>().unwrap(), "index out of bounds: the len is 1 but the index is 1");
146
147        assert_eq!(format!("{rl1}"), r#""arn:*:ec*:us-*-2:123?56789012:instance/*""#);
148        assert_eq!(
149            format!("{rl3}"),
150            indoc! { r#"
151            [
152                "arn:*:ec*:us-*-2:123?56789012:instance/*"
153            ]"# }
154        );
155    }
156
157    #[test_log::test]
158    fn check_bad() {
159        let e = Resource::from_str("arn:aws").unwrap_err();
160        assert_eq!(e.to_string(), "Invalid resource: arn:aws");
161    }
162
163    #[test_log::test]
164    fn check_derived() {
165        let r1a = Resource::from_str("arn:aws:ec2:us-east-2:123456789012:instance/*").unwrap();
166        let r1b = Resource::from_str("arn:aws:ec2:us-east-2:123456789012:instance/*").unwrap();
167        let r2 = Resource::Any;
168
169        assert_eq!(r1a, r1b);
170        assert_ne!(r1a, r2);
171        assert_eq!(r1a, r1a.clone());
172
173        assert_eq!(r1a.to_string(), "arn:aws:ec2:us-east-2:123456789012:instance/*");
174        assert_eq!(r2.to_string(), "*");
175    }
176}