]> git.proxmox.com Git - proxmox-backup.git/blob - src/config/acl.rs
clippy: remove unnecessary closures
[proxmox-backup.git] / src / config / acl.rs
1 use std::collections::{BTreeMap, BTreeSet, HashMap};
2 use std::io::Write;
3 use std::path::{Path, PathBuf};
4 use std::str::FromStr;
5 use std::sync::{Arc, RwLock};
6
7 use anyhow::{bail, Error};
8
9 use lazy_static::lazy_static;
10
11 use ::serde::{Deserialize, Serialize};
12 use serde::de::{value, IntoDeserializer};
13
14 use proxmox::api::{api, schema::*};
15 use proxmox::constnamedbitmap;
16 use proxmox::tools::{fs::replace_file, fs::CreateOptions};
17
18 use crate::api2::types::{Authid, Userid};
19
20 // define Privilege bitfield
21
22 constnamedbitmap! {
23 /// Contains a list of privilege name to privilege value mappings.
24 ///
25 /// The names are used when displaying/persisting privileges anywhere, the values are used to
26 /// allow easy matching of privileges as bitflags.
27 PRIVILEGES: u64 => {
28 /// Sys.Audit allows knowing about the system and its status
29 PRIV_SYS_AUDIT("Sys.Audit");
30 /// Sys.Modify allows modifying system-level configuration
31 PRIV_SYS_MODIFY("Sys.Modify");
32 /// Sys.Modify allows to poweroff/reboot/.. the system
33 PRIV_SYS_POWER_MANAGEMENT("Sys.PowerManagement");
34
35 /// Datastore.Audit allows knowing about a datastore,
36 /// including reading the configuration entry and listing its contents
37 PRIV_DATASTORE_AUDIT("Datastore.Audit");
38 /// Datastore.Allocate allows creating or deleting datastores
39 PRIV_DATASTORE_ALLOCATE("Datastore.Allocate");
40 /// Datastore.Modify allows modifying a datastore and its contents
41 PRIV_DATASTORE_MODIFY("Datastore.Modify");
42 /// Datastore.Read allows reading arbitrary backup contents
43 PRIV_DATASTORE_READ("Datastore.Read");
44 /// Allows verifying a datastore
45 PRIV_DATASTORE_VERIFY("Datastore.Verify");
46
47 /// Datastore.Backup allows Datastore.Read|Verify and creating new snapshots,
48 /// but also requires backup ownership
49 PRIV_DATASTORE_BACKUP("Datastore.Backup");
50 /// Datastore.Prune allows deleting snapshots,
51 /// but also requires backup ownership
52 PRIV_DATASTORE_PRUNE("Datastore.Prune");
53
54 /// Permissions.Modify allows modifying ACLs
55 PRIV_PERMISSIONS_MODIFY("Permissions.Modify");
56
57 /// Remote.Audit allows reading remote.cfg and sync.cfg entries
58 PRIV_REMOTE_AUDIT("Remote.Audit");
59 /// Remote.Modify allows modifying remote.cfg
60 PRIV_REMOTE_MODIFY("Remote.Modify");
61 /// Remote.Read allows reading data from a configured `Remote`
62 PRIV_REMOTE_READ("Remote.Read");
63
64 /// Sys.Console allows access to the system's console
65 PRIV_SYS_CONSOLE("Sys.Console");
66 }
67 }
68
69 /// Admin always has all privileges. It can do everything except a few actions
70 /// which are limited to the 'root@pam` superuser
71 pub const ROLE_ADMIN: u64 = std::u64::MAX;
72
73 /// NoAccess can be used to remove privileges from specific (sub-)paths
74 pub const ROLE_NO_ACCESS: u64 = 0;
75
76 #[rustfmt::skip]
77 /// Audit can view configuration and status information, but not modify it.
78 pub const ROLE_AUDIT: u64 = 0
79 | PRIV_SYS_AUDIT
80 | PRIV_DATASTORE_AUDIT;
81
82 #[rustfmt::skip]
83 /// Datastore.Admin can do anything on the datastore.
84 pub const ROLE_DATASTORE_ADMIN: u64 = 0
85 | PRIV_DATASTORE_AUDIT
86 | PRIV_DATASTORE_MODIFY
87 | PRIV_DATASTORE_READ
88 | PRIV_DATASTORE_VERIFY
89 | PRIV_DATASTORE_BACKUP
90 | PRIV_DATASTORE_PRUNE;
91
92 #[rustfmt::skip]
93 /// Datastore.Reader can read/verify datastore content and do restore
94 pub const ROLE_DATASTORE_READER: u64 = 0
95 | PRIV_DATASTORE_AUDIT
96 | PRIV_DATASTORE_VERIFY
97 | PRIV_DATASTORE_READ;
98
99 #[rustfmt::skip]
100 /// Datastore.Backup can do backup and restore, but no prune.
101 pub const ROLE_DATASTORE_BACKUP: u64 = 0
102 | PRIV_DATASTORE_BACKUP;
103
104 #[rustfmt::skip]
105 /// Datastore.PowerUser can do backup, restore, and prune.
106 pub const ROLE_DATASTORE_POWERUSER: u64 = 0
107 | PRIV_DATASTORE_PRUNE
108 | PRIV_DATASTORE_BACKUP;
109
110 #[rustfmt::skip]
111 /// Datastore.Audit can audit the datastore.
112 pub const ROLE_DATASTORE_AUDIT: u64 = 0
113 | PRIV_DATASTORE_AUDIT;
114
115 #[rustfmt::skip]
116 /// Remote.Audit can audit the remote
117 pub const ROLE_REMOTE_AUDIT: u64 = 0
118 | PRIV_REMOTE_AUDIT;
119
120 #[rustfmt::skip]
121 /// Remote.Admin can do anything on the remote.
122 pub const ROLE_REMOTE_ADMIN: u64 = 0
123 | PRIV_REMOTE_AUDIT
124 | PRIV_REMOTE_MODIFY
125 | PRIV_REMOTE_READ;
126
127 #[rustfmt::skip]
128 /// Remote.SyncOperator can do read and prune on the remote.
129 pub const ROLE_REMOTE_SYNC_OPERATOR: u64 = 0
130 | PRIV_REMOTE_AUDIT
131 | PRIV_REMOTE_READ;
132
133 /// NoAccess can be used to remove privileges from specific (sub-)paths
134 pub const ROLE_NAME_NO_ACCESS: &str = "NoAccess";
135
136 #[api()]
137 #[repr(u64)]
138 #[derive(Serialize, Deserialize)]
139 /// Enum representing roles via their [PRIVILEGES] combination.
140 ///
141 /// Since privileges are implemented as bitflags, each unique combination of privileges maps to a
142 /// single, unique `u64` value that is used in this enum definition.
143 pub enum Role {
144 /// Administrator
145 Admin = ROLE_ADMIN,
146 /// Auditor
147 Audit = ROLE_AUDIT,
148 /// Disable Access
149 NoAccess = ROLE_NO_ACCESS,
150 /// Datastore Administrator
151 DatastoreAdmin = ROLE_DATASTORE_ADMIN,
152 /// Datastore Reader (inspect datastore content and do restores)
153 DatastoreReader = ROLE_DATASTORE_READER,
154 /// Datastore Backup (backup and restore owned backups)
155 DatastoreBackup = ROLE_DATASTORE_BACKUP,
156 /// Datastore PowerUser (backup, restore and prune owned backup)
157 DatastorePowerUser = ROLE_DATASTORE_POWERUSER,
158 /// Datastore Auditor
159 DatastoreAudit = ROLE_DATASTORE_AUDIT,
160 /// Remote Auditor
161 RemoteAudit = ROLE_REMOTE_AUDIT,
162 /// Remote Administrator
163 RemoteAdmin = ROLE_REMOTE_ADMIN,
164 /// Syncronisation Opertator
165 RemoteSyncOperator = ROLE_REMOTE_SYNC_OPERATOR,
166 }
167
168 impl FromStr for Role {
169 type Err = value::Error;
170
171 fn from_str(s: &str) -> Result<Self, Self::Err> {
172 Self::deserialize(s.into_deserializer())
173 }
174 }
175
176 lazy_static! {
177 /// Map of pre-defined [Roles](Role) to their associated [privileges](PRIVILEGES) combination and
178 /// description.
179 pub static ref ROLE_NAMES: HashMap<&'static str, (u64, &'static str)> = {
180 let mut map = HashMap::new();
181
182 let list = match Role::API_SCHEMA {
183 Schema::String(StringSchema { format: Some(ApiStringFormat::Enum(list)), .. }) => list,
184 _ => unreachable!(),
185 };
186
187 for entry in list.iter() {
188 let privs: u64 = Role::from_str(entry.value).unwrap() as u64;
189 map.insert(entry.value, (privs, entry.description));
190 }
191
192 map
193 };
194 }
195
196 pub(crate) fn split_acl_path(path: &str) -> Vec<&str> {
197 let items = path.split('/');
198
199 let mut components = vec![];
200
201 for name in items {
202 if name.is_empty() {
203 continue;
204 }
205 components.push(name);
206 }
207
208 components
209 }
210
211 /// Check whether a given ACL `path` conforms to the expected schema.
212 ///
213 /// Currently this just checks for the number of components for various sub-trees.
214 pub fn check_acl_path(path: &str) -> Result<(), Error> {
215 let components = split_acl_path(path);
216
217 let components_len = components.len();
218
219 if components_len == 0 {
220 return Ok(());
221 }
222 match components[0] {
223 "access" => {
224 if components_len == 1 {
225 return Ok(());
226 }
227 match components[1] {
228 "acl" | "users" => {
229 if components_len == 2 {
230 return Ok(());
231 }
232 }
233 _ => {}
234 }
235 }
236 "datastore" => {
237 // /datastore/{store}
238 if components_len <= 2 {
239 return Ok(());
240 }
241 }
242 "remote" => {
243 // /remote/{remote}/{store}
244 if components_len <= 3 {
245 return Ok(());
246 }
247 }
248 "system" => {
249 if components_len == 1 {
250 return Ok(());
251 }
252 match components[1] {
253 "disks" | "log" | "status" | "tasks" | "time" => {
254 if components_len == 2 {
255 return Ok(());
256 }
257 }
258 "services" => {
259 // /system/services/{service}
260 if components_len <= 3 {
261 return Ok(());
262 }
263 }
264 "network" => {
265 if components_len == 2 {
266 return Ok(());
267 }
268 match components[2] {
269 "dns" => {
270 if components_len == 3 {
271 return Ok(());
272 }
273 }
274 "interfaces" => {
275 // /system/network/interfaces/{iface}
276 if components_len <= 4 {
277 return Ok(());
278 }
279 }
280 _ => {}
281 }
282 }
283 _ => {}
284 }
285 }
286 _ => {}
287 }
288
289 bail!("invalid acl path '{}'.", path);
290 }
291
292 /// Tree representing a parsed acl.cfg
293 pub struct AclTree {
294 /// Root node of the tree.
295 ///
296 /// The rest of the tree is available via [find_node()](AclTree::find_node()) or an
297 /// [AclTreeNode]'s [children](AclTreeNode::children) member.
298 pub root: AclTreeNode,
299 }
300
301 /// Node representing ACLs for a certain ACL path.
302 pub struct AclTreeNode {
303 /// [User](crate::config::user::User) or
304 /// [Token](crate::config::user::ApiToken) ACLs for this node.
305 pub users: HashMap<Authid, HashMap<String, bool>>,
306 /// `Group` ACLs for this node (not yet implemented)
307 pub groups: HashMap<String, HashMap<String, bool>>,
308 /// `AclTreeNodes` representing ACL paths directly below the current one.
309 pub children: BTreeMap<String, AclTreeNode>,
310 }
311
312 impl AclTreeNode {
313 /// Creates a new, empty AclTreeNode.
314 pub fn new() -> Self {
315 Self {
316 users: HashMap::new(),
317 groups: HashMap::new(),
318 children: BTreeMap::new(),
319 }
320 }
321
322 /// Returns applicable [Role] and their propagation status for a given
323 /// [Authid](crate::api2::types::Authid).
324 ///
325 /// If the `Authid` is a [User](crate::config::user::User) that has no specific `Roles` configured on this node,
326 /// applicable `Group` roles will be returned instead.
327 ///
328 /// If `leaf` is `false`, only those roles where the propagate flag in the ACL is set to `true`
329 /// are returned. Otherwise, all roles will be returned.
330 pub fn extract_roles(&self, auth_id: &Authid, leaf: bool) -> HashMap<String, bool> {
331 let user_roles = self.extract_user_roles(auth_id, leaf);
332 if !user_roles.is_empty() || auth_id.is_token() {
333 // user privs always override group privs
334 return user_roles;
335 };
336
337 self.extract_group_roles(auth_id.user(), leaf)
338 }
339
340 fn extract_user_roles(&self, auth_id: &Authid, leaf: bool) -> HashMap<String, bool> {
341 let mut map = HashMap::new();
342
343 let roles = match self.users.get(auth_id) {
344 Some(m) => m,
345 None => return map,
346 };
347
348 for (role, propagate) in roles {
349 if *propagate || leaf {
350 if role == ROLE_NAME_NO_ACCESS {
351 // return a map with a single role 'NoAccess'
352 let mut map = HashMap::new();
353 map.insert(role.to_string(), false);
354 return map;
355 }
356 map.insert(role.to_string(), *propagate);
357 }
358 }
359
360 map
361 }
362
363 fn extract_group_roles(&self, _user: &Userid, leaf: bool) -> HashMap<String, bool> {
364 let mut map = HashMap::new();
365
366 for (_group, roles) in &self.groups {
367 let is_member = false; // fixme: check if user is member of the group
368 if !is_member {
369 continue;
370 }
371
372 for (role, propagate) in roles {
373 if *propagate || leaf {
374 if role == ROLE_NAME_NO_ACCESS {
375 // return a map with a single role 'NoAccess'
376 let mut map = HashMap::new();
377 map.insert(role.to_string(), false);
378 return map;
379 }
380 map.insert(role.to_string(), *propagate);
381 }
382 }
383 }
384
385 map
386 }
387
388 fn delete_group_role(&mut self, group: &str, role: &str) {
389 let roles = match self.groups.get_mut(group) {
390 Some(r) => r,
391 None => return,
392 };
393 roles.remove(role);
394 }
395
396 fn delete_user_role(&mut self, auth_id: &Authid, role: &str) {
397 let roles = match self.users.get_mut(auth_id) {
398 Some(r) => r,
399 None => return,
400 };
401 roles.remove(role);
402 }
403
404 fn insert_group_role(&mut self, group: String, role: String, propagate: bool) {
405 let map = self.groups.entry(group).or_insert_with(HashMap::new);
406 if role == ROLE_NAME_NO_ACCESS {
407 map.clear();
408 map.insert(role, propagate);
409 } else {
410 map.remove(ROLE_NAME_NO_ACCESS);
411 map.insert(role, propagate);
412 }
413 }
414
415 fn insert_user_role(&mut self, auth_id: Authid, role: String, propagate: bool) {
416 let map = self.users.entry(auth_id).or_insert_with(HashMap::new);
417 if role == ROLE_NAME_NO_ACCESS {
418 map.clear();
419 map.insert(role, propagate);
420 } else {
421 map.remove(ROLE_NAME_NO_ACCESS);
422 map.insert(role, propagate);
423 }
424 }
425 }
426
427 impl AclTree {
428 /// Create a new, empty ACL tree with a single, empty root [node](AclTreeNode)
429 pub fn new() -> Self {
430 Self {
431 root: AclTreeNode::new(),
432 }
433 }
434
435 /// Iterates over the tree looking for a node matching `path`.
436 pub fn find_node(&mut self, path: &str) -> Option<&mut AclTreeNode> {
437 let path = split_acl_path(path);
438 return self.get_node(&path);
439 }
440
441 fn get_node(&mut self, path: &[&str]) -> Option<&mut AclTreeNode> {
442 let mut node = &mut self.root;
443 for comp in path {
444 node = match node.children.get_mut(*comp) {
445 Some(n) => n,
446 None => return None,
447 };
448 }
449 Some(node)
450 }
451
452 fn get_or_insert_node(&mut self, path: &[&str]) -> &mut AclTreeNode {
453 let mut node = &mut self.root;
454 for comp in path {
455 node = node
456 .children
457 .entry(String::from(*comp))
458 .or_insert_with(AclTreeNode::new);
459 }
460 node
461 }
462
463 /// Deletes the specified `role` from the `group`'s ACL on `path`.
464 ///
465 /// Never fails, even if the `path` has no ACLs configured, or the `group`/`role` combination
466 /// does not exist on `path`.
467 pub fn delete_group_role(&mut self, path: &str, group: &str, role: &str) {
468 let path = split_acl_path(path);
469 let node = match self.get_node(&path) {
470 Some(n) => n,
471 None => return,
472 };
473 node.delete_group_role(group, role);
474 }
475
476 /// Deletes the specified `role` from the `user`'s ACL on `path`.
477 ///
478 /// Never fails, even if the `path` has no ACLs configured, or the `user`/`role` combination
479 /// does not exist on `path`.
480 pub fn delete_user_role(&mut self, path: &str, auth_id: &Authid, role: &str) {
481 let path = split_acl_path(path);
482 let node = match self.get_node(&path) {
483 Some(n) => n,
484 None => return,
485 };
486 node.delete_user_role(auth_id, role);
487 }
488
489 /// Inserts the specified `role` into the `group` ACL on `path`.
490 ///
491 /// The [AclTreeNode] representing `path` will be created and inserted into the tree if
492 /// necessary.
493 pub fn insert_group_role(&mut self, path: &str, group: &str, role: &str, propagate: bool) {
494 let path = split_acl_path(path);
495 let node = self.get_or_insert_node(&path);
496 node.insert_group_role(group.to_string(), role.to_string(), propagate);
497 }
498
499 /// Inserts the specified `role` into the `user` ACL on `path`.
500 ///
501 /// The [AclTreeNode] representing `path` will be created and inserted into the tree if
502 /// necessary.
503 pub fn insert_user_role(&mut self, path: &str, auth_id: &Authid, role: &str, propagate: bool) {
504 let path = split_acl_path(path);
505 let node = self.get_or_insert_node(&path);
506 node.insert_user_role(auth_id.to_owned(), role.to_string(), propagate);
507 }
508
509 fn write_node_config(node: &AclTreeNode, path: &str, w: &mut dyn Write) -> Result<(), Error> {
510 let mut role_ug_map0 = HashMap::new();
511 let mut role_ug_map1 = HashMap::new();
512
513 for (auth_id, roles) in &node.users {
514 // no need to save, because root is always 'Administrator'
515 if !auth_id.is_token() && auth_id.user() == "root@pam" {
516 continue;
517 }
518 for (role, propagate) in roles {
519 let role = role.as_str();
520 let auth_id = auth_id.to_string();
521 if *propagate {
522 role_ug_map1
523 .entry(role)
524 .or_insert_with(BTreeSet::new)
525 .insert(auth_id);
526 } else {
527 role_ug_map0
528 .entry(role)
529 .or_insert_with(BTreeSet::new)
530 .insert(auth_id);
531 }
532 }
533 }
534
535 for (group, roles) in &node.groups {
536 for (role, propagate) in roles {
537 let group = format!("@{}", group);
538 if *propagate {
539 role_ug_map1
540 .entry(role)
541 .or_insert_with(BTreeSet::new)
542 .insert(group);
543 } else {
544 role_ug_map0
545 .entry(role)
546 .or_insert_with(BTreeSet::new)
547 .insert(group);
548 }
549 }
550 }
551
552 fn group_by_property_list(
553 item_property_map: &HashMap<&str, BTreeSet<String>>,
554 ) -> BTreeMap<String, BTreeSet<String>> {
555 let mut result_map = BTreeMap::new();
556 for (item, property_map) in item_property_map {
557 let item_list = property_map.iter().fold(String::new(), |mut acc, v| {
558 if !acc.is_empty() {
559 acc.push(',');
560 }
561 acc.push_str(v);
562 acc
563 });
564 result_map
565 .entry(item_list)
566 .or_insert_with(BTreeSet::new)
567 .insert(item.to_string());
568 }
569 result_map
570 }
571
572 let uglist_role_map0 = group_by_property_list(&role_ug_map0);
573 let uglist_role_map1 = group_by_property_list(&role_ug_map1);
574
575 fn role_list(roles: &BTreeSet<String>) -> String {
576 if roles.contains(ROLE_NAME_NO_ACCESS) {
577 return String::from(ROLE_NAME_NO_ACCESS);
578 }
579 roles.iter().fold(String::new(), |mut acc, v| {
580 if !acc.is_empty() {
581 acc.push(',');
582 }
583 acc.push_str(v);
584 acc
585 })
586 }
587
588 for (uglist, roles) in &uglist_role_map0 {
589 let role_list = role_list(roles);
590 writeln!(
591 w,
592 "acl:0:{}:{}:{}",
593 if path.is_empty() { "/" } else { path },
594 uglist,
595 role_list
596 )?;
597 }
598
599 for (uglist, roles) in &uglist_role_map1 {
600 let role_list = role_list(roles);
601 writeln!(
602 w,
603 "acl:1:{}:{}:{}",
604 if path.is_empty() { "/" } else { path },
605 uglist,
606 role_list
607 )?;
608 }
609
610 for (name, child) in node.children.iter() {
611 let child_path = format!("{}/{}", path, name);
612 Self::write_node_config(child, &child_path, w)?;
613 }
614
615 Ok(())
616 }
617
618 fn write_config(&self, w: &mut dyn Write) -> Result<(), Error> {
619 Self::write_node_config(&self.root, "", w)
620 }
621
622 fn parse_acl_line(&mut self, line: &str) -> Result<(), Error> {
623 let items: Vec<&str> = line.split(':').collect();
624
625 if items.len() != 5 {
626 bail!("wrong number of items.");
627 }
628
629 if items[0] != "acl" {
630 bail!("line does not start with 'acl'.");
631 }
632
633 let propagate = if items[1] == "0" {
634 false
635 } else if items[1] == "1" {
636 true
637 } else {
638 bail!("expected '0' or '1' for propagate flag.");
639 };
640
641 let path_str = items[2];
642 let path = split_acl_path(path_str);
643 let node = self.get_or_insert_node(&path);
644
645 let uglist: Vec<&str> = items[3].split(',').map(|v| v.trim()).collect();
646
647 let rolelist: Vec<&str> = items[4].split(',').map(|v| v.trim()).collect();
648
649 for user_or_group in &uglist {
650 for role in &rolelist {
651 if !ROLE_NAMES.contains_key(role) {
652 bail!("unknown role '{}'", role);
653 }
654 if user_or_group.starts_with('@') {
655 let group = &user_or_group[1..];
656 node.insert_group_role(group.to_string(), role.to_string(), propagate);
657 } else {
658 node.insert_user_role(user_or_group.parse()?, role.to_string(), propagate);
659 }
660 }
661 }
662
663 Ok(())
664 }
665
666 fn load(filename: &Path) -> Result<(Self, [u8; 32]), Error> {
667 let mut tree = Self::new();
668
669 let raw = match std::fs::read_to_string(filename) {
670 Ok(v) => v,
671 Err(err) => {
672 if err.kind() == std::io::ErrorKind::NotFound {
673 String::new()
674 } else {
675 bail!("unable to read acl config {:?} - {}", filename, err);
676 }
677 }
678 };
679
680 let digest = openssl::sha::sha256(raw.as_bytes());
681
682 for (linenr, line) in raw.lines().enumerate() {
683 let line = line.trim();
684 if line.is_empty() {
685 continue;
686 }
687 if let Err(err) = tree.parse_acl_line(line) {
688 bail!(
689 "unable to parse acl config {:?}, line {} - {}",
690 filename,
691 linenr + 1,
692 err
693 );
694 }
695 }
696
697 Ok((tree, digest))
698 }
699
700 #[cfg(test)]
701 pub(crate) fn from_raw(raw: &str) -> Result<Self, Error> {
702 let mut tree = Self::new();
703 for (linenr, line) in raw.lines().enumerate() {
704 let line = line.trim();
705 if line.is_empty() {
706 continue;
707 }
708 if let Err(err) = tree.parse_acl_line(line) {
709 bail!(
710 "unable to parse acl config data, line {} - {}",
711 linenr + 1,
712 err
713 );
714 }
715 }
716 Ok(tree)
717 }
718
719 /// Returns a map of role name and propagation status for a given `auth_id` and `path`.
720 ///
721 /// This will collect role mappings according to the following algorithm:
722 /// - iterate over all intermediate nodes along `path` and collect roles with `propagate` set
723 /// - get all (propagating and non-propagating) roles for last component of path
724 /// - more specific role maps replace less specific role maps
725 /// -- user/token is more specific than group at each level
726 /// -- roles lower in the tree are more specific than those higher up along the path
727 pub fn roles(&self, auth_id: &Authid, path: &[&str]) -> HashMap<String, bool> {
728 let mut node = &self.root;
729 let mut role_map = node.extract_roles(auth_id, path.is_empty());
730
731 for (pos, comp) in path.iter().enumerate() {
732 let last_comp = (pos + 1) == path.len();
733 node = match node.children.get(*comp) {
734 Some(n) => n,
735 None => return role_map, // path not found
736 };
737
738 let new_map = node.extract_roles(auth_id, last_comp);
739 if !new_map.is_empty() {
740 // overwrite previous maptings
741 role_map = new_map;
742 }
743 }
744
745 role_map
746 }
747 }
748
749 /// Filename where [AclTree] is stored.
750 pub const ACL_CFG_FILENAME: &str = "/etc/proxmox-backup/acl.cfg";
751 /// Path used to lock the [AclTree] when modifying.
752 pub const ACL_CFG_LOCKFILE: &str = "/etc/proxmox-backup/.acl.lck";
753
754 /// Reads the [AclTree] from the [default path](ACL_CFG_FILENAME).
755 pub fn config() -> Result<(AclTree, [u8; 32]), Error> {
756 let path = PathBuf::from(ACL_CFG_FILENAME);
757 AclTree::load(&path)
758 }
759
760 /// Returns a cached [AclTree] or fresh copy read directly from the [default path](ACL_CFG_FILENAME)
761 ///
762 /// Since the AclTree is used for every API request's permission check, this caching mechanism
763 /// allows to skip reading and parsing the file again if it is unchanged.
764 pub fn cached_config() -> Result<Arc<AclTree>, Error> {
765 struct ConfigCache {
766 data: Option<Arc<AclTree>>,
767 last_mtime: i64,
768 last_mtime_nsec: i64,
769 }
770
771 lazy_static! {
772 static ref CACHED_CONFIG: RwLock<ConfigCache> = RwLock::new(ConfigCache {
773 data: None,
774 last_mtime: 0,
775 last_mtime_nsec: 0
776 });
777 }
778
779 let stat = match nix::sys::stat::stat(ACL_CFG_FILENAME) {
780 Ok(stat) => Some(stat),
781 Err(nix::Error::Sys(nix::errno::Errno::ENOENT)) => None,
782 Err(err) => bail!("unable to stat '{}' - {}", ACL_CFG_FILENAME, err),
783 };
784
785 {
786 // limit scope
787 let cache = CACHED_CONFIG.read().unwrap();
788 if let Some(ref config) = cache.data {
789 if let Some(stat) = stat {
790 if stat.st_mtime == cache.last_mtime && stat.st_mtime_nsec == cache.last_mtime_nsec
791 {
792 return Ok(config.clone());
793 }
794 } else if cache.last_mtime == 0 && cache.last_mtime_nsec == 0 {
795 return Ok(config.clone());
796 }
797 }
798 }
799
800 let (config, _digest) = config()?;
801 let config = Arc::new(config);
802
803 let mut cache = CACHED_CONFIG.write().unwrap();
804 if let Some(stat) = stat {
805 cache.last_mtime = stat.st_mtime;
806 cache.last_mtime_nsec = stat.st_mtime_nsec;
807 }
808 cache.data = Some(config.clone());
809
810 Ok(config)
811 }
812
813 /// Saves an [AclTree] to the [default path](ACL_CFG_FILENAME), ensuring proper ownership and
814 /// file permissions.
815 pub fn save_config(acl: &AclTree) -> Result<(), Error> {
816 let mut raw: Vec<u8> = Vec::new();
817
818 acl.write_config(&mut raw)?;
819
820 let backup_user = crate::backup::backup_user()?;
821 let mode = nix::sys::stat::Mode::from_bits_truncate(0o0640);
822 // set the correct owner/group/permissions while saving file
823 // owner(rw) = root, group(r)= backup
824 let options = CreateOptions::new()
825 .perm(mode)
826 .owner(nix::unistd::ROOT)
827 .group(backup_user.gid);
828
829 replace_file(ACL_CFG_FILENAME, &raw, options)?;
830
831 Ok(())
832 }
833
834 #[cfg(test)]
835 mod test {
836 use super::AclTree;
837 use anyhow::Error;
838
839 use crate::api2::types::Authid;
840
841 fn check_roles(tree: &AclTree, auth_id: &Authid, path: &str, expected_roles: &str) {
842 let path_vec = super::split_acl_path(path);
843 let mut roles = tree
844 .roles(auth_id, &path_vec)
845 .iter()
846 .map(|(v, _)| v.clone())
847 .collect::<Vec<String>>();
848 roles.sort();
849 let roles = roles.join(",");
850
851 assert_eq!(
852 roles, expected_roles,
853 "\nat check_roles for '{}' on '{}'",
854 auth_id, path
855 );
856 }
857
858 #[test]
859 fn test_acl_line_compression() {
860 let tree = AclTree::from_raw(
861 "\
862 acl:0:/store/store2:user1@pbs:Admin\n\
863 acl:0:/store/store2:user2@pbs:Admin\n\
864 acl:0:/store/store2:user1@pbs:DatastoreBackup\n\
865 acl:0:/store/store2:user2@pbs:DatastoreBackup\n\
866 ",
867 )
868 .expect("failed to parse acl tree");
869
870 let mut raw: Vec<u8> = Vec::new();
871 tree.write_config(&mut raw)
872 .expect("failed to write acl tree");
873 let raw = std::str::from_utf8(&raw).expect("acl tree is not valid utf8");
874
875 assert_eq!(
876 raw,
877 "acl:0:/store/store2:user1@pbs,user2@pbs:Admin,DatastoreBackup\n"
878 );
879 }
880
881 #[test]
882 fn test_roles_1() -> Result<(), Error> {
883 let tree = AclTree::from_raw(
884 r###"
885 acl:1:/storage:user1@pbs:Admin
886 acl:1:/storage/store1:user1@pbs:DatastoreBackup
887 acl:1:/storage/store2:user2@pbs:DatastoreBackup
888 "###,
889 )?;
890 let user1: Authid = "user1@pbs".parse()?;
891 check_roles(&tree, &user1, "/", "");
892 check_roles(&tree, &user1, "/storage", "Admin");
893 check_roles(&tree, &user1, "/storage/store1", "DatastoreBackup");
894 check_roles(&tree, &user1, "/storage/store2", "Admin");
895
896 let user2: Authid = "user2@pbs".parse()?;
897 check_roles(&tree, &user2, "/", "");
898 check_roles(&tree, &user2, "/storage", "");
899 check_roles(&tree, &user2, "/storage/store1", "");
900 check_roles(&tree, &user2, "/storage/store2", "DatastoreBackup");
901
902 Ok(())
903 }
904
905 #[test]
906 fn test_role_no_access() -> Result<(), Error> {
907 let tree = AclTree::from_raw(
908 r###"
909 acl:1:/:user1@pbs:Admin
910 acl:1:/storage:user1@pbs:NoAccess
911 acl:1:/storage/store1:user1@pbs:DatastoreBackup
912 "###,
913 )?;
914 let user1: Authid = "user1@pbs".parse()?;
915 check_roles(&tree, &user1, "/", "Admin");
916 check_roles(&tree, &user1, "/storage", "NoAccess");
917 check_roles(&tree, &user1, "/storage/store1", "DatastoreBackup");
918 check_roles(&tree, &user1, "/storage/store2", "NoAccess");
919 check_roles(&tree, &user1, "/system", "Admin");
920
921 let tree = AclTree::from_raw(
922 r###"
923 acl:1:/:user1@pbs:Admin
924 acl:0:/storage:user1@pbs:NoAccess
925 acl:1:/storage/store1:user1@pbs:DatastoreBackup
926 "###,
927 )?;
928 check_roles(&tree, &user1, "/", "Admin");
929 check_roles(&tree, &user1, "/storage", "NoAccess");
930 check_roles(&tree, &user1, "/storage/store1", "DatastoreBackup");
931 check_roles(&tree, &user1, "/storage/store2", "Admin");
932 check_roles(&tree, &user1, "/system", "Admin");
933
934 Ok(())
935 }
936
937 #[test]
938 fn test_role_add_delete() -> Result<(), Error> {
939 let mut tree = AclTree::new();
940
941 let user1: Authid = "user1@pbs".parse()?;
942
943 tree.insert_user_role("/", &user1, "Admin", true);
944 tree.insert_user_role("/", &user1, "Audit", true);
945
946 check_roles(&tree, &user1, "/", "Admin,Audit");
947
948 tree.insert_user_role("/", &user1, "NoAccess", true);
949 check_roles(&tree, &user1, "/", "NoAccess");
950
951 let mut raw: Vec<u8> = Vec::new();
952 tree.write_config(&mut raw)?;
953 let raw = std::str::from_utf8(&raw)?;
954
955 assert_eq!(raw, "acl:1:/:user1@pbs:NoAccess\n");
956
957 Ok(())
958 }
959
960 #[test]
961 fn test_no_access_overwrite() -> Result<(), Error> {
962 let mut tree = AclTree::new();
963
964 let user1: Authid = "user1@pbs".parse()?;
965
966 tree.insert_user_role("/storage", &user1, "NoAccess", true);
967
968 check_roles(&tree, &user1, "/storage", "NoAccess");
969
970 tree.insert_user_role("/storage", &user1, "Admin", true);
971 tree.insert_user_role("/storage", &user1, "Audit", true);
972
973 check_roles(&tree, &user1, "/storage", "Admin,Audit");
974
975 tree.insert_user_role("/storage", &user1, "NoAccess", true);
976
977 check_roles(&tree, &user1, "/storage", "NoAccess");
978
979 Ok(())
980 }
981 }