]> git.proxmox.com Git - proxmox.git/blob - proxmox-router/src/permission.rs
65d2fce3aa03d25dcb28bbc157730e80fa9af5b1
[proxmox.git] / proxmox-router / src / permission.rs
1 //! Declarative permission system
2 //!
3 //! A declarative way to define API access permissions.
4
5 use std::collections::HashMap;
6 use std::fmt;
7 use std::ops::Deref;
8
9 /// Access permission
10 #[cfg_attr(feature = "test-harness", derive(Eq, PartialEq))]
11 pub enum Permission {
12 /// Allow Superuser
13 Superuser,
14 /// Allow the whole World, no authentication required
15 World,
16 /// Allow any authenticated user
17 Anybody,
18 /// Allow access for the specified user
19 User(&'static str),
20 /// Allow access if specified param matches logged in user
21 UserParam(&'static str),
22 /// Allow access for the specified group of users
23 Group(&'static str),
24 /// Use a parameter value as userid to run sub-permission tests.
25 WithParam(&'static str, &'static Permission),
26 /// Check privilege/role on the specified path. The boolean
27 /// attribute specifies if you want to allow partial matches (u64
28 /// interpreted as bitmask).
29 Privilege(&'static [&'static str], u64, bool),
30 /// Allow access if all sub-permissions match
31 And(&'static [&'static Permission]),
32 /// Allow access if any sub-permissions match
33 Or(&'static [&'static Permission]),
34 }
35
36 impl fmt::Debug for Permission {
37 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
38 match self {
39 Permission::Superuser => f.write_str("Superuser"),
40 Permission::World => f.write_str("World"),
41 Permission::Anybody => f.write_str("Anybody"),
42 Permission::User(ref userid) => write!(f, "User({})", userid),
43 Permission::UserParam(param_name) => write!(f, "UserParam({})", param_name),
44 Permission::Group(ref group) => write!(f, "Group({})", group),
45 Permission::WithParam(param_name, subtest) => {
46 write!(f, "WithParam({}, {:?})", param_name, subtest)
47 }
48 Permission::Privilege(path, privs, partial) => {
49 write!(f, "Privilege({:?}, {:0b}, {})", path, privs, partial)
50 }
51 Permission::And(list) => {
52 f.write_str("And(\n")?;
53 for subtest in list.iter() {
54 writeln!(f, " {:?}", subtest)?;
55 }
56 f.write_str(")\n")
57 }
58 Permission::Or(list) => {
59 f.write_str("Or(\n")?;
60 for subtest in list.iter() {
61 writeln!(f, " {:?}", subtest)?;
62 }
63 f.write_str(")\n")
64 }
65 }
66 }
67 }
68
69 /// Trait to query user information (used by check_api_permission)
70 pub trait UserInformation {
71 fn is_superuser(&self, userid: &str) -> bool;
72 fn is_group_member(&self, userid: &str, group: &str) -> bool;
73 fn lookup_privs(&self, userid: &str, path: &[&str]) -> u64;
74 }
75
76 impl<T: UserInformation> UserInformation for std::sync::Arc<T> {
77 fn is_superuser(&self, userid: &str) -> bool {
78 self.deref().is_superuser(userid)
79 }
80 fn is_group_member(&self, userid: &str, group: &str) -> bool {
81 self.deref().is_group_member(userid, group)
82 }
83 fn lookup_privs(&self, userid: &str, path: &[&str]) -> u64 {
84 self.deref().lookup_privs(userid, path)
85 }
86 }
87
88 /// Example implementation to check access permissions
89 ///
90 /// This implementation supports URI variables in Privilege path
91 /// components, i.e. '{storage}'. We replace this with actual
92 /// parameter values before calling lookup_privs().
93 pub fn check_api_permission(
94 perm: &Permission,
95 userid: Option<&str>,
96 param: &HashMap<String, String>,
97 info: &dyn UserInformation,
98 ) -> bool {
99 if let Some(userid) = userid {
100 if info.is_superuser(userid) {
101 return true;
102 }
103 }
104
105 check_api_permission_tail(perm, userid, param, info)
106 }
107
108 // some of them are deeply nested
109 #[allow(clippy::needless_return)]
110 fn check_api_permission_tail(
111 perm: &Permission,
112 userid: Option<&str>,
113 param: &HashMap<String, String>,
114 info: &dyn UserInformation,
115 ) -> bool {
116 match perm {
117 Permission::World => return true,
118 Permission::Anybody => {
119 return userid.is_some();
120 }
121 Permission::Superuser => match userid {
122 None => return false,
123 Some(userid) => return info.is_superuser(userid),
124 },
125 Permission::User(expected_userid) => match userid {
126 None => return false,
127 Some(ref userid) => return userid == expected_userid,
128 },
129 Permission::UserParam(param_name) => match (userid, param.get(&param_name.to_string())) {
130 (None, _) => return false,
131 (_, None) => return false,
132 (Some(ref userid), Some(ref expected)) => return userid == expected,
133 },
134 Permission::Group(expected_group) => match userid {
135 None => return false,
136 Some(userid) => return info.is_group_member(userid, expected_group),
137 },
138 Permission::WithParam(param_name, subtest) => {
139 return check_api_permission(
140 subtest,
141 param.get(*param_name).map(|v| v.as_str()),
142 param,
143 info,
144 );
145 }
146 Permission::Privilege(path, expected_privs, partial) => {
147 // replace uri vars
148 let mut new_path: Vec<&str> = Vec::new();
149 for comp in path.iter() {
150 if comp.starts_with('{') && comp.ends_with('}') {
151 let param_name = unsafe { comp.get_unchecked(1..comp.len() - 1) };
152 match param.get(param_name) {
153 None => return false,
154 Some(value) => {
155 new_path.push(value);
156 }
157 }
158 } else {
159 new_path.push(comp);
160 }
161 }
162 match userid {
163 None => return false,
164 Some(userid) => {
165 let privs = info.lookup_privs(userid, &new_path);
166 if privs == 0 {
167 return false;
168 };
169 if *partial {
170 return (expected_privs & privs) != 0;
171 } else {
172 return (*expected_privs & privs) == *expected_privs;
173 }
174 }
175 }
176 }
177 Permission::And(list) => {
178 for subtest in list.iter() {
179 if !check_api_permission_tail(subtest, userid, param, info) {
180 return false;
181 }
182 }
183
184 return true;
185 }
186 Permission::Or(list) => {
187 for subtest in list.iter() {
188 if check_api_permission_tail(subtest, userid, param, info) {
189 return true;
190 }
191 }
192
193 return false;
194 }
195 }
196 }
197
198 #[cfg(test)]
199 mod test {
200 use serde_json::{json, Value};
201
202 use crate::permission::*;
203
204 struct MockedUserInfo {
205 privs: Value,
206 groups: Value,
207 }
208
209 impl UserInformation for MockedUserInfo {
210 fn is_superuser(&self, userid: &str) -> bool {
211 userid == "root"
212 }
213
214 fn is_group_member(&self, userid: &str, group: &str) -> bool {
215 if let Some(groups) = self.groups[userid].as_array() {
216 return groups.contains(&Value::from(group));
217 }
218
219 false
220 }
221
222 fn lookup_privs(&self, userid: &str, path: &[&str]) -> u64 {
223 let path = format!("/{}", path.join("/"));
224 if let Some(users) = self.privs.get(path) {
225 if let Some(privilege) = users.get(userid) {
226 return privilege.as_u64().unwrap();
227 }
228 }
229
230 0
231 }
232 }
233
234 #[test]
235 fn test_privileges() {
236 let userinfo = MockedUserInfo {
237 privs: json!({
238 "/": {
239 "user1": 0b10,
240 },
241 "/datastore": {
242 "user1": 0b00,
243 "user2": 0b01,
244 },
245 "/datastore/foo": {
246 "user1": 0b01,
247 },
248 }),
249 groups: json!({
250 "user1": [
251 "group1",
252 ],
253 "user2": [
254 "group2",
255 ],
256 }),
257 };
258
259 let mut param = HashMap::new();
260 param.insert("user".to_string(), "user1".to_string());
261 param.insert("datastore".to_string(), "foo".to_string());
262
263 let test_check = |perm: &Permission, userid: Option<&str>, should_succeed: bool| {
264 println!("{:?} on {:?}: {}", userid, perm, should_succeed);
265 assert_eq!(
266 check_api_permission(perm, userid, &param, &userinfo),
267 should_succeed
268 )
269 };
270
271 test_check(&Permission::Superuser, Some("root"), true);
272 test_check(&Permission::Superuser, Some("user1"), false);
273 test_check(&Permission::Superuser, None, false);
274
275 test_check(&Permission::World, Some("root"), true);
276 test_check(&Permission::World, Some("user1"), true);
277 test_check(&Permission::World, None, true);
278
279 test_check(&Permission::Anybody, Some("root"), true);
280 test_check(&Permission::Anybody, Some("user1"), true);
281 test_check(&Permission::Anybody, None, false);
282
283 test_check(&Permission::User("user1"), Some("root"), true);
284 test_check(&Permission::User("user1"), Some("user1"), true);
285 test_check(&Permission::User("user1"), Some("user2"), false);
286 test_check(&Permission::User("user1"), None, false);
287
288 test_check(&Permission::Group("group1"), Some("root"), true);
289 test_check(&Permission::Group("group1"), Some("user1"), true);
290 test_check(&Permission::Group("group1"), Some("user2"), false);
291 test_check(&Permission::Group("group1"), None, false);
292
293 test_check(
294 &Permission::WithParam("user", &Permission::User("root")),
295 Some("root"),
296 true,
297 );
298 test_check(
299 &Permission::WithParam("user", &Permission::User("user1")),
300 Some("user1"),
301 true,
302 );
303 test_check(
304 &Permission::WithParam("user", &Permission::User("user2")),
305 Some("user2"),
306 false,
307 );
308 test_check(
309 &Permission::WithParam("user", &Permission::User("")),
310 None,
311 false,
312 );
313
314 test_check(
315 &Permission::And(&[&Permission::User("user1"), &Permission::Group("group2")]),
316 Some("root"),
317 true,
318 );
319 test_check(
320 &Permission::And(&[&Permission::User("user1"), &Permission::Group("group2")]),
321 Some("user1"),
322 false,
323 );
324 test_check(
325 &Permission::And(&[&Permission::User("user1"), &Permission::Group("group1")]),
326 Some("user1"),
327 true,
328 );
329 test_check(
330 &Permission::And(&[&Permission::User("user1"), &Permission::Group("group2")]),
331 None,
332 false,
333 );
334
335 test_check(
336 &Permission::Or(&[&Permission::User("user1"), &Permission::Group("group2")]),
337 Some("root"),
338 true,
339 );
340 test_check(
341 &Permission::Or(&[&Permission::User("user1"), &Permission::Group("group2")]),
342 Some("user1"),
343 true,
344 );
345 test_check(
346 &Permission::Or(&[&Permission::User("user1"), &Permission::Group("group1")]),
347 Some("user2"),
348 false,
349 );
350 test_check(
351 &Permission::Or(&[&Permission::User("user1"), &Permission::Group("group2")]),
352 None,
353 false,
354 );
355
356 test_check(&Permission::Privilege(&[], 0b11, true), Some("root"), true);
357 test_check(&Permission::Privilege(&[], 0b11, true), Some("user1"), true);
358 test_check(
359 &Permission::Privilege(&[], 0b11, false),
360 Some("user1"),
361 false,
362 );
363 test_check(
364 &Permission::Privilege(&[], 0b11, true),
365 Some("user2"),
366 false,
367 );
368 test_check(
369 &Permission::Privilege(&[], 0b11, false),
370 Some("user2"),
371 false,
372 );
373 test_check(&Permission::Privilege(&[], 0b11, true), None, false);
374 test_check(&Permission::Privilege(&[], 0b11, false), None, false);
375
376 test_check(
377 &Permission::Privilege(&["datastore"], 0b01, true),
378 Some("user1"),
379 false,
380 );
381 test_check(
382 &Permission::Privilege(&["datastore"], 0b01, true),
383 Some("user2"),
384 true,
385 );
386 test_check(
387 &Permission::Privilege(&["datastore"], 0b01, true),
388 None,
389 false,
390 );
391
392 test_check(
393 &Permission::Privilege(&["datastore", "{datastore}"], 0b01, true),
394 Some("user1"),
395 true,
396 );
397 test_check(
398 &Permission::Privilege(&["datastore", "{datastore}"], 0b01, true),
399 Some("user2"),
400 false,
401 );
402 test_check(
403 &Permission::Privilege(&["datastore", "{datastore}"], 0b01, true),
404 None,
405 false,
406 );
407 }
408 }