]> git.proxmox.com Git - proxmox-backup.git/blobdiff - src/config/acl.rs
verify: introduce & use new Datastore.Verify privilege
[proxmox-backup.git] / src / config / acl.rs
index c8734265b8c4afa7b801a606cf28ca2b4428af61..7345adea5939dcdbe98b03dcb5577f3d926559e8 100644 (file)
@@ -1,5 +1,5 @@
 use std::io::Write;
-use std::collections::{HashMap, HashSet, BTreeMap, BTreeSet};
+use std::collections::{HashMap, BTreeMap, BTreeSet};
 use std::path::{PathBuf, Path};
 use std::sync::{Arc, RwLock};
 use std::str::FromStr;
@@ -12,31 +12,48 @@ use ::serde::{Deserialize, Serialize};
 use serde::de::{value, IntoDeserializer};
 
 use proxmox::tools::{fs::replace_file, fs::CreateOptions};
+use proxmox::constnamedbitmap;
 use proxmox::api::{api, schema::*};
 
+use crate::api2::types::{Authid,Userid};
+
 // define Privilege bitfield
 
-pub const PRIV_SYS_AUDIT: u64                    = 1 << 0;
-pub const PRIV_SYS_MODIFY: u64                   = 1 << 1;
-pub const PRIV_SYS_POWER_MANAGEMENT: u64         = 1 << 2;
+constnamedbitmap! {
+    /// Contains a list of Privileges
+    PRIVILEGES: u64 => {
+        PRIV_SYS_AUDIT("Sys.Audit");
+        PRIV_SYS_MODIFY("Sys.Modify");
+        PRIV_SYS_POWER_MANAGEMENT("Sys.PowerManagement");
+
+        PRIV_DATASTORE_AUDIT("Datastore.Audit");
+        PRIV_DATASTORE_ALLOCATE("Datastore.Allocate");
+        PRIV_DATASTORE_MODIFY("Datastore.Modify");
+        PRIV_DATASTORE_READ("Datastore.Read");
+        PRIV_DATASTORE_VERIFY("Datastore.Verify");
 
-pub const PRIV_DATASTORE_AUDIT: u64              = 1 << 3;
-pub const PRIV_DATASTORE_MODIFY: u64             = 1 << 4;
-pub const PRIV_DATASTORE_READ: u64               = 1 << 5;
+        /// Datastore.Backup also requires backup ownership
+        PRIV_DATASTORE_BACKUP("Datastore.Backup");
+        /// Datastore.Prune also requires backup ownership
+        PRIV_DATASTORE_PRUNE("Datastore.Prune");
 
-/// Datastore.Backup also requires backup ownership
-pub const PRIV_DATASTORE_BACKUP: u64             = 1 << 6;
-/// Datastore.Prune also requires backup ownership
-pub const PRIV_DATASTORE_PRUNE: u64              = 1 << 7;
+        PRIV_PERMISSIONS_MODIFY("Permissions.Modify");
 
-pub const PRIV_PERMISSIONS_MODIFY: u64           = 1 << 8;
+        PRIV_REMOTE_AUDIT("Remote.Audit");
+        PRIV_REMOTE_MODIFY("Remote.Modify");
+        PRIV_REMOTE_READ("Remote.Read");
+        PRIV_REMOTE_PRUNE("Remote.Prune");
 
-pub const PRIV_REMOTE_AUDIT: u64                 = 1 << 9;
-pub const PRIV_REMOTE_MODIFY: u64                = 1 << 10;
-pub const PRIV_REMOTE_READ: u64                  = 1 << 11;
-pub const PRIV_REMOTE_PRUNE: u64                 = 1 << 12;
+        PRIV_SYS_CONSOLE("Sys.Console");
+    }
+}
 
+
+/// Admin always has all privileges. It can do everything except a few actions
+/// which are limited to the 'root@pam` superuser
 pub const ROLE_ADMIN: u64 = std::u64::MAX;
+
+/// NoAccess can be used to remove privileges from specific paths
 pub const ROLE_NO_ACCESS: u64 = 0;
 
 pub const ROLE_AUDIT: u64 =
