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