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