scratchstack_aspen/
action.rs

1use {
2    crate::{eval::regex_from_glob, serutil::StringLikeList, AspenError},
3    log::debug,
4    std::{
5        fmt::{Display, Formatter, Result as FmtResult},
6        str::FromStr,
7    },
8};
9
10/// A list of actions. In JSON, this may be a string or an array of strings.
11pub type ActionList = StringLikeList<Action>;
12
13/// An action in an Aspen policy.
14///
15/// This can either be `Any` action (represented by the string `*`), or a service and an API pattern (`Specific`)
16/// in the form `service:api_pattern`. The API pattern may contain wildcard characters (`*` and `?`).
17#[derive(Clone, Debug)]
18pub enum Action {
19    /// Any action.
20    Any,
21
22    /// A specific action.
23    Specific(SpecificActionDetails),
24}
25
26#[derive(Clone, Debug, Eq, PartialEq)]
27pub struct SpecificActionDetails {
28    /// The service the action is for. This may not contain wildcards.
29    service: String,
30
31    /// The api pattern. This may contain wildcards.
32    api: String,
33}
34
35impl PartialEq for Action {
36    fn eq(&self, other: &Self) -> bool {
37        match (self, other) {
38            (Self::Any, Self::Any) => true,
39            (Self::Specific(my_details), Self::Specific(other_details)) => my_details == other_details,
40            _ => false,
41        }
42    }
43}
44
45impl Eq for Action {}
46
47impl Action {
48    /// Create a new [Action::Specific] action.
49    ///
50    /// # Errors
51    ///
52    /// An [AspenError::InvalidAction] error is returned in any of the following cases:
53    /// * `service` or `api` is empty.
54    /// * `service` contains non-ASCII alphanumeric characters, hyphen (`-`), or underscore (`_`).
55    /// * `service` begins or ends with a hyphen or underscore.
56    /// * `api` contains non-ASCII alphanumeric characters, hyphen (`-`), underscore (`_`), asterisk (`*`), or
57    ///    question mark (`?`).
58    /// * `api` begins or ends with a hyphen or underscore.
59    pub fn new<S: Into<String>, A: Into<String>>(service: S, api: A) -> Result<Self, AspenError> {
60        let service = service.into();
61        let api = api.into();
62
63        if service.is_empty() {
64            debug!("Action '{service}:{api}' has an empty service.");
65            return Err(AspenError::InvalidAction(format!("{service}:{api}")));
66        }
67
68        if api.is_empty() {
69            debug!("Action '{service}:{api}' has an empty API.");
70            return Err(AspenError::InvalidAction(format!("{service}:{api}")));
71        }
72
73        if !service.is_ascii() || !api.is_ascii() {
74            debug!("Action '{service}:{api}' is not ASCII.");
75            return Err(AspenError::InvalidAction(format!("{service}:{api}")));
76        }
77
78        for (i, c) in service.bytes().enumerate() {
79            if !c.is_ascii_alphanumeric() && !(i > 0 && i < service.len() - 1 && (c == b'-' || c == b'_')) {
80                debug!("Action '{service}:{api}' has an invalid service.");
81                return Err(AspenError::InvalidAction(format!("{service}:{api}")));
82            }
83        }
84
85        for (i, c) in api.bytes().enumerate() {
86            if !c.is_ascii_alphanumeric()
87                && c != b'*'
88                && c != b'?'
89                && !(i > 0 && i < api.len() - 1 && (c == b'-' || c == b'_'))
90            {
91                debug!("Action '{service}:{api}' has an invalid API.");
92                return Err(AspenError::InvalidAction(format!("{service}:{api}")));
93            }
94        }
95
96        Ok(Action::Specific(SpecificActionDetails {
97            service,
98            api,
99        }))
100    }
101
102    /// Returns true if this action is [Action::Any].
103    #[inline]
104    pub fn is_any(&self) -> bool {
105        matches!(self, Self::Any)
106    }
107
108    /// If the action is [Action::Specific], returns the service and action.
109    #[inline]
110    pub fn specific(&self) -> Option<(&str, &str)> {
111        match self {
112            Self::Any => None,
113            Self::Specific(details) => Some((&details.service, &details.api)),
114        }
115    }
116
117    /// Returns the service for this action or "*" if this action is [Action::Any].
118    #[inline]
119    pub fn service(&self) -> &str {
120        match self {
121            Self::Any => "*",
122            Self::Specific(SpecificActionDetails {
123                service,
124                ..
125            }) => service,
126        }
127    }
128
129    /// Returns the API for this action or "*" if this action is [Action::Any].
130    #[inline]
131    pub fn api(&self) -> &str {
132        match self {
133            Self::Any => "*",
134            Self::Specific(SpecificActionDetails {
135                api,
136                ..
137            }) => api,
138        }
139    }
140
141    /// Indicates whether this action matches the given service and action.
142    pub fn matches(&self, service: &str, api: &str) -> bool {
143        match self {
144            Self::Any => true,
145            Self::Specific(SpecificActionDetails {
146                service: self_service,
147                api: self_api,
148            }) => {
149                if self_service == service {
150                    regex_from_glob(self_api, false).is_match(api)
151                } else {
152                    false
153                }
154            }
155        }
156    }
157}
158
159impl FromStr for Action {
160    type Err = AspenError;
161    fn from_str(v: &str) -> Result<Self, Self::Err> {
162        if v == "*" {
163            return Ok(Self::Any);
164        }
165
166        let parts: Vec<&str> = v.split(':').collect();
167        if parts.len() != 2 {
168            return Err(AspenError::InvalidAction(v.to_string()));
169        }
170
171        let service = parts[0];
172        let api = parts[1];
173
174        Action::new(service, api)
175    }
176}
177
178impl Display for Action {
179    fn fmt(&self, f: &mut Formatter) -> FmtResult {
180        match self {
181            Self::Any => f.write_str("*"),
182            Self::Specific(details) => Display::fmt(details, f),
183        }
184    }
185}
186
187impl Display for SpecificActionDetails {
188    fn fmt(&self, f: &mut Formatter) -> FmtResult {
189        write!(f, "{}:{}", self.service, self.api)
190    }
191}
192
193#[cfg(test)]
194mod tests {
195    use {
196        crate::{Action, ActionList},
197        indoc::indoc,
198        pretty_assertions::{assert_eq, assert_ne},
199        std::{panic::catch_unwind, str::FromStr},
200    };
201
202    #[test_log::test]
203    fn test_eq() {
204        let a1a: ActionList = Action::new("s1", "a1").unwrap().into();
205        let a1b: ActionList = vec![Action::new("s1", "a1").unwrap()].into();
206        let a2a: ActionList = Action::new("s2", "a1").unwrap().into();
207        let a2b: ActionList = vec![Action::new("s2", "a1").unwrap()].into();
208        let a3a: ActionList = Action::new("s1", "a2").unwrap().into();
209        let a3b: ActionList = vec![Action::new("s1", "a2").unwrap()].into();
210        let a4a: ActionList = vec![].into();
211        let a4b: ActionList = vec![].into();
212
213        assert_eq!(a1a, a1a.clone());
214        assert_eq!(a1b, a1b.clone());
215        assert_eq!(a2a, a2a.clone());
216        assert_eq!(a2b, a2b.clone());
217        assert_eq!(a3a, a3a.clone());
218        assert_eq!(a3b, a3b.clone());
219        assert_eq!(a4a, a4a.clone());
220        assert_eq!(a4b, a4b.clone());
221
222        assert_eq!(a1a.len(), 1);
223        assert_eq!(a1b.len(), 1);
224        assert_eq!(a2a.len(), 1);
225        assert_eq!(a2b.len(), 1);
226        assert_eq!(a3a.len(), 1);
227        assert_eq!(a3b.len(), 1);
228        assert_eq!(a4a.len(), 0);
229        assert_eq!(a4b.len(), 0);
230
231        assert!(!a1a.is_empty());
232        assert!(!a1b.is_empty());
233        assert!(!a2a.is_empty());
234        assert!(!a2b.is_empty());
235        assert!(!a3a.is_empty());
236        assert!(!a3b.is_empty());
237        assert!(a4a.is_empty());
238        assert!(a4b.is_empty());
239
240        assert_eq!(a1a, a1b);
241        assert_eq!(a1b, a1a);
242        assert_eq!(a2a, a2b);
243        assert_eq!(a2b, a2a);
244        assert_eq!(a3a, a3b);
245        assert_eq!(a3b, a3a);
246        assert_eq!(a4a, a4b);
247        assert_eq!(a4b, a4a);
248
249        assert_ne!(a1a, a2a);
250        assert_ne!(a1a, a2b);
251        assert_ne!(a1a, a3a);
252        assert_ne!(a1a, a3b);
253        assert_ne!(a1a, a4a);
254        assert_ne!(a1a, a4b);
255        assert_ne!(a2a, a1a);
256        assert_ne!(a2b, a1a);
257        assert_ne!(a3a, a1a);
258        assert_ne!(a3b, a1a);
259        assert_ne!(a4a, a1a);
260        assert_ne!(a4b, a1a);
261
262        assert_ne!(a1b, a2a);
263        assert_ne!(a1b, a2b);
264        assert_ne!(a1b, a3a);
265        assert_ne!(a1b, a3b);
266        assert_ne!(a1b, a4a);
267        assert_ne!(a1b, a4b);
268        assert_ne!(a2a, a1b);
269        assert_ne!(a2b, a1b);
270        assert_ne!(a3a, a1b);
271        assert_ne!(a3b, a1b);
272        assert_ne!(a4a, a1b);
273        assert_ne!(a4b, a1b);
274
275        assert_ne!(a2a, a3a);
276        assert_ne!(a2a, a3b);
277        assert_ne!(a2a, a4a);
278        assert_ne!(a2a, a4b);
279        assert_ne!(a3a, a2a);
280        assert_ne!(a3b, a2a);
281        assert_ne!(a4a, a2a);
282        assert_ne!(a4b, a2a);
283
284        assert_ne!(a2b, a3a);
285        assert_ne!(a2b, a3b);
286        assert_ne!(a2b, a4a);
287        assert_ne!(a2b, a4b);
288        assert_ne!(a3a, a2b);
289        assert_ne!(a3b, a2b);
290        assert_ne!(a4a, a2b);
291        assert_ne!(a4b, a2b);
292
293        assert_ne!(a3a, a4a);
294        assert_ne!(a3a, a4b);
295        assert_ne!(a4a, a3a);
296        assert_ne!(a4b, a3a);
297
298        assert_ne!(a3b, a4a);
299        assert_ne!(a3b, a4b);
300        assert_ne!(a4a, a3b);
301        assert_ne!(a4b, a3b);
302
303        assert_eq!(Action::Any, Action::Any);
304    }
305
306    #[test_log::test]
307    fn test_from() {
308        let a1a: ActionList = vec![Action::new("s1", "a1").unwrap()].into();
309        let a1b: ActionList = Action::new("s1", "a1").unwrap().into();
310        let a2a: ActionList = vec![Action::Any].into();
311
312        assert_eq!(a1a, a1b);
313        assert_eq!(a1b, a1a);
314        assert_ne!(a1a, a2a);
315
316        assert_eq!(a1a[0], a1b[0]);
317
318        assert_eq!(
319            format!("{a1a}"),
320            indoc! {r#"
321            [
322                "s1:a1"
323            ]"#}
324        );
325        assert_eq!(format!("{a1b}"), r#""s1:a1""#);
326        assert_eq!(
327            format!("{a2a}"),
328            indoc! {r#"
329            [
330                "*"
331            ]"#}
332        );
333
334        assert_eq!(format!("{}", a2a[0]), "*");
335
336        let e = catch_unwind(|| {
337            println!("This will not be printed: {}", a1b[1]);
338        })
339        .unwrap_err();
340        assert_eq!(*e.downcast::<String>().unwrap(), "index out of bounds: the len is 1 but the index is 1");
341    }
342
343    #[test_log::test]
344    fn test_bad_strings() {
345        assert_eq!(Action::from_str("").unwrap_err().to_string(), "Invalid action: ");
346        assert_eq!(Action::from_str("ec2:").unwrap_err().to_string(), "Invalid action: ec2:");
347        assert_eq!(
348            Action::from_str(":DescribeInstances").unwrap_err().to_string(),
349            "Invalid action: :DescribeInstances"
350        );
351        assert_eq!(
352            Action::from_str("🦀:DescribeInstances").unwrap_err().to_string(),
353            "Invalid action: 🦀:DescribeInstances"
354        );
355        assert_eq!(Action::from_str("ec2:🦀").unwrap_err().to_string(), "Invalid action: ec2:🦀");
356        assert_eq!(
357            Action::from_str("-ec2:DescribeInstances").unwrap_err().to_string(),
358            "Invalid action: -ec2:DescribeInstances"
359        );
360        assert_eq!(
361            Action::from_str("_ec2:DescribeInstances").unwrap_err().to_string(),
362            "Invalid action: _ec2:DescribeInstances"
363        );
364        assert_eq!(
365            Action::from_str("ec2-:DescribeInstances").unwrap_err().to_string(),
366            "Invalid action: ec2-:DescribeInstances"
367        );
368        assert_eq!(
369            Action::from_str("ec2_:DescribeInstances").unwrap_err().to_string(),
370            "Invalid action: ec2_:DescribeInstances"
371        );
372        assert_eq!(
373            Action::from_str("ec2:-DescribeInstances").unwrap_err().to_string(),
374            "Invalid action: ec2:-DescribeInstances"
375        );
376        assert_eq!(
377            Action::from_str("ec2:_DescribeInstances").unwrap_err().to_string(),
378            "Invalid action: ec2:_DescribeInstances"
379        );
380        assert_eq!(
381            Action::from_str("ec2:DescribeInstances-").unwrap_err().to_string(),
382            "Invalid action: ec2:DescribeInstances-"
383        );
384        assert_eq!(
385            Action::from_str("ec2:DescribeInstances_").unwrap_err().to_string(),
386            "Invalid action: ec2:DescribeInstances_"
387        );
388
389        assert_eq!(Action::from_str("e_c-2:De-scribe_Instances").unwrap().service(), "e_c-2");
390        assert_eq!(Action::from_str("e_c-2:De-scribe_Instances").unwrap().api(), "De-scribe_Instances");
391        assert!(Action::from_str("e_c-2:De-scribe_Instances").unwrap().specific().is_some());
392        assert!(!Action::from_str("e_c-2:De-scribe_Instances").unwrap().is_any());
393        assert_eq!(Action::from_str("*").unwrap().service(), "*");
394        assert_eq!(Action::from_str("*").unwrap().api(), "*");
395        assert!(Action::from_str("*").unwrap().is_any());
396        assert!(Action::from_str("*").unwrap().specific().is_none());
397    }
398}