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