]> git.proxmox.com Git - proxmox-backup.git/blob - src/config/acl.rs
src/config/acl.rs: introduice privileges and roles for remotes
[proxmox-backup.git] / src / config / acl.rs
1 use std::io::Write;
2 use std::collections::{HashMap, HashSet, BTreeMap, BTreeSet};
3 use std::path::{PathBuf, Path};
4 use std::sync::{Arc, RwLock};
5
6 use anyhow::{bail, Error};
7
8 use lazy_static::lazy_static;
9
10 use proxmox::tools::{fs::replace_file, fs::CreateOptions};
11
12 // define Privilege bitfield
13
14 pub const PRIV_SYS_AUDIT: u64 = 1 << 0;
15 pub const PRIV_SYS_MODIFY: u64 = 1 << 1;
16 pub const PRIV_SYS_POWER_MANAGEMENT: u64 = 1 << 2;
17
18 pub const PRIV_DATASTORE_AUDIT: u64 = 1 << 3;
19 pub const PRIV_DATASTORE_MODIFY: u64 = 1 << 4;
20 pub const PRIV_DATASTORE_READ: u64 = 1 << 5;
21
22 /// Datastore.Backup also requires backup ownership
23 pub const PRIV_DATASTORE_BACKUP: u64 = 1 << 6;
24 /// Datastore.Prune also requires backup ownership
25 pub const PRIV_DATASTORE_PRUNE: u64 = 1 << 7;
26
27 pub const PRIV_PERMISSIONS_MODIFY: u64 = 1 << 8;
28
29 pub const PRIV_REMOTE_AUDIT: u64 = 1 << 9;
30 pub const PRIV_REMOTE_MODIFY: u64 = 1 << 10;
31 pub const PRIV_REMOTE_READ: u64 = 1 << 11;
32 pub const PRIV_REMOTE_PRUNE: u64 = 1 << 12;
33
34 pub const ROLE_ADMIN: u64 = std::u64::MAX;
35 pub const ROLE_NO_ACCESS: u64 = 0;
36
37 pub const ROLE_AUDIT: u64 =
38 PRIV_SYS_AUDIT |
39 PRIV_DATASTORE_AUDIT;
40
41 /// Datastore.Admin can do anything on the datastore.
42 pub const ROLE_DATASTORE_ADMIN: u64 =
43 PRIV_DATASTORE_AUDIT |
44 PRIV_DATASTORE_MODIFY |
45 PRIV_DATASTORE_READ |
46 PRIV_DATASTORE_BACKUP |
47 PRIV_DATASTORE_PRUNE;
48
49 /// Datastore.Reader can read datastore content an do restore
50 pub const ROLE_DATASTORE_READER: u64 =
51 PRIV_DATASTORE_AUDIT |
52 PRIV_DATASTORE_READ;
53
54 /// Datastore.Backup can do backup and restore, but no prune.
55 pub const ROLE_DATASTORE_BACKUP: u64 =
56 PRIV_DATASTORE_BACKUP;
57
58 /// Datastore.PowerUser can do backup, restore, and prune.
59 pub const ROLE_DATASTORE_POWERUSER: u64 =
60 PRIV_DATASTORE_PRUNE |
61 PRIV_DATASTORE_BACKUP;
62
63 /// Datastore.Audit can audit the datastore.
64 pub const ROLE_DATASTORE_AUDIT: u64 =
65 PRIV_DATASTORE_AUDIT;
66
67 /// Remote.Audit can audit the remote
68 pub const ROLE_REMOTE_AUDIT: u64 =
69 PRIV_REMOTE_AUDIT;
70
71 /// Remote.Admin can do anything on the remote.
72 pub const ROLE_REMOTE_ADMIN: u64 =
73 PRIV_REMOTE_AUDIT |
74 PRIV_REMOTE_MODIFY |
75 PRIV_REMOTE_READ |
76 PRIV_REMOTE_PRUNE;
77
78 /// Remote.SyncOperator can do read and prune on the remote.
79 pub const ROLE_REMOTE_SYNC_OPERATOR: u64 =
80 PRIV_REMOTE_AUDIT |
81 PRIV_REMOTE_READ |
82 PRIV_REMOTE_PRUNE;
83
84 pub const ROLE_NAME_NO_ACCESS: &str ="NoAccess";
85
86 lazy_static! {
87 pub static ref ROLE_NAMES: HashMap<&'static str, (u64, &'static str)> = {
88 let mut map = HashMap::new();
89
90 map.insert("Admin", (
91 ROLE_ADMIN,
92 "Administrator",
93 ));
94 map.insert("Audit", (
95 ROLE_AUDIT,
96 "Auditor",
97 ));
98 map.insert(ROLE_NAME_NO_ACCESS, (
99 ROLE_NO_ACCESS,
100 "Disable access",
101 ));
102
103 map.insert("Datastore.Admin", (
104 ROLE_DATASTORE_ADMIN,
105 "Datastore Administrator",
106 ));
107 map.insert("Datastore.Reader", (
108 ROLE_DATASTORE_READER,
109 "Datastore Reader (inspect datastore content and do restores)",
110 ));
111 map.insert("Datastore.Backup", (
112 ROLE_DATASTORE_BACKUP,
113 "Datastore Backup (backup and restore owned backups)",
114 ));
115 map.insert("Datastore.PowerUser", (
116 ROLE_DATASTORE_POWERUSER,
117 "Datastore PowerUser (backup, restore and prune owned backup)",
118 ));
119 map.insert("Datastore.Audit", (
120 ROLE_DATASTORE_AUDIT,
121 "Datastore Auditor",
122 ));
123
124 map.insert("Remote.Audit", (
125 ROLE_REMOTE_AUDIT,
126 "Remote Auditor",
127 ));
128 map.insert("Remote.Admin", (
129 ROLE_REMOTE_ADMIN,
130 "Remote Administrator",
131 ));
132 map.insert("Remote.SyncOperator", (
133 ROLE_REMOTE_SYNC_OPERATOR,
134 "Syncronisation Opertator",
135 ));
136
137 map
138 };
139 }
140
141 pub fn split_acl_path(path: &str) -> Vec<&str> {
142
143 let items = path.split('/');
144
145 let mut components = vec![];
146
147 for name in items {
148 if name.is_empty() { continue; }
149 components.push(name);
150 }
151
152 components
153 }
154
155 pub struct AclTree {
156 pub root: AclTreeNode,
157 }
158
159 pub struct AclTreeNode {
160 pub users: HashMap<String, HashMap<String, bool>>,
161 pub groups: HashMap<String, HashMap<String, bool>>,
162 pub children: BTreeMap<String, AclTreeNode>,
163 }
164
165 impl AclTreeNode {
166
167 pub fn new() -> Self {
168 Self {
169 users: HashMap::new(),
170 groups: HashMap::new(),
171 children: BTreeMap::new(),
172 }
173 }
174
175 pub fn extract_roles(&self, user: &str, all: bool) -> HashSet<String> {
176 let user_roles = self.extract_user_roles(user, all);
177 if !user_roles.is_empty() {
178 // user privs always override group privs
179 return user_roles
180 };
181
182 self.extract_group_roles(user, all)
183 }
184
185 pub fn extract_user_roles(&self, user: &str, all: bool) -> HashSet<String> {
186
187 let mut set = HashSet::new();
188
189 let roles = match self.users.get(user) {
190 Some(m) => m,
191 None => return set,
192 };
193
194 for (role, propagate) in roles {
195 if *propagate || all {
196 if role == ROLE_NAME_NO_ACCESS {
197 // return a set with a single role 'NoAccess'
198 let mut set = HashSet::new();
199 set.insert(role.to_string());
200 return set;
201 }
202 set.insert(role.to_string());
203 }
204 }
205
206 set
207 }
208
209 pub fn extract_group_roles(&self, _user: &str, all: bool) -> HashSet<String> {
210
211 let mut set = HashSet::new();
212
213 for (_group, roles) in &self.groups {
214 let is_member = false; // fixme: check if user is member of the group
215 if !is_member { continue; }
216
217 for (role, propagate) in roles {
218 if *propagate || all {
219 if role == ROLE_NAME_NO_ACCESS {
220 // return a set with a single role 'NoAccess'
221 let mut set = HashSet::new();
222 set.insert(role.to_string());
223 return set;
224 }
225 set.insert(role.to_string());
226 }
227 }
228 }
229
230 set
231 }
232
233 pub fn delete_group_role(&mut self, group: &str, role: &str) {
234 let roles = match self.groups.get_mut(group) {
235 Some(r) => r,
236 None => return,
237 };
238 roles.remove(role);
239 }
240
241 pub fn delete_user_role(&mut self, userid: &str, role: &str) {
242 let roles = match self.users.get_mut(userid) {
243 Some(r) => r,
244 None => return,
245 };
246 roles.remove(role);
247 }
248
249 pub fn insert_group_role(&mut self, group: String, role: String, propagate: bool) {
250 let map = self.groups.entry(group).or_insert_with(|| HashMap::new());
251 if role == ROLE_NAME_NO_ACCESS {
252 map.clear();
253 map.insert(role, propagate);
254 } else {
255 map.remove(ROLE_NAME_NO_ACCESS);
256 map.insert(role, propagate);
257 }
258 }
259
260 pub fn insert_user_role(&mut self, user: String, role: String, propagate: bool) {
261 let map = self.users.entry(user).or_insert_with(|| HashMap::new());
262 if role == ROLE_NAME_NO_ACCESS {
263 map.clear();
264 map.insert(role, propagate);
265 } else {
266 map.remove(ROLE_NAME_NO_ACCESS);
267 map.insert(role, propagate);
268 }
269 }
270 }
271
272 impl AclTree {
273
274 pub fn new() -> Self {
275 Self { root: AclTreeNode::new() }
276 }
277
278 fn get_node(&mut self, path: &[&str]) -> Option<&mut AclTreeNode> {
279 let mut node = &mut self.root;
280 for comp in path {
281 node = match node.children.get_mut(*comp) {
282 Some(n) => n,
283 None => return None,
284 };
285 }
286 Some(node)
287 }
288
289 fn get_or_insert_node(&mut self, path: &[&str]) -> &mut AclTreeNode {
290 let mut node = &mut self.root;
291 for comp in path {
292 node = node.children.entry(String::from(*comp))
293 .or_insert_with(|| AclTreeNode::new());
294 }
295 node
296 }
297
298 pub fn delete_group_role(&mut self, path: &str, group: &str, role: &str) {
299 let path = split_acl_path(path);
300 let node = match self.get_node(&path) {
301 Some(n) => n,
302 None => return,
303 };
304 node.delete_group_role(group, role);
305 }
306
307 pub fn delete_user_role(&mut self, path: &str, userid: &str, role: &str) {
308 let path = split_acl_path(path);
309 let node = match self.get_node(&path) {
310 Some(n) => n,
311 None => return,
312 };
313 node.delete_user_role(userid, role);
314 }
315
316 pub fn insert_group_role(&mut self, path: &str, group: &str, role: &str, propagate: bool) {
317 let path = split_acl_path(path);
318 let node = self.get_or_insert_node(&path);
319 node.insert_group_role(group.to_string(), role.to_string(), propagate);
320 }
321
322 pub fn insert_user_role(&mut self, path: &str, user: &str, role: &str, propagate: bool) {
323 let path = split_acl_path(path);
324 let node = self.get_or_insert_node(&path);
325 node.insert_user_role(user.to_string(), role.to_string(), propagate);
326 }
327
328 fn write_node_config(
329 node: &AclTreeNode,
330 path: &str,
331 w: &mut dyn Write,
332 ) -> Result<(), Error> {
333
334 let mut role_ug_map0 = HashMap::new();
335 let mut role_ug_map1 = HashMap::new();
336
337 for (user, roles) in &node.users {
338 // no need to save, because root is always 'Administrator'
339 if user == "root@pam" { continue; }
340 for (role, propagate) in roles {
341 let role = role.as_str();
342 let user = user.to_string();
343 if *propagate {
344 role_ug_map1.entry(role).or_insert_with(|| BTreeSet::new())
345 .insert(user);
346 } else {
347 role_ug_map0.entry(role).or_insert_with(|| BTreeSet::new())
348 .insert(user);
349 }
350 }
351 }
352
353 for (group, roles) in &node.groups {
354 for (role, propagate) in roles {
355 let group = format!("@{}", group);
356 if *propagate {
357 role_ug_map1.entry(role).or_insert_with(|| BTreeSet::new())
358 .insert(group);
359 } else {
360 role_ug_map0.entry(role).or_insert_with(|| BTreeSet::new())
361 .insert(group);
362 }
363 }
364 }
365
366 fn group_by_property_list(
367 item_property_map: &HashMap<&str, BTreeSet<String>>,
368 ) -> BTreeMap<String, BTreeSet<String>> {
369 let mut result_map = BTreeMap::new();
370 for (item, property_map) in item_property_map {
371 let item_list = property_map.iter().fold(String::new(), |mut acc, v| {
372 if !acc.is_empty() { acc.push(','); }
373 acc.push_str(v);
374 acc
375 });
376 result_map.entry(item_list).or_insert_with(|| BTreeSet::new())
377 .insert(item.to_string());
378 }
379 result_map
380 }
381
382 let uglist_role_map0 = group_by_property_list(&role_ug_map0);
383 let uglist_role_map1 = group_by_property_list(&role_ug_map1);
384
385 fn role_list(roles: &BTreeSet<String>) -> String {
386 if roles.contains(ROLE_NAME_NO_ACCESS) { return String::from(ROLE_NAME_NO_ACCESS); }
387 roles.iter().fold(String::new(), |mut acc, v| {
388 if !acc.is_empty() { acc.push(','); }
389 acc.push_str(v);
390 acc
391 })
392 }
393
394 for (uglist, roles) in &uglist_role_map0 {
395 let role_list = role_list(roles);
396 writeln!(w, "acl:0:{}:{}:{}", if path.is_empty() { "/" } else { path }, uglist, role_list)?;
397 }
398
399 for (uglist, roles) in &uglist_role_map1 {
400 let role_list = role_list(roles);
401 writeln!(w, "acl:1:{}:{}:{}", if path.is_empty() { "/" } else { path }, uglist, role_list)?;
402 }
403
404 for (name, child) in node.children.iter() {
405 let child_path = format!("{}/{}", path, name);
406 Self::write_node_config(child, &child_path, w)?;
407 }
408
409 Ok(())
410 }
411
412 pub fn write_config(&self, w: &mut dyn Write) -> Result<(), Error> {
413 Self::write_node_config(&self.root, "", w)
414 }
415
416 fn parse_acl_line(&mut self, line: &str) -> Result<(), Error> {
417
418 let items: Vec<&str> = line.split(':').collect();
419
420 if items.len() != 5 {
421 bail!("wrong number of items.");
422 }
423
424 if items[0] != "acl" {
425 bail!("line does not start with 'acl'.");
426 }
427
428 let propagate = if items[1] == "0" {
429 false
430 } else if items[1] == "1" {
431 true
432 } else {
433 bail!("expected '0' or '1' for propagate flag.");
434 };
435
436 let path = split_acl_path(items[2]);
437 let node = self.get_or_insert_node(&path);
438
439 let uglist: Vec<&str> = items[3].split(',').map(|v| v.trim()).collect();
440
441 let rolelist: Vec<&str> = items[4].split(',').map(|v| v.trim()).collect();
442
443 for user_or_group in &uglist {
444 for role in &rolelist {
445 if !ROLE_NAMES.contains_key(role) {
446 bail!("unknown role '{}'", role);
447 }
448 if user_or_group.starts_with('@') {
449 let group = &user_or_group[1..];
450 node.insert_group_role(group.to_string(), role.to_string(), propagate);
451 } else {
452 node.insert_user_role(user_or_group.to_string(), role.to_string(), propagate);
453 }
454 }
455 }
456
457 Ok(())
458 }
459
460 pub fn load(filename: &Path) -> Result<(Self, [u8;32]), Error> {
461 let mut tree = Self::new();
462
463 let raw = match std::fs::read_to_string(filename) {
464 Ok(v) => v,
465 Err(err) => {
466 if err.kind() == std::io::ErrorKind::NotFound {
467 String::new()
468 } else {
469 bail!("unable to read acl config {:?} - {}", filename, err);
470 }
471 }
472 };
473
474 let digest = openssl::sha::sha256(raw.as_bytes());
475
476 for (linenr, line) in raw.lines().enumerate() {
477 let line = line.trim();
478 if line.is_empty() { continue; }
479 if let Err(err) = tree.parse_acl_line(line) {
480 bail!("unable to parse acl config {:?}, line {} - {}",
481 filename, linenr+1, err);
482 }
483 }
484
485 Ok((tree, digest))
486 }
487
488 pub fn from_raw(raw: &str) -> Result<Self, Error> {
489 let mut tree = Self::new();
490 for (linenr, line) in raw.lines().enumerate() {
491 let line = line.trim();
492 if line.is_empty() { continue; }
493 if let Err(err) = tree.parse_acl_line(line) {
494 bail!("unable to parse acl config data, line {} - {}", linenr+1, err);
495 }
496 }
497 Ok(tree)
498 }
499
500 pub fn roles(&self, userid: &str, path: &[&str]) -> HashSet<String> {
501
502 let mut node = &self.root;
503 let mut role_set = node.extract_roles(userid, path.is_empty());
504
505 for (pos, comp) in path.iter().enumerate() {
506 let last_comp = (pos + 1) == path.len();
507 node = match node.children.get(*comp) {
508 Some(n) => n,
509 None => return role_set, // path not found
510 };
511 let new_set = node.extract_roles(userid, last_comp);
512 if !new_set.is_empty() {
513 // overwrite previous settings
514 role_set = new_set;
515 }
516 }
517
518 role_set
519 }
520 }
521
522 pub const ACL_CFG_FILENAME: &str = "/etc/proxmox-backup/acl.cfg";
523 pub const ACL_CFG_LOCKFILE: &str = "/etc/proxmox-backup/.acl.lck";
524
525 pub fn config() -> Result<(AclTree, [u8; 32]), Error> {
526 let path = PathBuf::from(ACL_CFG_FILENAME);
527 AclTree::load(&path)
528 }
529
530 pub fn cached_config() -> Result<Arc<AclTree>, Error> {
531
532 struct ConfigCache {
533 data: Option<Arc<AclTree>>,
534 last_mtime: i64,
535 last_mtime_nsec: i64,
536 }
537
538 lazy_static! {
539 static ref CACHED_CONFIG: RwLock<ConfigCache> = RwLock::new(
540 ConfigCache { data: None, last_mtime: 0, last_mtime_nsec: 0 });
541 }
542
543 let stat = nix::sys::stat::stat(ACL_CFG_FILENAME)?;
544
545 { // limit scope
546 let cache = CACHED_CONFIG.read().unwrap();
547 if stat.st_mtime == cache.last_mtime && stat.st_mtime_nsec == cache.last_mtime_nsec {
548 if let Some(ref config) = cache.data {
549 return Ok(config.clone());
550 }
551 }
552 }
553
554 let (config, _digest) = config()?;
555 let config = Arc::new(config);
556
557 let mut cache = CACHED_CONFIG.write().unwrap();
558 cache.last_mtime = stat.st_mtime;
559 cache.last_mtime_nsec = stat.st_mtime_nsec;
560 cache.data = Some(config.clone());
561
562 Ok(config)
563 }
564
565 pub fn save_config(acl: &AclTree) -> Result<(), Error> {
566 let mut raw: Vec<u8> = Vec::new();
567
568 acl.write_config(&mut raw)?;
569
570 let backup_user = crate::backup::backup_user()?;
571 let mode = nix::sys::stat::Mode::from_bits_truncate(0o0640);
572 // set the correct owner/group/permissions while saving file
573 // owner(rw) = root, group(r)= backup
574 let options = CreateOptions::new()
575 .perm(mode)
576 .owner(nix::unistd::ROOT)
577 .group(backup_user.gid);
578
579 replace_file(ACL_CFG_FILENAME, &raw, options)?;
580
581 Ok(())
582 }
583
584 #[cfg(test)]
585 mod test {
586
587 use anyhow::{Error};
588 use super::AclTree;
589
590 fn check_roles(
591 tree: &AclTree,
592 user: &str,
593 path: &str,
594 expected_roles: &str,
595 ) {
596
597 let path_vec = super::split_acl_path(path);
598 let mut roles = tree.roles(user, &path_vec)
599 .iter().map(|v| v.clone()).collect::<Vec<String>>();
600 roles.sort();
601 let roles = roles.join(",");
602
603 assert_eq!(roles, expected_roles, "\nat check_roles for '{}' on '{}'", user, path);
604 }
605
606 #[test]
607 fn test_acl_line_compression() -> Result<(), Error> {
608
609 let tree = AclTree::from_raw(r###"
610 acl:0:/store/store2:user1:Admin
611 acl:0:/store/store2:user2:Admin
612 acl:0:/store/store2:user1:Datastore.Backup
613 acl:0:/store/store2:user2:Datastore.Backup
614 "###)?;
615
616 let mut raw: Vec<u8> = Vec::new();
617 tree.write_config(&mut raw)?;
618 let raw = std::str::from_utf8(&raw)?;
619
620 assert_eq!(raw, "acl:0:/store/store2:user1,user2:Admin,Datastore.Backup\n");
621
622 Ok(())
623 }
624
625 #[test]
626 fn test_roles_1() -> Result<(), Error> {
627
628 let tree = AclTree::from_raw(r###"
629 acl:1:/storage:user1@pbs:Admin
630 acl:1:/storage/store1:user1@pbs:Datastore.Backup
631 acl:1:/storage/store2:user2@pbs:Datastore.Backup
632 "###)?;
633 check_roles(&tree, "user1@pbs", "/", "");
634 check_roles(&tree, "user1@pbs", "/storage", "Admin");
635 check_roles(&tree, "user1@pbs", "/storage/store1", "Datastore.Backup");
636 check_roles(&tree, "user1@pbs", "/storage/store2", "Admin");
637
638 check_roles(&tree, "user2@pbs", "/", "");
639 check_roles(&tree, "user2@pbs", "/storage", "");
640 check_roles(&tree, "user2@pbs", "/storage/store1", "");
641 check_roles(&tree, "user2@pbs", "/storage/store2", "Datastore.Backup");
642
643 Ok(())
644 }
645
646 #[test]
647 fn test_role_no_access() -> Result<(), Error> {
648
649 let tree = AclTree::from_raw(r###"
650 acl:1:/:user1@pbs:Admin
651 acl:1:/storage:user1@pbs:NoAccess
652 acl:1:/storage/store1:user1@pbs:Datastore.Backup
653 "###)?;
654 check_roles(&tree, "user1@pbs", "/", "Admin");
655 check_roles(&tree, "user1@pbs", "/storage", "NoAccess");
656 check_roles(&tree, "user1@pbs", "/storage/store1", "Datastore.Backup");
657 check_roles(&tree, "user1@pbs", "/storage/store2", "NoAccess");
658 check_roles(&tree, "user1@pbs", "/system", "Admin");
659
660 let tree = AclTree::from_raw(r###"
661 acl:1:/:user1@pbs:Admin
662 acl:0:/storage:user1@pbs:NoAccess
663 acl:1:/storage/store1:user1@pbs:Datastore.Backup
664 "###)?;
665 check_roles(&tree, "user1@pbs", "/", "Admin");
666 check_roles(&tree, "user1@pbs", "/storage", "NoAccess");
667 check_roles(&tree, "user1@pbs", "/storage/store1", "Datastore.Backup");
668 check_roles(&tree, "user1@pbs", "/storage/store2", "Admin");
669 check_roles(&tree, "user1@pbs", "/system", "Admin");
670
671 Ok(())
672 }
673
674 #[test]
675 fn test_role_add_delete() -> Result<(), Error> {
676
677 let mut tree = AclTree::new();
678
679 tree.insert_user_role("/", "user1@pbs", "Admin", true);
680 tree.insert_user_role("/", "user1@pbs", "Audit", true);
681
682 check_roles(&tree, "user1@pbs", "/", "Admin,Audit");
683
684 tree.insert_user_role("/", "user1@pbs", "NoAccess", true);
685 check_roles(&tree, "user1@pbs", "/", "NoAccess");
686
687 let mut raw: Vec<u8> = Vec::new();
688 tree.write_config(&mut raw)?;
689 let raw = std::str::from_utf8(&raw)?;
690
691 assert_eq!(raw, "acl:1:/:user1@pbs:NoAccess\n");
692
693 Ok(())
694 }
695
696 #[test]
697 fn test_no_access_overwrite() -> Result<(), Error> {
698
699 let mut tree = AclTree::new();
700
701 tree.insert_user_role("/storage", "user1@pbs", "NoAccess", true);
702
703 check_roles(&tree, "user1@pbs", "/storage", "NoAccess");
704
705 tree.insert_user_role("/storage", "user1@pbs", "Admin", true);
706 tree.insert_user_role("/storage", "user1@pbs", "Audit", true);
707
708 check_roles(&tree, "user1@pbs", "/storage", "Admin,Audit");
709
710 tree.insert_user_role("/storage", "user1@pbs", "NoAccess", true);
711
712 check_roles(&tree, "user1@pbs", "/storage", "NoAccess");
713
714 Ok(())
715 }
716
717 }