@@ -48,12 +65,14 @@ pub const ROLE_DATASTORE_ADMIN: u64 =
 PRIV_DATASTORE_AUDIT |
 PRIV_DATASTORE_MODIFY |
 PRIV_DATASTORE_READ |
+PRIV_DATASTORE_VERIFY |
 PRIV_DATASTORE_BACKUP |
 PRIV_DATASTORE_PRUNE;
 
-/// Datastore.Reader can read datastore content an do restore
+/// Datastore.Reader can read/verify datastore content and do restore
 pub const ROLE_DATASTORE_READER: u64 =
 PRIV_DATASTORE_AUDIT |
+PRIV_DATASTORE_VERIFY |
 PRIV_DATASTORE_READ;
 
 /// Datastore.Backup can do backup and restore, but no prune.
@@ -157,12 +176,65 @@ pub fn split_acl_path(path: &str) -> Vec<&str> {
     components
 }
 
+pub fn check_acl_path(path: &str) -> Result<(), Error> {
+
+    let components = split_acl_path(path);
+
+    let components_len = components.len();
+
+    if components_len == 0 { return Ok(()); }
+    match components[0] {
+        "access" => {
+            if components_len == 1 { return Ok(()); }
+            match components[1] {
+                "acl" | "users" => {
+                    if components_len == 2 { return Ok(()); }
+                }
+                _ => {},
+            }
+        }
+        "datastore" => {  // /datastore/{store}
+            if components_len <= 2 { return Ok(()); }
+        }
+        "remote" => { // /remote/{remote}/{store}
+            if components_len <= 3 { return Ok(()); }
+        }
+        "system" => {
+            if components_len == 1 { return Ok(()); }
+            match components[1] {
+                "disks" | "log" | "status" | "tasks" | "time" => {
+                    if components_len == 2 { return Ok(()); }
+                }
+                "services" => { // /system/services/{service}
+                    if components_len <= 3 { return Ok(()); }
+                }
+                "network" => {
+                    if components_len == 2 { return Ok(()); }
+                    match components[2] {
+                        "dns" => {
+                            if components_len == 3 { return Ok(()); }
+                        }
+                        "interfaces" => { // /system/network/interfaces/{iface}
+                            if components_len <= 4 { return Ok(()); }
+                        }
+                        _ => {}
+                    }
+                }
+                _ => {}
+            }
+        }
+        _ => {}
+    }
+
+    bail!("invalid acl path '{}'.", path);
+}
+
 pub struct AclTree {
     pub root: AclTreeNode,
 }
 
 pub struct AclTreeNode {
-    pub users: HashMap<String, HashMap<String, bool>>,
+    pub users: HashMap<Authid, HashMap<String, bool>>,
     pub groups: HashMap<String, HashMap<String, bool>>,
     pub children: BTreeMap<String, AclTreeNode>,
 }
@@ -177,43 +249,43 @@ impl AclTreeNode {
         }
     }
 
