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