1use crate::effect::Effect;
2use crate::request::Request;
3use ic_cdk::export::Principal;
4use serde::{Deserialize, Serialize};
5
6#[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}