-    pub fn extract_roles(&self, user: &str, all: bool) -> HashSet<String> {
-        let user_roles = self.extract_user_roles(user, all);
-        if !user_roles.is_empty() {
+    pub fn extract_roles(&self, auth_id: &Authid, all: bool) -> HashMap<String, bool> {
+        let user_roles = self.extract_user_roles(auth_id, all);
+        if !user_roles.is_empty() || auth_id.is_token() {
             // user privs always override group privs
             return user_roles
         };
 
-        self.extract_group_roles(user, all)
+        self.extract_group_roles(auth_id.user(), all)
     }
 
-    pub fn extract_user_roles(&self, user: &str, all: bool) -> HashSet<String> {
+    pub fn extract_user_roles(&self, auth_id: &Authid, all: bool) -> HashMap<String, bool> {
 
-        let mut set = HashSet::new();
+        let mut map = HashMap::new();
 
-        let roles = match self.users.get(user) {
+        let roles = match self.users.get(auth_id) {
             Some(m) => m,
-            None => return set,
+            None => return map,
         };
 
         for (role, propagate) in roles {
             if *propagate || all {
                 if role == ROLE_NAME_NO_ACCESS {
-                    // return a set with a single role 'NoAccess'
-                    let mut set = HashSet::new();
-                    set.insert(role.to_string());
-                    return set;
+                    // return a map with a single role 'NoAccess'
+                    let mut map = HashMap::new();
+                    map.insert(role.to_string(), false);
+                    return map;
                 }
-                set.insert(role.to_string());
+                map.insert(role.to_string(), *propagate);
             }
         }
 
-        set
+        map
     }
 
-    pub fn extract_group_roles(&self, _user: &str, all: bool) -> HashSet<String> {
+    pub fn extract_group_roles(&self, _user: &Userid, all: bool) -> HashMap<String, bool> {
 
-        let mut set = HashSet::new();
+        let mut map = HashMap::new();
 
         for (_group, roles) in &self.groups {
             let is_member = false; // fixme: check if user is member of the group
@@ -222,17 +294,17 @@ impl AclTreeNode {
             for (role, propagate) in roles {
                 if *propagate || all {
                     if role == ROLE_NAME_NO_ACCESS {
-                        // return a set with a single role 'NoAccess'
-                        let mut set = HashSet::new();
-                        set.insert(role.to_string());
-                        return set;
+                        // return a map with a single role 'NoAccess'
+                        let mut map = HashMap::new();
+                        map.insert(role.to_string(), false);
+                        return map;
                     }
-                    set.insert(role.to_string());
+                    map.insert(role.to_string(), *propagate);
                 }
             }
         }
 
-        set
+        map
     }
 
     pub fn delete_group_role(&mut self, group: &str, role: &str) {
@@ -243,8 +315,8 @@ impl AclTreeNode {
         roles.remove(role);
     }
 
-    pub fn delete_user_role(&mut self, userid: &str, role: &str) {
-        let roles = match self.users.get_mut(userid) {
+    pub fn delete_user_role(&mut self, auth_id: &Authid, role: &str) {
+        let roles = match self.users.get_mut(auth_id) {
             Some(r) => r,
             None => return,
         };
@@ -262,8 +334,8 @@ impl AclTreeNode {
         }
     }
 
-    pub fn insert_user_role(&mut self, user: String, role: String, propagate: bool) {
-        let map = self.users.entry(user).or_insert_with(|| HashMap::new());
+    pub fn insert_user_role(&mut self, auth_id: Authid, role: String, propagate: bool) {
+        let map = self.users.entry(auth_id).or_insert_with(|| HashMap::new());
         if role == ROLE_NAME_NO_ACCESS {
             map.clear();
             map.insert(role, propagate);
@@ -277,7 +349,14 @@ impl AclTreeNode {
 impl AclTree {
 
     pub fn new() -> Self {
-        Self { root: AclTreeNode::new() }
+        Self {
+            root: AclTreeNode::new(),
+        }
+    }
+
+    pub fn find_node(&mut self, path: &str) -> Option<&mut AclTreeNode> {
+        let path = split_acl_path(path);
+        return self.get_node(&path);
     }
 
     fn get_node(&mut self, path: &[&str]) -> Option<&mut AclTreeNode> {
@@ -309,13 +388,13 @@ impl AclTree {
         node.delete_group_role(group, role);
     }
 
-    pub fn delete_user_role(&mut self, path: &str, userid: &str, role: &str) {
+    pub fn delete_user_role(&mut self, path: &str, auth_id: &Authid, role: &str) {
         let path = split_acl_path(path);
         let node = match self.get_node(&path) {
             Some(n) => n,
             None => return,
         };
-        node.delete_user_role(userid, role);
+        node.delete_user_role(auth_id, role);
     }
 
     pub fn insert_group_role(&mut self, path: &str, group: &str, role: &str, propagate: bool) {
@@ -324,10 +403,10 @@ impl AclTree {
         node.insert_group_role(group.to_string(), role.to_string(), propagate);
     }
 
-    pub fn insert_user_role(&mut self, path: &str, user: &str, role: &str, propagate: bool) {
+    pub fn insert_user_role(&mut self, path: &str, auth_id: &Authid, role: &str, propagate: bool) {
         let path = split_acl_path(path);
         let node = self.get_or_insert_node(&path);
-        node.insert_user_role(user.to_string(), role.to_string(), propagate);
+        node.insert_user_role(auth_id.to_owned(), role.to_string(), propagate);
     }
 
     fn write_node_config(
@@ -339,18 +418,18 @@ impl AclTree {
         let mut role_ug_map0 = HashMap::new();
         let mut role_ug_map1 = HashMap::new();
 
-        for (user, roles) in &node.users {
+        for (auth_id, roles) in &node.users {
             // no need to save, because root is always 'Administrator'
-            if user == "root@pam" { continue; }
+            if !auth_id.is_token() && auth_id.user() == "root@pam" { continue; }
             for (role, propagate) in roles {
                 let role = role.as_str();
-                let user = user.to_string();
+                let auth_id = auth_id.to_string();
                 if *propagate {
                     role_ug_map1.entry(role).or_insert_with(|| BTreeSet::new())
-                        .insert(user);
+                        .insert(auth_id);
                 } else {
                     role_ug_map0.entry(role).or_insert_with(|| BTreeSet::new())
-                        .insert(user);
+                        .insert(auth_id);
                 }
             }
         }
@@ -438,7 +517,8 @@ impl AclTree {
             bail!("expected '0' or '1' for propagate flag.");
         };
 
-        let path = split_acl_path(items[2]);
+        let path_str = items[2];
+        let path = split_acl_path(path_str);
         let node = self.get_or_insert_node(&path);
 
         let uglist: Vec<&str> = items[3].split(',').map(|v| v.trim()).collect();
@@ -454,7 +534,7 @@ impl AclTree {
                     let group = &user_or_group[1..];
                     node.insert_group_role(group.to_string(), role.to_string(), propagate);
                 } else {
-                    node.insert_user_role(user_or_group.to_string(), role.to_string(), propagate);
+                    node.insert_user_role(user_or_group.parse()?, role.to_string(), propagate);
                 }
             }
         }
@@ -502,25 +582,26 @@ impl AclTree {
         Ok(tree)
     }
 
-    pub fn roles(&self, userid: &str, path: &[&str]) -> HashSet<String> {
+    pub fn roles(&self, auth_id: &Authid, path: &[&str]) -> HashMap<String, bool> {
 
         let mut node = &self.root;
-        let mut role_set = node.extract_roles(userid, path.is_empty());
+        let mut role_map = node.extract_roles(auth_id, path.is_empty());
 
         for (pos, comp) in path.iter().enumerate() {
             let last_comp = (pos + 1) == path.len();
             node = match node.children.get(*comp) {
                 Some(n) => n,
-                None => return role_set, // path not found
+                None => return role_map, // path not found
             };
-            let new_set = node.extract_roles(userid, last_comp);
-            if !new_set.is_empty() {
-                // overwrite previous settings
-                role_set = new_set;
+
+            let new_map = node.extract_roles(auth_id, last_comp);
+            if !new_map.is_empty() {
+                // overwrite previous maptings
+                role_map = new_map;
             }
         }
 
-        role_set
+        role_map
     }
 }
 
@@ -551,10 +632,14 @@ pub fn cached_config() -> Result<Arc<AclTree>, Error> {
         Err(err) => bail!("unable to stat '{}' - {}", ACL_CFG_FILENAME, err),
     };
 
-    if let Some(stat) = stat {
+    { // limit scope
         let cache = CACHED_CONFIG.read().unwrap();
-        if stat.st_mtime == cache.last_mtime && stat.st_mtime_nsec == cache.last_mtime_nsec {
-            if let Some(ref config) = cache.data {
+        if let Some(ref config) = cache.data {
+            if let Some(stat) = stat {
+                if stat.st_mtime == cache.last_mtime && stat.st_mtime_nsec == cache.last_mtime_nsec {
+                    return Ok(config.clone());
+                }
+            } else if cache.last_mtime == 0 && cache.last_mtime_nsec == 0 {
                 return Ok(config.clone());
             }
         }
@@ -594,43 +679,45 @@ pub fn save_config(acl: &AclTree) -> Result<(), Error> {
 
 #[cfg(test)]
 mod test {
-
     use anyhow::{Error};
     use super::AclTree;
 
+    use crate::api2::types::Authid;
+
     fn check_roles(
         tree: &AclTree,
-        user: &str,
+        auth_id: &Authid,
         path: &str,
         expected_roles: &str,
     ) {
 
         let path_vec = super::split_acl_path(path);
-        let mut roles = tree.roles(user, &path_vec)
-            .iter().map(|v| v.clone()).collect::<Vec<String>>();
+        let mut roles = tree.roles(auth_id, &path_vec)
+            .iter().map(|(v, _)| v.clone()).collect::<Vec<String>>();
         roles.sort();
         let roles = roles.join(",");
 
-        assert_eq!(roles, expected_roles, "\nat check_roles for '{}' on '{}'", user, path);
+        assert_eq!(roles, expected_roles, "\nat check_roles for '{}' on '{}'", auth_id, path);
     }
 
     #[test]
-    fn test_acl_line_compression() -> Result<(), Error> {
-
-        let tree = AclTree::from_raw(r###"
-acl:0:/store/store2:user1:Admin
-acl:0:/store/store2:user2:Admin
-acl:0:/store/store2:user1:DatastoreBackup
-acl:0:/store/store2:user2:DatastoreBackup
-"###)?;
+    fn test_acl_line_compression() {
+
+        let tree = AclTree::from_raw(
+            "\
+            acl:0:/store/store2:user1@pbs:Admin\n\
+            acl:0:/store/store2:user2@pbs:Admin\n\
+            acl:0:/store/store2:user1@pbs:DatastoreBackup\n\
+            acl:0:/store/store2:user2@pbs:DatastoreBackup\n\
+            ",
+        )
+        .expect("failed to parse acl tree");
 
         let mut raw: Vec<u8> = Vec::new();
-        tree.write_config(&mut raw)?;
-        let raw = std::str::from_utf8(&raw)?;
+        tree.write_config(&mut raw).expect("failed to write acl tree");
+        let raw = std::str::from_utf8(&raw).expect("acl tree is not valid utf8");
 
-        assert_eq!(raw, "acl:0:/store/store2:user1,user2:Admin,DatastoreBackup\n");
-
-        Ok(())
+        assert_eq!(raw, "acl:0:/store/store2:user1@pbs,user2@pbs:Admin,DatastoreBackup\n");
     }
 
     #[test]
@@ -641,15 +728,17 @@ acl:1:/storage:user1@pbs:Admin
 acl:1:/storage/store1:user1@pbs:DatastoreBackup
 acl:1:/storage/store2:user2@pbs:DatastoreBackup
 "###)?;
-        check_roles(&tree, "user1@pbs", "/", "");
-        check_roles(&tree, "user1@pbs", "/storage", "Admin");
-        check_roles(&tree, "user1@pbs", "/storage/store1", "DatastoreBackup");
-        check_roles(&tree, "user1@pbs", "/storage/store2", "Admin");
-
-        check_roles(&tree, "user2@pbs", "/", "");
-        check_roles(&tree, "user2@pbs", "/storage", "");
-        check_roles(&tree, "user2@pbs", "/storage/store1", "");
-        check_roles(&tree, "user2@pbs", "/storage/store2", "DatastoreBackup");
+        let user1: Authid = "user1@pbs".parse()?;
+        check_roles(&tree, &user1, "/", "");
+        check_roles(&tree, &user1, "/storage", "Admin");
+        check_roles(&tree, &user1, "/storage/store1", "DatastoreBackup");
+        check_roles(&tree, &user1, "/storage/store2", "Admin");
+
+        let user2: Authid = "user2@pbs".parse()?;
+        check_roles(&tree, &user2, "/", "");
+        check_roles(&tree, &user2, "/storage", "");
+        check_roles(&tree, &user2, "/storage/store1", "");
+        check_roles(&tree, &user2, "/storage/store2", "DatastoreBackup");
 
         Ok(())
     }
@@ -662,22 +751,23 @@ acl:1:/:user1@pbs:Admin
 acl:1:/storage:user1@pbs:NoAccess
 acl:1:/storage/store1:user1@pbs:DatastoreBackup
 "###)?;
-        check_roles(&tree, "user1@pbs", "/", "Admin");
-        check_roles(&tree, "user1@pbs", "/storage", "NoAccess");
-        check_roles(&tree, "user1@pbs", "/storage/store1", "DatastoreBackup");
-        check_roles(&tree, "user1@pbs", "/storage/store2", "NoAccess");
-        check_roles(&tree, "user1@pbs", "/system", "Admin");
+        let user1: Authid = "user1@pbs".parse()?;
+        check_roles(&tree, &user1, "/", "Admin");
+        check_roles(&tree, &user1, "/storage", "NoAccess");
+        check_roles(&tree, &user1, "/storage/store1", "DatastoreBackup");
+        check_roles(&tree, &user1, "/storage/store2", "NoAccess");
+        check_roles(&tree, &user1, "/system", "Admin");
 
         let tree = AclTree::from_raw(r###"
 acl:1:/:user1@pbs:Admin
 acl:0:/storage:user1@pbs:NoAccess
 acl:1:/storage/store1:user1@pbs:DatastoreBackup
 "###)?;
-        check_roles(&tree, "user1@pbs", "/", "Admin");
-        check_roles(&tree, "user1@pbs", "/storage", "NoAccess");
-        check_roles(&tree, "user1@pbs", "/storage/store1", "DatastoreBackup");
-        check_roles(&tree, "user1@pbs", "/storage/store2", "Admin");
-        check_roles(&tree, "user1@pbs", "/system", "Admin");
+        check_roles(&tree, &user1, "/", "Admin");
+        check_roles(&tree, &user1, "/storage", "NoAccess");
+        check_roles(&tree, &user1, "/storage/store1", "DatastoreBackup");
+        check_roles(&tree, &user1, "/storage/store2", "Admin");
+        check_roles(&tree, &user1, "/system", "Admin");
 
         Ok(())
     }
@@ -687,13 +777,15 @@ acl:1:/storage/store1:user1@pbs:DatastoreBackup
 
         let mut tree = AclTree::new();
 
-        tree.insert_user_role("/", "user1@pbs", "Admin", true);
-        tree.insert_user_role("/", "user1@pbs", "Audit", true);
+        let user1: Authid = "user1@pbs".parse()?;
 
-        check_roles(&tree, "user1@pbs", "/", "Admin,Audit");
+        tree.insert_user_role("/", &user1, "Admin", true);
+        tree.insert_user_role("/", &user1, "Audit", true);
 
-        tree.insert_user_role("/", "user1@pbs", "NoAccess", true);
-        check_roles(&tree, "user1@pbs", "/", "NoAccess");
+        check_roles(&tree, &user1, "/", "Admin,Audit");
+
+        tree.insert_user_role("/", &user1, "NoAccess", true);
+        check_roles(&tree, &user1, "/", "NoAccess");
 
         let mut raw: Vec<u8> = Vec::new();
         tree.write_config(&mut raw)?;
@@ -709,20 +801,21 @@ acl:1:/storage/store1:user1@pbs:DatastoreBackup
 
         let mut tree = AclTree::new();
 
-        tree.insert_user_role("/storage", "user1@pbs", "NoAccess", true);
+        let user1: Authid = "user1@pbs".parse()?;
+
+        tree.insert_user_role("/storage", &user1, "NoAccess", true);
 
-        check_roles(&tree, "user1@pbs", "/storage", "NoAccess");
+        check_roles(&tree, &user1, "/storage", "NoAccess");
 
-        tree.insert_user_role("/storage", "user1@pbs", "Admin", true);
-        tree.insert_user_role("/storage", "user1@pbs", "Audit", true);
+        tree.insert_user_role("/storage", &user1, "Admin", true);
+        tree.insert_user_role("/storage", &user1, "Audit", true);
 
-        check_roles(&tree, "user1@pbs", "/storage", "Admin,Audit");
+        check_roles(&tree, &user1, "/storage", "Admin,Audit");
 
-        tree.insert_user_role("/storage", "user1@pbs", "NoAccess", true);
+        tree.insert_user_role("/storage", &user1, "NoAccess", true);
 
-        check_roles(&tree, "user1@pbs", "/storage", "NoAccess");
+        check_roles(&tree, &user1, "/storage", "NoAccess");
 
         Ok(())
     }
-
 }