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