1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
use {
    super::variant::Variant,
    crate::{serutil::StringLikeList, AspenError, Context, PolicyVersion},
    scratchstack_aws_principal::SessionValue,
};

/// String operation names.
const STRING_DISPLAY_NAMES: [&str; 12] = [
    "StringEquals",
    "StringEqualsIfExists",
    "StringNotEquals",
    "StringNotEqualsIfExists",
    "StringEqualsIgnoreCase",
    "StringEqualsIgnoreCaseIfExists",
    "StringNotEqualsIgnoreCase",
    "StringNotEqualsIgnoreCaseIfExists",
    "StringLike",
    "StringLikeIfExists",
    "StringNotLike",
    "StringNotLikeIfExists",
];

#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
#[repr(u8)]
pub enum StringCmp {
    Equals = 0,
    EqualsIgnoreCase = 4,
    Like = 8,
}

impl StringCmp {
    pub(super) fn display_name(&self, variant: &Variant) -> &'static str {
        STRING_DISPLAY_NAMES[*self as usize | variant.as_usize()]
    }
}

pub(super) fn string_match(
    context: &Context,
    pv: PolicyVersion,
    allowed: &StringLikeList<String>,
    value: &SessionValue,
    cmp: StringCmp,
    variant: Variant,
) -> Result<bool, AspenError> {
    match value {
        SessionValue::Null => Ok(variant.if_exists()),
        SessionValue::String(value) => {
            match cmp {
                StringCmp::Like => {
                    // Convert each entry to a glob pattern.
                    for el in allowed.iter() {
                        let el = context.matcher(el, pv)?.build().unwrap();
                        let is_match = el.is_match(value);
                        log::trace!("regex={:?} value={:?} is_match={:?}", el, value, is_match);
                        // If it is a match and we're not negated, or it is not a match and we are negated, return true.
                        if is_match != variant.negated() {
                            return Ok(true);
                        }
                    }

                    Ok(false)
                }
                StringCmp::Equals => string_equal_match(context, allowed, value, variant, |a: &str, b: &str| a == b),
                StringCmp::EqualsIgnoreCase => {
                    string_equal_match(context, allowed, value, variant, |a: &str, b: &str| {
                        a.to_lowercase() == b.to_lowercase()
                    })
                }
            }
        }
        _ => Ok(false),
    }
}

fn string_equal_match<F: Fn(&str, &str) -> bool>(
    context: &Context,
    allowed: &StringLikeList<String>,
    value: &str,
    variant: Variant,
    fn_op: F,
) -> Result<bool, AspenError> {
    for el in allowed.iter() {
        let el = context.subst_vars_plain(el)?;
        let is_match = fn_op(value, &el);

        // If it is a match and we're not negated, or it is not a match and we are negated, return true.
        if is_match != variant.negated() {
            return Ok(true);
        }
    }
    Ok(false)
}

#[cfg(test)]
mod tests {
    use {super::StringCmp, pretty_assertions::assert_eq};

    #[test_log::test]
    fn test_clone() {
        assert_eq!(StringCmp::Equals.clone(), StringCmp::Equals);
        assert_eq!(StringCmp::EqualsIgnoreCase.clone(), StringCmp::EqualsIgnoreCase);
        assert_eq!(StringCmp::Like.clone(), StringCmp::Like);
    }
}