1use std::str::FromStr;
2
3use regex::Regex;
4use serde::{Deserialize, Serialize};
5
6use crate::{error::ApiError, request::Operation};
7
8#[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 .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 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 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 pub fn parse(s: &str) -> Result<Vec<Self>, ApiError> {
91 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}