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