scratchstack_aspen/condition/
string.rs

1use {
2    super::variant::Variant,
3    crate::{serutil::StringLikeList, AspenError, Context, PolicyVersion},
4    scratchstack_aws_principal::SessionValue,
5};
6
7/// String operation names.
8const STRING_DISPLAY_NAMES: [&str; 12] = [
9    "StringEquals",
10    "StringEqualsIfExists",
11    "StringNotEquals",
12    "StringNotEqualsIfExists",
13    "StringEqualsIgnoreCase",
14    "StringEqualsIgnoreCaseIfExists",
15    "StringNotEqualsIgnoreCase",
16    "StringNotEqualsIgnoreCaseIfExists",
17    "StringLike",
18    "StringLikeIfExists",
19    "StringNotLike",
20    "StringNotLikeIfExists",
21];
22
23#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
24#[repr(u8)]
25pub enum StringCmp {
26    Equals = 0,
27    EqualsIgnoreCase = 4,
28    Like = 8,
29}
30
31impl StringCmp {
32    pub(super) fn display_name(&self, variant: &Variant) -> &'static str {
33        STRING_DISPLAY_NAMES[*self as usize | variant.as_usize()]
34    }
35}
36
37pub(super) fn string_match(
38    context: &Context,
39    pv: PolicyVersion,
40    allowed: &StringLikeList<String>,
41    value: &SessionValue,
42    cmp: StringCmp,
43    variant: Variant,
44) -> Result<bool, AspenError> {
45    match value {
46        SessionValue::Null => Ok(variant.if_exists()),
47        SessionValue::String(value) => {
48            match cmp {
49                StringCmp::Like => {
50                    // Convert each entry to a glob pattern.
51                    for el in allowed.iter() {
52                        let el = context.matcher(el, pv, false)?;
53                        let is_match = el.is_match(value);
54                        log::trace!("regex={:?} value={:?} is_match={:?}", el, value, is_match);
55                        // If it is a match and we're not negated, or it is not a match and we are negated, return true.
56                        if is_match != variant.negated() {
57                            return Ok(true);
58                        }
59                    }
60
61                    Ok(false)
62                }
63                StringCmp::Equals => string_equal_match(context, allowed, value, variant, |a: &str, b: &str| a == b),
64                StringCmp::EqualsIgnoreCase => {
65                    string_equal_match(context, allowed, value, variant, |a: &str, b: &str| {
66                        a.to_lowercase() == b.to_lowercase()
67                    })
68                }
69            }
70        }
71        _ => Ok(false),
72    }
73}
74
75fn string_equal_match<F: Fn(&str, &str) -> bool>(
76    context: &Context,
77    allowed: &StringLikeList<String>,
78    value: &str,
79    variant: Variant,
80    fn_op: F,
81) -> Result<bool, AspenError> {
82    for el in allowed.iter() {
83        let el = context.subst_vars_plain(el)?;
84        let is_match = fn_op(value, &el);
85
86        // If it is a match and we're not negated, or it is not a match and we are negated, return true.
87        if is_match != variant.negated() {
88            return Ok(true);
89        }
90    }
91    Ok(false)
92}
93
94#[cfg(test)]
95mod tests {
96    use {super::StringCmp, pretty_assertions::assert_eq};
97
98    #[test_log::test]
99    fn test_clone() {
100        assert_eq!(StringCmp::Equals.clone(), StringCmp::Equals);
101        assert_eq!(StringCmp::EqualsIgnoreCase.clone(), StringCmp::EqualsIgnoreCase);
102        assert_eq!(StringCmp::Like.clone(), StringCmp::Like);
103    }
104}