allow_me/
substituter.rs

1use crate::{Error, Request};
2
3/// Trait to extend [`Policy`](`crate::Policy`) variable rules resolution.
4pub trait Substituter {
5    /// The type of the context associated with the request.
6    type Context;
7
8    /// This method is called by [`Policy`](`crate::Policy`) on every [`Request`] for every variable identity rule.
9    fn visit_identity(
10        &self,
11        value: &str,
12        context: &Request<Self::Context>,
13    ) -> Result<String, Error>;
14
15    /// This method is called by [`Policy`](`crate::Policy`) on every [`Request`] for every variable operation rule.
16    fn visit_operation(
17        &self,
18        value: &str,
19        context: &Request<Self::Context>,
20    ) -> Result<String, Error>;
21
22    /// This method is called by [`Policy`](`crate::Policy`) on every [`Request`] for every variable resource rule.
23    fn visit_resource(
24        &self,
25        value: &str,
26        context: &Request<Self::Context>,
27    ) -> Result<String, Error>;
28}
29
30pub(crate) const ANY_VAR: &str = "{{any}}";
31pub(crate) const IDENTITY_VAR: &str = "{{identity}}";
32pub(crate) const OPERATION_VAR: &str = "{{operation}}";
33
34/// Default implementation of [`Substituter`]. It supports several useful variables:
35/// * `any` - replaced by input value from the Request.
36/// * `identity` - replaced by identity value from the Request.
37/// * `operation` - replaced by operation value from the Request.
38#[derive(Debug)]
39pub struct DefaultSubstituter;
40
41impl Substituter for DefaultSubstituter {
42    type Context = ();
43
44    fn visit_identity(
45        &self,
46        value: &str,
47        context: &Request<Self::Context>,
48    ) -> Result<String, Error> {
49        Ok(replace_identity(value, context))
50    }
51
52    fn visit_operation(
53        &self,
54        value: &str,
55        context: &Request<Self::Context>,
56    ) -> Result<String, Error> {
57        Ok(replace_operation(value, context))
58    }
59
60    fn visit_resource(
61        &self,
62        value: &str,
63        context: &Request<Self::Context>,
64    ) -> Result<String, Error> {
65        Ok(replace_resource(value, context))
66    }
67}
68
69fn replace_identity<RC>(value: &str, context: &Request<RC>) -> String {
70    let mut result = value.to_owned();
71    for variable in VariableIter::new(value) {
72        result = match variable {
73            ANY_VAR | IDENTITY_VAR => replace(&result, variable, context.identity()),
74            _ => result,
75        };
76    }
77    result
78}
79
80fn replace_operation<RC>(value: &str, context: &Request<RC>) -> String {
81    let mut result = value.to_owned();
82    for variable in VariableIter::new(value) {
83        result = match variable {
84            ANY_VAR | OPERATION_VAR => replace(&result, variable, context.operation()),
85            IDENTITY_VAR => replace(&result, variable, context.identity()),
86            _ => result,
87        };
88    }
89    result
90}
91
92fn replace_resource<RC>(value: &str, context: &Request<RC>) -> String {
93    let mut result = value.to_owned();
94    for variable in VariableIter::new(value) {
95        result = match variable {
96            ANY_VAR => replace(&result, variable, context.resource()),
97            IDENTITY_VAR => replace(&result, variable, context.identity()),
98            OPERATION_VAR => replace(&result, variable, context.operation()),
99            _ => result,
100        };
101    }
102    result
103}
104
105fn replace(value: &str, variable: &str, substitution: &str) -> String {
106    value.replace(variable, substitution)
107}
108
109/// A simple iterator that returns all occurrences
110/// of variable substrings like `{{var_name}}` in the
111/// provided string value. Can be used in your custom
112/// [`Substituter`] implementation.
113#[derive(Debug)]
114pub struct VariableIter<'a> {
115    value: &'a str,
116    index: usize,
117}
118
119impl<'a> VariableIter<'a> {
120    pub fn new(value: &'a str) -> Self {
121        Self { value, index: 0 }
122    }
123}
124
125impl<'a> Iterator for VariableIter<'a> {
126    type Item = &'a str;
127
128    fn next(&mut self) -> Option<Self::Item> {
129        let value = &self.value[self.index..];
130        if let Some(start) = value.find("{{") {
131            if let Some(end) = value.find("}}") {
132                if start < end {
133                    self.index = self.index + end + 2;
134                    return Some(&value[start..end + 2]);
135                }
136            }
137        }
138        None
139    }
140}
141
142#[cfg(test)]
143mod tests {
144    use proptest::prelude::*;
145    use test_case::test_case;
146
147    use super::*;
148
149    #[test_case("{{any}}", 
150        "some_identity", 
151        "some_operation", 
152        "some_resource", 
153        "some_identity"; 
154        "any var substitution")]
155    #[test_case("{{identity}}", 
156        "some_identity", 
157        "some_operation", 
158        "some_resource", 
159        "some_identity"; 
160        "identity var substitution")]
161    fn visit_identity_test(
162        input: &str,
163        identity: &str,
164        operation: &str,
165        resource: &str,
166        expected: &str,
167    ) {
168        let request = Request::new(identity, operation, resource).unwrap();
169
170        assert_eq!(
171            expected,
172            DefaultSubstituter.visit_identity(input, &request).unwrap()
173        );
174    }
175
176    #[test_case("{{any}}", 
177        "some_identity", 
178        "some_operation", 
179        "some_resource", 
180        "some_operation"; 
181        "any var substitution")]
182    #[test_case("{{operation}}", 
183        "some_identity", 
184        "some_operation", 
185        "some_resource", 
186        "some_operation"; 
187        "operation var substitution")]
188    #[test_case("{{identity}}", 
189        "some_identity", 
190        "some_operation", 
191        "some_resource", 
192        "some_identity"; 
193        "identity var substitution")]
194    #[test_case("prefix-{{identity}}-suffix", 
195        "some_identity", 
196        "some_operation", 
197        "some_resource", 
198        "prefix-some_identity-suffix"; 
199        "contains identity var substitution")]
200    #[test_case("prefix-{{identity}}-contains-{{identity}}-suffix", 
201        "some_identity", 
202        "some_operation", 
203        "some_resource", 
204        "prefix-some_identity-contains-some_identity-suffix"; 
205        "multiple vars substitution")]
206    fn visit_operation_test(
207        input: &str,
208        identity: &str,
209        operation: &str,
210        resource: &str,
211        expected: &str,
212    ) {
213        let request = Request::new(identity, operation, resource).unwrap();
214
215        assert_eq!(
216            expected,
217            DefaultSubstituter.visit_operation(input, &request).unwrap()
218        );
219    }
220
221    #[test_case("{{any}}", 
222        "some_identity", 
223        "some_operation", 
224        "some_resource", 
225        "some_resource"; 
226        "any var substitution")]
227    #[test_case("{{operation}}", 
228        "some_identity", 
229        "some_operation", 
230        "some_resource", 
231        "some_operation"; 
232        "operation var substitution")]
233    #[test_case("{{identity}}", 
234        "some_identity", 
235        "some_operation", 
236        "some_resource", 
237        "some_identity"; 
238        "identity var substitution")]
239    #[test_case("home/{{identity}}/middle/{{operation}}/last", 
240        "some_identity", 
241        "some_operation", 
242        "some_resource", 
243        "home/some_identity/middle/some_operation/last"; 
244        "contains multiple vars substitution")]
245    fn visit_resource_test(
246        input: &str,
247        identity: &str,
248        operation: &str,
249        resource: &str,
250        expected: &str,
251    ) {
252        let request = Request::new(identity, operation, resource).unwrap();
253
254        assert_eq!(
255            expected,
256            DefaultSubstituter.visit_resource(input, &request).unwrap()
257        );
258    }
259
260    proptest! {
261        #[test]
262        fn iterator_does_not_crash(value in "[a-z\\{\\}]+") {
263            let _ = VariableIter::new(&value).collect::<Vec<_>>();
264        }
265    }
266}