1 use std
::collections
::{BTreeMap, BTreeSet, HashMap}
;
3 use std
::path
::{Path, PathBuf}
;
5 use std
::sync
::{Arc, RwLock}
;
7 use anyhow
::{bail, Error}
;
9 use lazy_static
::lazy_static
;
11 use proxmox
::api
::schema
::{Schema, StringSchema, ApiStringFormat, ApiType}
;
13 use pbs_api_types
::{Authid, Userid, Role, ROLE_NAME_NO_ACCESS}
;
15 use crate::{open_backup_lockfile, replace_backup_config, BackupLockGuard}
;
18 /// Map of pre-defined [Roles](Role) to their associated [privileges](PRIVILEGES) combination and
20 pub static ref ROLE_NAMES
: HashMap
<&'
static str, (u64, &'
static str)> = {
21 let mut map
= HashMap
::new();
23 let list
= match Role
::API_SCHEMA
{
24 Schema
::String(StringSchema { format: Some(ApiStringFormat::Enum(list)), .. }
) => list
,
28 for entry
in list
.iter() {
29 let privs
: u64 = Role
::from_str(entry
.value
).unwrap() as u64;
30 map
.insert(entry
.value
, (privs
, entry
.description
));
37 pub fn split_acl_path(path
: &str) -> Vec
<&str> {
38 let items
= path
.split('
/'
);
40 let mut components
= vec
![];
46 components
.push(name
);
52 /// Check whether a given ACL `path` conforms to the expected schema.
54 /// Currently this just checks for the number of components for various sub-trees.
55 pub fn check_acl_path(path
: &str) -> Result
<(), Error
> {
56 let components
= split_acl_path(path
);
58 let components_len
= components
.len();
60 if components_len
== 0 {
65 if components_len
== 1 {
69 "acl" | "users" | "domains" => {
70 if components_len
== 2 {
74 // /access/openid/{endpoint}
76 if components_len
<= 3 {
85 if components_len
<= 2 {
90 // /remote/{remote}/{store}
91 if components_len
<= 3 {
96 if components_len
== 1 {
100 "certificates" | "disks" | "log" | "status" | "tasks" | "time" => {
101 if components_len
== 2 {
106 // /system/services/{service}
107 if components_len
<= 3 {
112 if components_len
== 2 {
115 match components
[2] {
117 if components_len
== 3 {
122 // /system/network/interfaces/{iface}
123 if components_len
<= 4 {
134 if components_len
== 1 {
137 match components
[1] {
139 // /tape/device/{name}
140 if components_len
<= 3 {
146 if components_len
<= 3 {
152 if components_len
<= 3 {
162 bail
!("invalid acl path '{}'.", path
);
165 /// Tree representing a parsed acl.cfg
168 /// Root node of the tree.
170 /// The rest of the tree is available via [find_node()](AclTree::find_node()) or an
171 /// [AclTreeNode]'s [children](AclTreeNode::children) member.
172 pub root
: AclTreeNode
,
175 /// Node representing ACLs for a certain ACL path.
177 pub struct AclTreeNode
{
178 /// [User](crate::config::user::User) or
179 /// [Token](crate::config::user::ApiToken) ACLs for this node.
180 pub users
: HashMap
<Authid
, HashMap
<String
, bool
>>,
181 /// `Group` ACLs for this node (not yet implemented)
182 pub groups
: HashMap
<String
, HashMap
<String
, bool
>>,
183 /// `AclTreeNodes` representing ACL paths directly below the current one.
184 pub children
: BTreeMap
<String
, AclTreeNode
>,
188 /// Creates a new, empty AclTreeNode.
189 pub fn new() -> Self {
191 users
: HashMap
::new(),
192 groups
: HashMap
::new(),
193 children
: BTreeMap
::new(),
197 /// Returns applicable [Role] and their propagation status for a given
198 /// [Authid](crate::api2::types::Authid).
200 /// If the `Authid` is a [User](crate::config::user::User) that has no specific `Roles` configured on this node,
201 /// applicable `Group` roles will be returned instead.
203 /// If `leaf` is `false`, only those roles where the propagate flag in the ACL is set to `true`
204 /// are returned. Otherwise, all roles will be returned.
205 pub fn extract_roles(&self, auth_id
: &Authid
, leaf
: bool
) -> HashMap
<String
, bool
> {
206 let user_roles
= self.extract_user_roles(auth_id
, leaf
);
207 if !user_roles
.is_empty() || auth_id
.is_token() {
208 // user privs always override group privs
212 self.extract_group_roles(auth_id
.user(), leaf
)
215 fn extract_user_roles(&self, auth_id
: &Authid
, leaf
: bool
) -> HashMap
<String
, bool
> {
216 let mut map
= HashMap
::new();
218 let roles
= match self.users
.get(auth_id
) {
223 for (role
, propagate
) in roles
{
224 if *propagate
|| leaf
{
225 if role
== ROLE_NAME_NO_ACCESS
{
226 // return a map with a single role 'NoAccess'
227 let mut map
= HashMap
::new();
228 map
.insert(role
.to_string(), false);
231 map
.insert(role
.to_string(), *propagate
);
238 fn extract_group_roles(&self, _user
: &Userid
, leaf
: bool
) -> HashMap
<String
, bool
> {
239 let mut map
= HashMap
::new();
241 #[allow(clippy::for_kv_map)]
242 for (_group
, roles
) in &self.groups
{
243 let is_member
= false; // fixme: check if user is member of the group
248 for (role
, propagate
) in roles
{
249 if *propagate
|| leaf
{
250 if role
== ROLE_NAME_NO_ACCESS
{
251 // return a map with a single role 'NoAccess'
252 let mut map
= HashMap
::new();
253 map
.insert(role
.to_string(), false);
256 map
.insert(role
.to_string(), *propagate
);
264 fn delete_group_role(&mut self, group
: &str, role
: &str) {
265 let roles
= match self.groups
.get_mut(group
) {
272 fn delete_user_role(&mut self, auth_id
: &Authid
, role
: &str) {
273 let roles
= match self.users
.get_mut(auth_id
) {
280 fn insert_group_role(&mut self, group
: String
, role
: String
, propagate
: bool
) {
281 let map
= self.groups
.entry(group
).or_default();
282 if role
== ROLE_NAME_NO_ACCESS
{
284 map
.insert(role
, propagate
);
286 map
.remove(ROLE_NAME_NO_ACCESS
);
287 map
.insert(role
, propagate
);
291 fn insert_user_role(&mut self, auth_id
: Authid
, role
: String
, propagate
: bool
) {
292 let map
= self.users
.entry(auth_id
).or_default();
293 if role
== ROLE_NAME_NO_ACCESS
{
295 map
.insert(role
, propagate
);
297 map
.remove(ROLE_NAME_NO_ACCESS
);
298 map
.insert(role
, propagate
);
304 /// Create a new, empty ACL tree with a single, empty root [node](AclTreeNode)
305 pub fn new() -> Self {
307 root
: AclTreeNode
::new(),
311 /// Iterates over the tree looking for a node matching `path`.
312 pub fn find_node(&mut self, path
: &str) -> Option
<&mut AclTreeNode
> {
313 let path
= split_acl_path(path
);
317 fn get_node(&mut self, path
: &[&str]) -> Option
<&mut AclTreeNode
> {
318 let mut node
= &mut self.root
;
320 node
= match node
.children
.get_mut(*comp
) {
328 fn get_or_insert_node(&mut self, path
: &[&str]) -> &mut AclTreeNode
{
329 let mut node
= &mut self.root
;
333 .entry(String
::from(*comp
))
339 /// Deletes the specified `role` from the `group`'s ACL on `path`.
341 /// Never fails, even if the `path` has no ACLs configured, or the `group`/`role` combination
342 /// does not exist on `path`.
343 pub fn delete_group_role(&mut self, path
: &str, group
: &str, role
: &str) {
344 let path
= split_acl_path(path
);
345 let node
= match self.get_node(&path
) {
349 node
.delete_group_role(group
, role
);
352 /// Deletes the specified `role` from the `user`'s ACL on `path`.
354 /// Never fails, even if the `path` has no ACLs configured, or the `user`/`role` combination
355 /// does not exist on `path`.
356 pub fn delete_user_role(&mut self, path
: &str, auth_id
: &Authid
, role
: &str) {
357 let path
= split_acl_path(path
);
358 let node
= match self.get_node(&path
) {
362 node
.delete_user_role(auth_id
, role
);
365 /// Inserts the specified `role` into the `group` ACL on `path`.
367 /// The [AclTreeNode] representing `path` will be created and inserted into the tree if
369 pub fn insert_group_role(&mut self, path
: &str, group
: &str, role
: &str, propagate
: bool
) {
370 let path
= split_acl_path(path
);
371 let node
= self.get_or_insert_node(&path
);
372 node
.insert_group_role(group
.to_string(), role
.to_string(), propagate
);
375 /// Inserts the specified `role` into the `user` ACL on `path`.
377 /// The [AclTreeNode] representing `path` will be created and inserted into the tree if
379 pub fn insert_user_role(&mut self, path
: &str, auth_id
: &Authid
, role
: &str, propagate
: bool
) {
380 let path
= split_acl_path(path
);
381 let node
= self.get_or_insert_node(&path
);
382 node
.insert_user_role(auth_id
.to_owned(), role
.to_string(), propagate
);
385 fn write_node_config(node
: &AclTreeNode
, path
: &str, w
: &mut dyn Write
) -> Result
<(), Error
> {
386 let mut role_ug_map0
= HashMap
::new();
387 let mut role_ug_map1
= HashMap
::new();
389 for (auth_id
, roles
) in &node
.users
{
390 // no need to save, because root is always 'Administrator'
391 if !auth_id
.is_token() && auth_id
.user() == "root@pam" {
394 for (role
, propagate
) in roles
{
395 let role
= role
.as_str();
396 let auth_id
= auth_id
.to_string();
400 .or_insert_with(BTreeSet
::new
)
405 .or_insert_with(BTreeSet
::new
)
411 for (group
, roles
) in &node
.groups
{
412 for (role
, propagate
) in roles
{
413 let group
= format
!("@{}", group
);
417 .or_insert_with(BTreeSet
::new
)
422 .or_insert_with(BTreeSet
::new
)
428 fn group_by_property_list(
429 item_property_map
: &HashMap
<&str, BTreeSet
<String
>>,
430 ) -> BTreeMap
<String
, BTreeSet
<String
>> {
431 let mut result_map
= BTreeMap
::new();
432 for (item
, property_map
) in item_property_map
{
433 let item_list
= property_map
.iter().fold(String
::new(), |mut acc
, v
| {
442 .or_insert_with(BTreeSet
::new
)
443 .insert(item
.to_string());
448 let uglist_role_map0
= group_by_property_list(&role_ug_map0
);
449 let uglist_role_map1
= group_by_property_list(&role_ug_map1
);
451 fn role_list(roles
: &BTreeSet
<String
>) -> String
{
452 if roles
.contains(ROLE_NAME_NO_ACCESS
) {
453 return String
::from(ROLE_NAME_NO_ACCESS
);
455 roles
.iter().fold(String
::new(), |mut acc
, v
| {
464 for (uglist
, roles
) in &uglist_role_map0
{
465 let role_list
= role_list(roles
);
469 if path
.is_empty() { "/" }
else { path }
,
475 for (uglist
, roles
) in &uglist_role_map1
{
476 let role_list
= role_list(roles
);
480 if path
.is_empty() { "/" }
else { path }
,
486 for (name
, child
) in node
.children
.iter() {
487 let child_path
= format
!("{}/{}", path
, name
);
488 Self::write_node_config(child
, &child_path
, w
)?
;
494 fn write_config(&self, w
: &mut dyn Write
) -> Result
<(), Error
> {
495 Self::write_node_config(&self.root
, "", w
)
498 fn parse_acl_line(&mut self, line
: &str) -> Result
<(), Error
> {
499 let items
: Vec
<&str> = line
.split('
:'
).collect();
501 if items
.len() != 5 {
502 bail
!("wrong number of items.");
505 if items
[0] != "acl" {
506 bail
!("line does not start with 'acl'.");
509 let propagate
= if items
[1] == "0" {
511 } else if items
[1] == "1" {
514 bail
!("expected '0' or '1' for propagate flag.");
517 let path_str
= items
[2];
518 let path
= split_acl_path(path_str
);
519 let node
= self.get_or_insert_node(&path
);
521 let uglist
: Vec
<&str> = items
[3].split('
,'
).map(|v
| v
.trim()).collect();
523 let rolelist
: Vec
<&str> = items
[4].split('
,'
).map(|v
| v
.trim()).collect();
525 for user_or_group
in &uglist
{
526 for role
in &rolelist
{
527 if !ROLE_NAMES
.contains_key(role
) {
528 bail
!("unknown role '{}'", role
);
530 if let Some(group
) = user_or_group
.strip_prefix('@'
) {
531 node
.insert_group_role(group
.to_string(), role
.to_string(), propagate
);
533 node
.insert_user_role(user_or_group
.parse()?
, role
.to_string(), propagate
);
541 fn load(filename
: &Path
) -> Result
<(Self, [u8; 32]), Error
> {
542 let mut tree
= Self::new();
544 let raw
= match std
::fs
::read_to_string(filename
) {
547 if err
.kind() == std
::io
::ErrorKind
::NotFound
{
550 bail
!("unable to read acl config {:?} - {}", filename
, err
);
555 let digest
= openssl
::sha
::sha256(raw
.as_bytes());
557 for (linenr
, line
) in raw
.lines().enumerate() {
558 let line
= line
.trim();
562 if let Err(err
) = tree
.parse_acl_line(line
) {
564 "unable to parse acl config {:?}, line {} - {}",
575 /// This is used for testing
576 pub fn from_raw(raw
: &str) -> Result
<Self, Error
> {
577 let mut tree
= Self::new();
578 for (linenr
, line
) in raw
.lines().enumerate() {
579 let line
= line
.trim();
583 if let Err(err
) = tree
.parse_acl_line(line
) {
585 "unable to parse acl config data, line {} - {}",
594 /// Returns a map of role name and propagation status for a given `auth_id` and `path`.
596 /// This will collect role mappings according to the following algorithm:
597 /// - iterate over all intermediate nodes along `path` and collect roles with `propagate` set
598 /// - get all (propagating and non-propagating) roles for last component of path
599 /// - more specific role maps replace less specific role maps
600 /// -- user/token is more specific than group at each level
601 /// -- roles lower in the tree are more specific than those higher up along the path
602 pub fn roles(&self, auth_id
: &Authid
, path
: &[&str]) -> HashMap
<String
, bool
> {
603 let mut node
= &self.root
;
604 let mut role_map
= node
.extract_roles(auth_id
, path
.is_empty());
606 for (pos
, comp
) in path
.iter().enumerate() {
607 let last_comp
= (pos
+ 1) == path
.len();
608 node
= match node
.children
.get(*comp
) {
610 None
=> return role_map
, // path not found
613 let new_map
= node
.extract_roles(auth_id
, last_comp
);
614 if !new_map
.is_empty() {
615 // overwrite previous maptings
624 /// Filename where [AclTree] is stored.
625 pub const ACL_CFG_FILENAME
: &str = "/etc/proxmox-backup/acl.cfg";
626 /// Path used to lock the [AclTree] when modifying.
627 pub const ACL_CFG_LOCKFILE
: &str = "/etc/proxmox-backup/.acl.lck";
629 /// Get exclusive lock
630 pub fn lock_config() -> Result
<BackupLockGuard
, Error
> {
631 open_backup_lockfile(ACL_CFG_LOCKFILE
, None
, true)
634 /// Reads the [AclTree] from the [default path](ACL_CFG_FILENAME).
635 pub fn config() -> Result
<(AclTree
, [u8; 32]), Error
> {
636 let path
= PathBuf
::from(ACL_CFG_FILENAME
);
640 /// Returns a cached [AclTree] or fresh copy read directly from the [default path](ACL_CFG_FILENAME)
642 /// Since the AclTree is used for every API request's permission check, this caching mechanism
643 /// allows to skip reading and parsing the file again if it is unchanged.
644 pub fn cached_config() -> Result
<Arc
<AclTree
>, Error
> {
646 data
: Option
<Arc
<AclTree
>>,
648 last_mtime_nsec
: i64,
652 static ref CACHED_CONFIG
: RwLock
<ConfigCache
> = RwLock
::new(ConfigCache
{
659 let stat
= match nix
::sys
::stat
::stat(ACL_CFG_FILENAME
) {
660 Ok(stat
) => Some(stat
),
661 Err(nix
::Error
::Sys(nix
::errno
::Errno
::ENOENT
)) => None
,
662 Err(err
) => bail
!("unable to stat '{}' - {}", ACL_CFG_FILENAME
, err
),
667 let cache
= CACHED_CONFIG
.read().unwrap();
668 if let Some(ref config
) = cache
.data
{
669 if let Some(stat
) = stat
{
670 if stat
.st_mtime
== cache
.last_mtime
&& stat
.st_mtime_nsec
== cache
.last_mtime_nsec
672 return Ok(config
.clone());
674 } else if cache
.last_mtime
== 0 && cache
.last_mtime_nsec
== 0 {
675 return Ok(config
.clone());
680 let (config
, _digest
) = config()?
;
681 let config
= Arc
::new(config
);
683 let mut cache
= CACHED_CONFIG
.write().unwrap();
684 if let Some(stat
) = stat
{
685 cache
.last_mtime
= stat
.st_mtime
;
686 cache
.last_mtime_nsec
= stat
.st_mtime_nsec
;
688 cache
.data
= Some(config
.clone());
693 /// Saves an [AclTree] to the [default path](ACL_CFG_FILENAME), ensuring proper ownership and
694 /// file permissions.
695 pub fn save_config(acl
: &AclTree
) -> Result
<(), Error
> {
696 let mut raw
: Vec
<u8> = Vec
::new();
698 acl
.write_config(&mut raw
)?
;
700 replace_backup_config(ACL_CFG_FILENAME
, &raw
)
708 use pbs_api_types
::Authid
;
710 fn check_roles(tree
: &AclTree
, auth_id
: &Authid
, path
: &str, expected_roles
: &str) {
711 let path_vec
= super::split_acl_path(path
);
713 .roles(auth_id
, &path_vec
)
715 .map(|(v
, _
)| v
.clone())
716 .collect
::<Vec
<String
>>();
718 let roles
= roles
.join(",");
721 roles
, expected_roles
,
722 "\nat check_roles for '{}' on '{}'",
728 fn test_acl_line_compression() {
729 let tree
= AclTree
::from_raw(
731 acl:0:/store/store2:user1@pbs:Admin\n\
732 acl:0:/store/store2:user2@pbs:Admin\n\
733 acl:0:/store/store2:user1@pbs:DatastoreBackup\n\
734 acl:0:/store/store2:user2@pbs:DatastoreBackup\n\
737 .expect("failed to parse acl tree");
739 let mut raw
: Vec
<u8> = Vec
::new();
740 tree
.write_config(&mut raw
)
741 .expect("failed to write acl tree");
742 let raw
= std
::str::from_utf8(&raw
).expect("acl tree is not valid utf8");
746 "acl:0:/store/store2:user1@pbs,user2@pbs:Admin,DatastoreBackup\n"
751 fn test_roles_1() -> Result
<(), Error
> {
752 let tree
= AclTree
::from_raw(
754 acl:1:/storage:user1@pbs:Admin
755 acl:1:/storage/store1:user1@pbs:DatastoreBackup
756 acl:1:/storage/store2:user2@pbs:DatastoreBackup
759 let user1
: Authid
= "user1@pbs".parse()?
;
760 check_roles(&tree
, &user1
, "/", "");
761 check_roles(&tree
, &user1
, "/storage", "Admin");
762 check_roles(&tree
, &user1
, "/storage/store1", "DatastoreBackup");
763 check_roles(&tree
, &user1
, "/storage/store2", "Admin");
765 let user2
: Authid
= "user2@pbs".parse()?
;
766 check_roles(&tree
, &user2
, "/", "");
767 check_roles(&tree
, &user2
, "/storage", "");
768 check_roles(&tree
, &user2
, "/storage/store1", "");
769 check_roles(&tree
, &user2
, "/storage/store2", "DatastoreBackup");
775 fn test_role_no_access() -> Result
<(), Error
> {
776 let tree
= AclTree
::from_raw(
778 acl:1:/:user1@pbs:Admin
779 acl:1:/storage:user1@pbs:NoAccess
780 acl:1:/storage/store1:user1@pbs:DatastoreBackup
783 let user1
: Authid
= "user1@pbs".parse()?
;
784 check_roles(&tree
, &user1
, "/", "Admin");
785 check_roles(&tree
, &user1
, "/storage", "NoAccess");
786 check_roles(&tree
, &user1
, "/storage/store1", "DatastoreBackup");
787 check_roles(&tree
, &user1
, "/storage/store2", "NoAccess");
788 check_roles(&tree
, &user1
, "/system", "Admin");
790 let tree
= AclTree
::from_raw(
792 acl:1:/:user1@pbs:Admin
793 acl:0:/storage:user1@pbs:NoAccess
794 acl:1:/storage/store1:user1@pbs:DatastoreBackup
797 check_roles(&tree
, &user1
, "/", "Admin");
798 check_roles(&tree
, &user1
, "/storage", "NoAccess");
799 check_roles(&tree
, &user1
, "/storage/store1", "DatastoreBackup");
800 check_roles(&tree
, &user1
, "/storage/store2", "Admin");
801 check_roles(&tree
, &user1
, "/system", "Admin");
807 fn test_role_add_delete() -> Result
<(), Error
> {
808 let mut tree
= AclTree
::new();
810 let user1
: Authid
= "user1@pbs".parse()?
;
812 tree
.insert_user_role("/", &user1
, "Admin", true);
813 tree
.insert_user_role("/", &user1
, "Audit", true);
815 check_roles(&tree
, &user1
, "/", "Admin,Audit");
817 tree
.insert_user_role("/", &user1
, "NoAccess", true);
818 check_roles(&tree
, &user1
, "/", "NoAccess");
820 let mut raw
: Vec
<u8> = Vec
::new();
821 tree
.write_config(&mut raw
)?
;
822 let raw
= std
::str::from_utf8(&raw
)?
;
824 assert_eq
!(raw
, "acl:1:/:user1@pbs:NoAccess\n");
830 fn test_no_access_overwrite() -> Result
<(), Error
> {
831 let mut tree
= AclTree
::new();
833 let user1
: Authid
= "user1@pbs".parse()?
;
835 tree
.insert_user_role("/storage", &user1
, "NoAccess", true);
837 check_roles(&tree
, &user1
, "/storage", "NoAccess");
839 tree
.insert_user_role("/storage", &user1
, "Admin", true);
840 tree
.insert_user_role("/storage", &user1
, "Audit", true);
842 check_roles(&tree
, &user1
, "/storage", "Admin,Audit");
844 tree
.insert_user_role("/storage", &user1
, "NoAccess", true);
846 check_roles(&tree
, &user1
, "/storage", "NoAccess");