dl_authorize/
statement.rs

1use crate::effect::Effect;
2use crate::request::Request;
3use ic_cdk::export::Principal;
4use serde::{Deserialize, Serialize};
5
6/// Enum representing the identity of a user
7#[derive(Serialize, Deserialize)]
8pub enum Identity {
9    Principal(Principal),
10}
11
12#[derive(Serialize, Deserialize)]
13pub enum StatementIdentity {
14    Identity(Identity),
15    Any,
16}
17
18impl StatementIdentity {
19    pub fn matches(&self, v: &Identity) -> bool {
20        match (self, v) {
21            (Self::Any, _) => true,
22            (Self::Identity(Identity::Principal(p1)), Identity::Principal(p2)) => p1 == p2,
23        }
24    }
25}
26
27#[derive(Serialize, Deserialize, Debug)]
28pub enum RequestResource {
29    Resource(String),
30    Nested {
31        node: String,
32        next: Option<Box<RequestResource>>,
33    },
34}
35
36#[derive(Serialize, Deserialize, Debug)]
37pub enum StatementResource {
38    Resource(String),
39    Nested {
40        node: String,
41        next: Vec<StatementResource>,
42    },
43}
44
45impl StatementResource {
46    pub fn get_node_name(&self) -> &String {
47        match self {
48            StatementResource::Resource(r) => r,
49            StatementResource::Nested { node, next } => node,
50        }
51    }
52
53    pub fn add_nested(mut self, nested: StatementResource) -> Self {
54        match &mut self {
55            StatementResource::Resource(node) => StatementResource::Nested {
56                node: node.clone(),
57                next: vec![nested],
58            },
59            StatementResource::Nested { node, next } => {
60                next.push(nested);
61                self
62            }
63        }
64    }
65
66    pub fn add_nested_resources(mut self, nested: Vec<StatementResource>) -> Self {
67        if nested.is_empty() {
68            return self;
69        }
70        match &mut self {
71            StatementResource::Resource(node) => StatementResource::Nested {
72                node: node.clone(),
73                next: nested,
74            },
75            StatementResource::Nested { node, next } => {
76                next.extend(nested);
77                self
78            }
79        }
80    }
81
82    pub fn matches(&self, request: &RequestResource) -> bool {
83        match (self, request) {
84            (Self::Resource(v), RequestResource::Nested { node, next }) => {
85                return v == node && next.is_none();
86            }
87            (Self::Resource(left), RequestResource::Resource(right)) => left == right,
88            (Self::Nested { node, next }, RequestResource::Resource(r)) => {
89                return next.is_empty() && node == r;
90            }
91            (
92                Self::Nested {
93                    node: node_left,
94                    next: next_left,
95                },
96                RequestResource::Nested {
97                    node: node_right,
98                    next: next_right,
99                },
100            ) => {
101                if node_left != node_right {
102                    return false;
103                }
104                for left in next_left {
105                    for right in next_right {
106                        if left.matches(right) {
107                            return true;
108                        }
109                    }
110                }
111                false
112            }
113            _ => false,
114        }
115    }
116}
117
118#[derive(Serialize, Deserialize)]
119pub struct Statement {
120    effect: Effect,
121    identities: Vec<StatementIdentity>,
122    operations: Vec<String>,
123    resources: Vec<StatementResource>,
124}
125
126impl Statement {
127    pub fn new(
128        effect: Effect,
129        identities: Vec<StatementIdentity>,
130        operations: Vec<String>,
131        resources: Vec<StatementResource>,
132    ) -> Self {
133        Statement {
134            effect,
135            identities,
136            operations,
137            resources,
138        }
139    }
140
141    pub fn get_effect(&self, request: &Request) -> Option<Effect> {
142        let identity = Identity::Principal(request.caller().clone());
143
144        if !self.operations.contains(&request.action()) {
145            return None;
146        }
147
148        let identity_match_maybe = self.identities.iter().find(|v| v.matches(&identity));
149
150        if identity_match_maybe.is_none() {
151            return None;
152        }
153
154        let resource_match_maybe = self
155            .resources
156            .iter()
157            .find(|v| v.matches(&request.resource()));
158
159        match (identity_match_maybe, resource_match_maybe) {
160            (Some(_), Some(_)) => Some(self.effect.clone()),
161            _ => None,
162        }
163    }
164}
165
166#[cfg(test)]
167mod resource_statement_tests {
168    use crate::request::RequestResourceBuilder;
169    use crate::statement::{RequestResource, StatementResource};
170
171    #[test]
172    fn it_builds_a_request_resource() {
173        let request_resource = RequestResourceBuilder::new("foo")
174            .add("bar")
175            .add("baz")
176            .build();
177
178        let mut expected = vec!["foo", "bar", "baz"];
179
180        fn c(l: Box<RequestResource>, mut expected: Vec<&str>) {
181            match *l {
182                RequestResource::Resource(v) => {
183                    assert!(v == expected.remove(0))
184                }
185                RequestResource::Nested { node, next } => {
186                    assert_eq!(node, expected.remove(0));
187                    c(next.unwrap(), expected)
188                }
189            }
190        }
191
192        c(Box::new(request_resource), expected);
193    }
194
195    #[test]
196    fn it_builds_a_statement_resource() {
197        let v = StatementResource::Resource("Foo".to_string())
198            .add_nested(StatementResource::Resource("Bar".to_string()))
199            .add_nested(
200                StatementResource::Resource("Baz".to_string()).add_nested_resources(vec![
201                    StatementResource::Resource("Fizz".to_string()),
202                    StatementResource::Resource("Fuzz".to_string()),
203                ]),
204            );
205        println!("{:?}", v);
206    }
207
208    #[test]
209    fn it_matches_a_request_to_a_statement() {
210        let statement = StatementResource::Resource("Foo".to_string()).add_nested(
211            StatementResource::Resource("Bar".to_string())
212                .add_nested(StatementResource::Resource("Baz".to_string())),
213        );
214        assert!(statement.matches(
215            &RequestResourceBuilder::new("Foo")
216                .add("Bar")
217                .add("Baz")
218                .build()
219        ));
220        assert!(!statement.matches(
221            &RequestResourceBuilder::new("Foo")
222                .add("Bar")
223                .add("Fizz")
224                .build()
225        ));
226    }
227
228    #[test]
229    fn it_matches_a_request_to_a_nested_statement() {
230        let statement = StatementResource::Resource("Foo".to_string()).add_nested(
231            StatementResource::Resource("Bar".to_string())
232                .add_nested(StatementResource::Resource("Baz".to_string()))
233                .add_nested(StatementResource::Resource("Fizz".to_string())),
234        );
235        assert!(statement.matches(
236            &RequestResourceBuilder::new("Foo")
237                .add("Bar")
238                .add("Baz")
239                .build()
240        ));
241        assert!(statement.matches(
242            &RequestResourceBuilder::new("Foo")
243                .add("Bar")
244                .add("Fizz")
245                .build()
246        ));
247    }
248
249    #[test]
250    fn it_matches_a_request_to_a_double_nexted_statement() {
251        let statement = StatementResource::Resource("Foo".to_string()).add_nested(
252            StatementResource::Resource("Bar".to_string())
253                .add_nested(StatementResource::Resource("Baz".to_string()))
254                .add_nested(
255                    StatementResource::Resource("Fizz".to_string())
256                        .add_nested(StatementResource::Resource("Buzz".to_string())),
257                ),
258        );
259        assert!(statement.matches(
260            &RequestResourceBuilder::new("Foo")
261                .add("Bar")
262                .add("Baz")
263                .build()
264        ));
265        assert!(!statement.matches(
266            &RequestResourceBuilder::new("Foo")
267                .add("Bar")
268                .add("Fizz")
269                .build()
270        ));
271        assert!(statement.matches(
272            &RequestResourceBuilder::new("Foo")
273                .add("Bar")
274                .add("Fizz")
275                .add("Buzz")
276                .build()
277        ));
278    }
279}