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