covert_types/
policy.rs

1use std::str::FromStr;
2
3use regex::Regex;
4use serde::{Deserialize, Serialize};
5
6use crate::{error::ApiError, request::Operation};
7
8// Policy is used to represent the policy specified by an ACL configuration.
9#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
10pub struct Policy {
11    pub name: String,
12    pub paths: Vec<PathPolicy>,
13    pub namespace_id: String,
14}
15
16impl Policy {
17    #[must_use]
18    pub fn new(name: String, paths: Vec<PathPolicy>, namespace_id: String) -> Self {
19        Self {
20            name,
21            paths,
22            namespace_id,
23        }
24    }
25
26    #[must_use]
27    pub fn name(&self) -> &str {
28        &self.name
29    }
30
31    #[must_use]
32    pub fn is_authorized(&self, path: &str, operations: &[Operation]) -> bool {
33        self.paths
34            .iter()
35            .any(|path_policy| path_policy.is_authorized(path, operations))
36    }
37
38    #[must_use]
39    pub fn batch_is_authorized(policies: &[Policy], derived_policies: &[Policy]) -> bool {
40        let mut derived_policies = derived_policies
41            .iter()
42            // No need to check policies that have the same name
43            .filter(|p| !policies.iter().any(|p2| p2.name == p.name));
44
45        derived_policies.all(|derived_policy| {
46            derived_policy.paths.iter().all(|derived_policy_path| {
47                // Verify that path for policy is allowed by any of the existing policies
48                policies.iter().any(|policy| {
49                    policy.is_authorized(&derived_policy_path.path, &derived_policy_path.operations)
50                })
51            })
52        })
53    }
54}
55
56#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, sqlx::FromRow)]
57pub struct PathPolicy {
58    pub path: String,
59    // TODO: rename to capabilities
60    pub operations: Vec<Operation>,
61}
62
63lazy_static::lazy_static! {
64    static ref HCL_POLICY_REGEX: Regex = Regex::new(r#"path"(.+)"\{capabilities=\[(.+)\]\}"#).expect("a valid regex");
65
66    static ref HCL_POLICY_RULE_REGEX: Regex = Regex::new(r"(?m)path[^\}]+\}").expect("a valid regex");
67}
68
69impl PathPolicy {
70    #[must_use]
71    pub fn new(path: String, operations: Vec<Operation>) -> Self {
72        Self { path, operations }
73    }
74
75    #[must_use]
76    pub fn path(&self) -> &str {
77        &self.path
78    }
79
80    #[must_use]
81    pub fn operations(&self) -> &[Operation] {
82        &self.operations
83    }
84
85    /// Parse a raw string into a list of policies.
86    ///
87    /// # Errors
88    ///
89    /// Returns an error if the string is not in a valid policy format.
90    pub fn parse(s: &str) -> Result<Vec<Self>, ApiError> {
91        // Remove comments
92        let mut s = s
93            .lines()
94            .filter(|line| !line.trim_start().starts_with('#'))
95            .collect::<String>();
96        s.retain(|c| !c.is_whitespace());
97
98        let s = s.replace('\n', "");
99
100        let rules = HCL_POLICY_RULE_REGEX.captures_iter(&s);
101        let mut policies = vec![];
102        for rule in rules {
103            let ma = rule.get(0).map(|m| m.range()).map(|range| &s[range]);
104            if let Some(m) = ma {
105                let caps = HCL_POLICY_REGEX
106                    .captures(m)
107                    .ok_or_else(ApiError::bad_request)?;
108                let path = caps.get(1).ok_or_else(ApiError::bad_request)?.as_str();
109                let operations = caps
110                    .get(2)
111                    .ok_or_else(ApiError::bad_request)?
112                    .as_str()
113                    .split(',')
114                    .map(|c| c.chars().filter(|c| c.is_alphabetic()).collect::<String>())
115                    .map(|s| Operation::from_str(&s))
116                    .collect::<Result<Vec<_>, _>>()?;
117                policies.push(PathPolicy {
118                    path: path.into(),
119                    operations,
120                });
121            }
122        }
123
124        Ok(policies)
125    }
126
127    fn is_authorized(&self, path: &str, operations: &[Operation]) -> bool {
128        if self.path.ends_with('*') {
129            if !path.starts_with(&self.path[..self.path.len() - 1]) {
130                return false;
131            }
132        } else if path != self.path {
133            return false;
134        };
135
136        operations.iter().all(|op| self.operations.contains(op))
137    }
138}
139
140#[cfg(test)]
141mod tests {
142    use super::*;
143
144    use Operation::{Create, Delete, Read, Update};
145
146    #[test]
147    fn parses_policy() {
148        let default_policy = r#"
149        # Allow tokens to look up their own properties
150path "auth/token/lookup-self" {
151    capabilities = ["read"]
152}
153
154# Allow tokens to renew themselves
155path "auth/token/renew-self" {
156    capabilities = ["update"]
157}
158
159# Allow tokens to revoke themselves
160path "auth/token/revoke-self" {
161    capabilities = ["update"]
162}
163
164# Allow a token to look up its own capabilities on a path
165path "sys/capabilities-self" {
166    capabilities = ["update"]
167}
168
169
170
171# Allow a token to look up its resultant ACL from all policies. This is useful
172# for UIs. It is an internal path because the format may change at any time
173# based on how the internal ACL features and capabilities change.
174path "sys/internal/ui/resultant-acl" {
175    capabilities = ["read"]
176}
177
178# Allow a token to renew a lease via lease_id in the request body; old path for
179# old clients, new path for newer
180path "sys/renew" {
181    capabilities = ["update"]
182}
183path "sys/leases/renew" {
184    capabilities = ["update"]
185}
186
187# Allow looking up lease properties. This requires knowing the lease ID ahead
188# of time and does not divulge any sensitive information.
189path "sys/leases/lookup" {
190    capabilities = ["update"]
191}
192
193# Allow a token to manage its own cubbyhole
194path "cubbyhole/*" {
195    capabilities = ["create", "read", "update", "delete"]
196}
197
198# Allow a token to wrap arbitrary values in a response-wrapping token
199path "sys/wrapping/wrap" {
200    capabilities = ["update"]
201}
202
203# Allow a token to look up the creation time and TTL of a given
204# response-wrapping token
205path "sys/wrapping/lookup" {
206    capabilities = ["update"]
207}
208
209# Allow a token to unwrap a response-wrapping token. This is a convenience to
210# avoid client token swapping since this is also part of the response wrapping
211# policy.
212path "sys/wrapping/unwrap" {
213    capabilities = ["update"]
214}
215
216# Allow general purpose tools
217path "sys/tools/hash" {
218    capabilities = ["update"]
219}
220path "sys/tools/hash/*" {
221    capabilities = ["update"]
222}
223
224# Allow checking the status of a Control Group request if the user has the
225# accessor
226path "sys/control-group/request" {
227    capabilities = ["update"]
228}
229"#;
230
231        let parse_result = PathPolicy::parse(default_policy);
232        assert!(parse_result.is_ok());
233        let policies = parse_result.unwrap();
234        assert_eq!(
235            policies,
236            vec![
237                PathPolicy {
238                    path: "auth/token/lookup-self".into(),
239                    operations: vec![Read],
240                },
241                PathPolicy {
242                    path: "auth/token/renew-self".into(),
243                    operations: vec![Update],
244                },
245                PathPolicy {
246                    path: "auth/token/revoke-self".into(),
247                    operations: vec![Update],
248                },
249                PathPolicy {
250                    path: "sys/capabilities-self".into(),
251                    operations: vec![Update],
252                },
253                PathPolicy {
254                    path: "sys/internal/ui/resultant-acl".into(),
255                    operations: vec![Read],
256                },
257                PathPolicy {
258                    path: "sys/renew".into(),
259                    operations: vec![Update],
260                },
261                PathPolicy {
262                    path: "sys/leases/renew".into(),
263                    operations: vec![Update],
264                },
265                PathPolicy {
266                    path: "sys/leases/lookup".into(),
267                    operations: vec![Update],
268                },
269                PathPolicy {
270                    path: "cubbyhole/*".into(),
271                    operations: vec![Create, Read, Update, Delete],
272                },
273                PathPolicy {
274                    path: "sys/wrapping/wrap".into(),
275                    operations: vec![Update],
276                },
277                PathPolicy {
278                    path: "sys/wrapping/lookup".into(),
279                    operations: vec![Update],
280                },
281                PathPolicy {
282                    path: "sys/wrapping/unwrap".into(),
283                    operations: vec![Update],
284                },
285                PathPolicy {
286                    path: "sys/tools/hash".into(),
287                    operations: vec![Update],
288                },
289                PathPolicy {
290                    path: "sys/tools/hash/*".into(),
291                    operations: vec![Update],
292                },
293                PathPolicy {
294                    path: "sys/control-group/request".into(),
295                    operations: vec![Update],
296                },
297            ]
298        );
299    }
300
301    #[test]
302    fn authorize_request_against_policy() {
303        use Operation::{Read, Update};
304        let policy = PathPolicy {
305            path: "sys/mounts".into(),
306            operations: vec![Read],
307        };
308        assert!(policy.is_authorized("sys/mounts", &[Read]));
309        assert!(!policy.is_authorized("sys/mounts", &[Update]));
310        assert!(!policy.is_authorized("sys/mounts/", &[Read]));
311        assert!(!policy.is_authorized("sys/", &[Read]));
312        assert!(!policy.is_authorized("secret/", &[Read]));
313        assert!(!policy.is_authorized("/", &[Read]));
314
315        let policy = PathPolicy {
316            path: "sys/*".into(),
317            operations: vec![Read, Update],
318        };
319        assert!(policy.is_authorized("sys/mounts", &[Read]));
320        assert!(policy.is_authorized("sys/mounts", &[Update]));
321        assert!(policy.is_authorized("sys/mounts/", &[Read]));
322        assert!(policy.is_authorized("sys/", &[Read]));
323        assert!(!policy.is_authorized("secret/", &[Read]));
324        assert!(!policy.is_authorized("/", &[Read]));
325    }
326}