scratchstack_aspen/
eval.rs1use {
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#[derive(Builder, Clone, Debug, Eq, PartialEq)]
14pub struct Context {
15 #[builder(setter(into))]
17 api: String,
18
19 actor: Principal,
21
22 #[builder(default)]
24 resources: Vec<Arn>,
25
26 session_data: SessionData,
28
29 #[builder(setter(into))]
31 service: String,
32}
33
34impl Context {
35 pub fn builder() -> ContextBuilder {
37 ContextBuilder::default()
38 }
39
40 #[inline]
42 pub fn api(&self) -> &str {
43 &self.api
44 }
45
46 #[inline]
48 pub fn actor(&self) -> &Principal {
49 &self.actor
50 }
51
52 #[inline]
54 pub fn resources(&self) -> &Vec<Arn> {
55 &self.resources
56 }
57
58 #[inline]
60 pub fn session_data(&self) -> &SessionData {
61 &self.session_data
62 }
63
64 #[inline]
66 pub fn service(&self) -> &str {
67 &self.service
68 }
69
70 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 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(®ex::escape("*")),
129 "$" => pattern.push_str(®ex::escape("$")),
130 "?" => pattern.push_str(®ex::escape("?")),
131 var => {
132 if let Some(value) = self.session_data.get(var) {
133 pattern.push_str(®ex::escape(&value.as_variable_value()));
134 }
135 }
136 }
137 }
138 '*' => pattern.push_str(".*"),
139 '?' => pattern.push('.'),
140 _ => pattern.push_str(®ex::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 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
198pub(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#[derive(Debug, Eq, PartialEq)]
223pub enum Decision {
224 Allow,
226
227 Deny,
229
230 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 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}