scratchstack_aspen/
eval.rs

1use {
2    crate::{AspenError, PolicyVersion},
3    derive_builder::Builder,
4    regex::{Regex, RegexBuilder},
5    scratchstack_arn::Arn,
6    scratchstack_aws_principal::{Principal, SessionData},
7    std::fmt::{Display, Formatter, Result as FmtResult},
8};
9
10/// The request context used when evaluating an Aspen policy.
11///
12/// Context structures are immutable.
13#[derive(Builder, Clone, Debug, Eq, PartialEq)]
14pub struct Context {
15    /// The API being invoked.
16    #[builder(setter(into))]
17    api: String,
18
19    /// The [Principal] actor making the request.
20    actor: Principal,
21
22    /// The resources associated with the request.
23    #[builder(default)]
24    resources: Vec<Arn>,
25
26    /// The session data associated with the request.
27    session_data: SessionData,
28
29    /// The service being invoked.
30    #[builder(setter(into))]
31    service: String,
32}
33
34impl Context {
35    /// Returns a new [ContextBuilder] for building a [Context].
36    pub fn builder() -> ContextBuilder {
37        ContextBuilder::default()
38    }
39
40    /// Returns the API being invoked.
41    #[inline]
42    pub fn api(&self) -> &str {
43        &self.api
44    }
45
46    /// Returns the [Principal] actor making the request.
47    #[inline]
48    pub fn actor(&self) -> &Principal {
49        &self.actor
50    }
51
52    /// Returns the resources associated with the request.
53    #[inline]
54    pub fn resources(&self) -> &Vec<Arn> {
55        &self.resources
56    }
57
58    /// Returrns the session data associated with the request.
59    #[inline]
60    pub fn session_data(&self) -> &SessionData {
61        &self.session_data
62    }
63
64    /// Returns the service being invoked.
65    #[inline]
66    pub fn service(&self) -> &str {
67        &self.service
68    }
69
70    /// Creates a [Regex] from the given string pattern and policy version.
71    ///
72    /// If `case_insensitive` is `true`, the returned [Regex] will be case insensitive.
73    ///
74    /// Wildcards are converted to their regular expression equivalents. If the policy version is
75    /// [PolicyVersion::V2012_10_17] or later, variables are substituted and regex-escaped as necessary. The special
76    /// variables `${*}`, `${$}`, and `${?}` are converted to literal `*`, `$`, and `?` characters, respectively, then
77    /// regex-escaped.
78    ///
79    /// # Errors
80    ///
81    /// If the string contains a malformed variable reference and [PolicyVersion::V2012_10_17] or later is used,
82    /// [AspenError::InvalidSubstitution] is returned.
83    pub fn matcher<T: AsRef<str>>(&self, s: T, pv: PolicyVersion, case_insensitive: bool) -> Result<Regex, AspenError> {
84        match pv {
85            PolicyVersion::None | PolicyVersion::V2008_10_17 => Ok(regex_from_glob(s.as_ref(), case_insensitive)),
86            PolicyVersion::V2012_10_17 => self.subst_vars(s.as_ref(), case_insensitive),
87        }
88    }
89
90    /// Creates a [Regex] from the given string pattern.
91    ///
92    /// If `case_insensitive` is `true`, the returned [Regex] will be case insensitive.
93    ///
94    /// Wildcards are converted to their regular expression equivalents. Variables are substituted and regex-escaped
95    /// as necessary. The special variables `${*}`, `${$}`, and `${?}` are converted to literal `*`, `$`, and `?`
96    /// characters, respectively, then regex-escaped.
97    ///
98    /// # Errors
99    ///
100    /// If the string contains a malformed variable reference and [PolicyVersion::V2012_10_17] or later is used,
101    /// [AspenError::InvalidSubstitution] is returned.
102    fn subst_vars(&self, s: &str, case_insensitive: bool) -> Result<Regex, AspenError> {
103        let mut i = s.chars();
104        let mut pattern = String::with_capacity(s.len() + 2);
105
106        pattern.push('^');
107
108        while let Some(c) = i.next() {
109            match c {
110                '$' => {
111                    let c = i.next().ok_or_else(|| AspenError::InvalidSubstitution(s.to_string()))?;
112                    if c != '{' {
113                        return Err(AspenError::InvalidSubstitution(s.to_string()));
114                    }
115
116                    let mut var = String::new();
117                    loop {
118                        let c = i.next().ok_or_else(|| AspenError::InvalidSubstitution(s.to_string()))?;
119
120                        if c == '}' {
121                            break;
122                        }
123
124                        var.push(c);
125                    }
126
127                    match var.as_str() {
128                        "*" => pattern.push_str(&regex::escape("*")),
129                        "$" => pattern.push_str(&regex::escape("$")),
130                        "?" => pattern.push_str(&regex::escape("?")),
131                        var => {
132                            if let Some(value) = self.session_data.get(var) {
133                                pattern.push_str(&regex::escape(&value.as_variable_value()));
134                            }
135                        }
136                    }
137                }
138                '*' => pattern.push_str(".*"),
139                '?' => pattern.push('.'),
140                _ => pattern.push_str(&regex::escape(&String::from(c))),
141            }
142        }
143
144        pattern.push('$');
145        Ok(RegexBuilder::new(&pattern)
146            .case_insensitive(case_insensitive)
147            .build()
148            .expect("regex builds should not fail"))
149    }
150
151    /// Substitutes variables from the given string, returning the resulting string.
152    ///
153    /// # Errors
154    ///
155    /// If the string contains a malformed variable reference and [PolicyVersion::V2012_10_17] or later is used,
156    /// [AspenError::InvalidSubstitution] is returned.
157    pub fn subst_vars_plain(&self, s: &str) -> Result<String, AspenError> {
158        let mut i = s.chars();
159        let mut result = String::new();
160
161        while let Some(c) = i.next() {
162            match c {
163                '$' => {
164                    let c = i.next().ok_or_else(|| AspenError::InvalidSubstitution(s.to_string()))?;
165                    if c != '{' {
166                        return Err(AspenError::InvalidSubstitution(s.to_string()));
167                    }
168
169                    let mut var = String::new();
170                    loop {
171                        let c = i.next().ok_or_else(|| AspenError::InvalidSubstitution(s.to_string()))?;
172                        if c == '}' {
173                            break;
174                        }
175
176                        var.push(c);
177                    }
178
179                    match var.as_str() {
180                        "*" => result.push('*'),
181                        "$" => result.push('$'),
182                        "?" => result.push('?'),
183                        var => {
184                            if let Some(value) = self.session_data.get(var) {
185                                result.push_str(&value.as_variable_value());
186                            }
187                        }
188                    }
189                }
190                _ => result.push(c),
191            }
192        }
193
194        Ok(result)
195    }
196}
197
198/// Creates a [Regex] from the given string pattern.
199///
200/// If `case_insensitive` is `true`, the returned [Regex] will be case insensitive.
201///
202/// Wildcards are converted to their regular expression equivalents. Variables are _not_ substituted here.
203pub(crate) fn regex_from_glob(s: &str, case_insensitive: bool) -> Regex {
204    let mut pattern = String::with_capacity(2 + s.len());
205    pattern.push('^');
206
207    for c in s.chars() {
208        match c {
209            '*' => pattern.push_str(".*"),
210            '?' => pattern.push('.'),
211            _ => {
212                let escaped: String = regex::escape(&String::from(c));
213                pattern.push_str(&escaped);
214            }
215        }
216    }
217    pattern.push('$');
218    RegexBuilder::new(&pattern).case_insensitive(case_insensitive).build().expect("regex builds should not fail")
219}
220
221/// The outcome of a policy evaluation.
222#[derive(Debug, Eq, PartialEq)]
223pub enum Decision {
224    /// Allow the request if no other statements or policies deny it.
225    Allow,
226
227    /// Deny the request unconditionally.
228    Deny,
229
230    /// Deny the request if no other statements or policies allow it.
231    DefaultDeny,
232}
233
234impl Display for Decision {
235    fn fmt(&self, f: &mut Formatter) -> FmtResult {
236        write!(
237            f,
238            "{}",
239            match self {
240                Decision::Allow => "Allow",
241                Decision::Deny => "Deny",
242                Decision::DefaultDeny => "DefaultDeny",
243            }
244        )
245    }
246}
247
248#[cfg(test)]
249mod test {
250    use {
251        crate::{Context, Decision},
252        scratchstack_aws_principal::{Principal, PrincipalIdentity, SessionData, User},
253    };
254
255    #[test_log::test]
256    fn test_context_derived() {
257        let actor =
258            Principal::from(vec![PrincipalIdentity::from(User::new("aws", "123456789012", "/", "user").unwrap())]);
259        let c1 = Context::builder()
260            .api("RunInstances")
261            .actor(actor)
262            .session_data(SessionData::default())
263            .service("ec2")
264            .build()
265            .unwrap();
266        assert_eq!(c1, c1.clone());
267
268        // Make sure we can debug print this.
269        let _ = format!("{c1:?}");
270    }
271
272    #[test_log::test]
273    fn test_decision_debug_display() {
274        assert_eq!(format!("{:?}", Decision::Allow), "Allow");
275        assert_eq!(format!("{:?}", Decision::Deny), "Deny");
276        assert_eq!(format!("{:?}", Decision::DefaultDeny), "DefaultDeny");
277
278        assert_eq!(format!("{}", Decision::Allow), "Allow");
279        assert_eq!(format!("{}", Decision::Deny), "Deny");
280        assert_eq!(format!("{}", Decision::DefaultDeny), "DefaultDeny");
281    }
282}