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