]> git.proxmox.com Git - proxmox-backup.git/blobdiff - src/config/acl.rs
src/config/acl.rs: introduce more/better datastore privileges
[proxmox-backup.git] / src / config / acl.rs
index 16f2a17d4b6af59aaae109c6a19eddfebcddd6e4..2cae1c741a6610e464c7210019e1c0ddbc5bc958 100644 (file)
@@ -1,8 +1,9 @@
 use std::io::Write;
-use std::collections::{HashMap, HashSet};
+use std::collections::{HashMap, HashSet, BTreeMap, BTreeSet};
 use std::path::{PathBuf, Path};
+use std::sync::{Arc, RwLock};
 
-use failure::*;
+use anyhow::{bail, Error};
 
 use lazy_static::lazy_static;
 
@@ -10,46 +11,75 @@ use proxmox::tools::{fs::replace_file, fs::CreateOptions};
 
 // 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;
+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;
 
-pub const PRIV_STORE_AUDIT: u64              = 1 << 3;
-pub const PRIV_STORE_ALLOCATE: u64           = 1 << 4;
-pub const PRIV_STORE_ALLOCATE_SPACE: u64     = 1 << 5;
+pub const PRIV_DATASTORE_AUDIT: u64              = 1 << 3;
+pub const PRIV_DATASTORE_MODIFY: u64             = 1 << 4;
+pub const PRIV_DATASTORE_CREATE_BACKUP: u64      = 1 << 5;
+pub const PRIV_DATASTORE_READ: u64               = 1 << 6;
+pub const PRIV_DATASTORE_PRUNE: u64              = 1 << 7;
+
+pub const PRIV_PERMISSIONS_MODIFY: u64           = 1 << 8;
 
 pub const ROLE_ADMIN: u64 = std::u64::MAX;
 pub const ROLE_NO_ACCESS: u64 = 0;
 
 pub const ROLE_AUDIT: u64 =
 PRIV_SYS_AUDIT |
-PRIV_STORE_AUDIT;
+PRIV_DATASTORE_AUDIT;
+
+pub const ROLE_DATASTORE_ADMIN: u64 =
+PRIV_DATASTORE_AUDIT |
+PRIV_DATASTORE_MODIFY |
+PRIV_DATASTORE_CREATE_BACKUP |
+PRIV_DATASTORE_READ |
+PRIV_DATASTORE_PRUNE;
+
+pub const ROLE_DATASTORE_USER: u64 =
+PRIV_DATASTORE_CREATE_BACKUP;
 
-pub const ROLE_STORE_ADMIN: u64 =
-PRIV_STORE_AUDIT |
-PRIV_STORE_ALLOCATE |
-PRIV_STORE_ALLOCATE_SPACE;
+pub const ROLE_DATASTORE_AUDIT: u64 =
+PRIV_DATASTORE_AUDIT;
 
-pub const ROLE_STORE_USER: u64 =
-PRIV_STORE_AUDIT |
-PRIV_STORE_ALLOCATE_SPACE;
+pub const ROLE_NAME_NO_ACCESS: &str ="NoAccess";
 
 lazy_static! {
-    static ref ROLE_NAMES: HashMap<&'static str, u64> = {
+    pub static ref ROLE_NAMES: HashMap<&'static str, (u64, &'static str)> = {
         let mut map = HashMap::new();
 
-        map.insert("Admin", ROLE_ADMIN);
-        map.insert("Audit", ROLE_AUDIT);
-        map.insert("NoAccess", ROLE_NO_ACCESS);
-
-        map.insert("Store.Admin", ROLE_STORE_ADMIN);
-        map.insert("Store.User", ROLE_STORE_USER);
+        map.insert("Admin", (
+            ROLE_ADMIN,
+            "Administrator",
+        ));
+        map.insert("Audit", (
+            ROLE_AUDIT,
+            "Auditor",
+        ));
+        map.insert(ROLE_NAME_NO_ACCESS, (
+            ROLE_NO_ACCESS,
+            "Disable access",
+        ));
+
+        map.insert("Datastore.Admin", (
+            ROLE_DATASTORE_ADMIN,
+            "Datastore Administrator",
+        ));
+        map.insert("Datastore.User", (
+            ROLE_DATASTORE_USER,
+            "Datastore User",
+        ));
+        map.insert("Datastore.Audit", (
+            ROLE_DATASTORE_AUDIT,
+            "Datastore Auditor",
+        ));
 
         map
     };
 }
 
-fn split_acl_path(path: &str) -> Vec<&str> {
+pub fn split_acl_path(path: &str) -> Vec<&str> {
 
     let items = path.split('/');
 
@@ -64,13 +94,13 @@ fn split_acl_path(path: &str) -> Vec<&str> {
 }
 
 pub struct AclTree {
-    root: AclTreeNode,
+    pub root: AclTreeNode,
 }
 
-struct AclTreeNode {
-    users: HashMap<String, HashMap<String, bool>>,
-    groups: HashMap<String, HashMap<String, bool>>,
-    children: HashMap<String, AclTreeNode>,
+pub struct AclTreeNode {
+    pub users: HashMap<String, HashMap<String, bool>>,
+    pub groups: HashMap<String, HashMap<String, bool>>,
+    pub children: BTreeMap<String, AclTreeNode>,
 }
 
 impl AclTreeNode {
@@ -79,7 +109,7 @@ impl AclTreeNode {
         Self {
             users: HashMap::new(),
             groups: HashMap::new(),
-            children: HashMap::new(),
+            children: BTreeMap::new(),
         }
     }
 
@@ -104,7 +134,7 @@ impl AclTreeNode {
 
         for (role, propagate) in roles {
             if *propagate || all {
-                if role == "NoAccess" {
+                if role == ROLE_NAME_NO_ACCESS {
                     // return a set with a single role 'NoAccess'
                     let mut set = HashSet::new();
                     set.insert(role.to_string());
@@ -127,7 +157,7 @@ impl AclTreeNode {
 
             for (role, propagate) in roles {
                 if *propagate || all {
-                    if role == "NoAccess" {
+                    if role == ROLE_NAME_NO_ACCESS {
                         // return a set with a single role 'NoAccess'
                         let mut set = HashSet::new();
                         set.insert(role.to_string());
@@ -141,16 +171,42 @@ impl AclTreeNode {
         set
     }
 
+    pub fn delete_group_role(&mut self, group: &str, role: &str) {
+        let roles = match self.groups.get_mut(group) {
+            Some(r) => r,
+            None => return,
+        };
+        roles.remove(role);
+    }
+
+    pub fn delete_user_role(&mut self, userid: &str, role: &str) {
+        let roles = match self.users.get_mut(userid) {
+            Some(r) => r,
+            None => return,
+        };
+        roles.remove(role);
+    }
+
     pub fn insert_group_role(&mut self, group: String, role: String, propagate: bool) {
-        self.groups
-            .entry(group).or_insert_with(|| HashMap::new())
-            .insert(role, propagate);
+        let map = self.groups.entry(group).or_insert_with(|| HashMap::new());
+        if role == ROLE_NAME_NO_ACCESS {
+            map.clear();
+            map.insert(role, propagate);
+        } else {
+            map.remove(ROLE_NAME_NO_ACCESS);
+            map.insert(role, propagate);
+        }
     }
 
     pub fn insert_user_role(&mut self, user: String, role: String, propagate: bool) {
-        self.users
-            .entry(user).or_insert_with(|| HashMap::new())
-            .insert(role, propagate);
+        let map = self.users.entry(user).or_insert_with(|| HashMap::new());
+        if role == ROLE_NAME_NO_ACCESS {
+            map.clear();
+            map.insert(role, propagate);
+        } else {
+            map.remove(ROLE_NAME_NO_ACCESS);
+            map.insert(role, propagate);
+        }
     }
 }
 
@@ -160,6 +216,17 @@ impl AclTree {
         Self { root: AclTreeNode::new() }
     }
 
+    fn get_node(&mut self, path: &[&str]) -> Option<&mut AclTreeNode> {
+        let mut node = &mut self.root;
+        for comp in path {
+            node = match node.children.get_mut(*comp) {
+                Some(n) => n,
+                None => return None,
+            };
+        }
+        Some(node)
+    }
+
     fn get_or_insert_node(&mut self, path: &[&str]) -> &mut AclTreeNode {
         let mut node = &mut self.root;
         for comp in path {
@@ -169,6 +236,24 @@ impl AclTree {
         node
     }
 
+    pub fn delete_group_role(&mut self, path: &str, group: &str, role: &str) {
+        let path = split_acl_path(path);
+        let node = match self.get_node(&path) {
+            Some(n) => n,
+            None => return,
+        };
+        node.delete_group_role(group, role);
+    }
+
+    pub fn delete_user_role(&mut self, path: &str, userid: &str, 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);
+    }
+
     pub fn insert_group_role(&mut self, path: &str, group: &str, role: &str, propagate: bool) {
         let path = split_acl_path(path);
         let node = self.get_or_insert_node(&path);
@@ -197,10 +282,10 @@ impl AclTree {
                 let role = role.as_str();
                 let user = user.to_string();
                 if *propagate {
-                    role_ug_map1.entry(role).or_insert_with(|| HashSet::new())
+                    role_ug_map1.entry(role).or_insert_with(|| BTreeSet::new())
                         .insert(user);
                 } else {
-                    role_ug_map0.entry(role).or_insert_with(|| HashSet::new())
+                    role_ug_map0.entry(role).or_insert_with(|| BTreeSet::new())
                         .insert(user);
                 }
             }
@@ -210,60 +295,54 @@ impl AclTree {
             for (role, propagate) in roles {
                 let group = format!("@{}", group);
                 if *propagate {
-                    role_ug_map1.entry(role).or_insert_with(|| HashSet::new())
+                    role_ug_map1.entry(role).or_insert_with(|| BTreeSet::new())
                         .insert(group);
                 } else {
-                    role_ug_map0.entry(role).or_insert_with(|| HashSet::new())
+                    role_ug_map0.entry(role).or_insert_with(|| BTreeSet::new())
                         .insert(group);
                 }
             }
         }
 
         fn group_by_property_list(
-            item_property_map: &HashMap<&str, HashSet<String>>,
-        ) -> HashMap<String, HashSet<String>> {
-            let mut result_map = HashMap::new();
+            item_property_map: &HashMap<&str, BTreeSet<String>>,
+        ) -> BTreeMap<String, BTreeSet<String>> {
+            let mut result_map = BTreeMap::new();
             for (item, property_map) in item_property_map {
-                let mut item_list = property_map.iter().map(|v| v.as_str())
-                    .collect::<Vec<&str>>();
-                item_list.sort();
-                let item_list = item_list.join(",");
-                result_map.entry(item_list).or_insert_with(|| HashSet::new())
+                let item_list = property_map.iter().fold(String::new(), |mut acc, v| {
+                    if !acc.is_empty() { acc.push(','); }
+                    acc.push_str(v);
+                    acc
+                });
+                result_map.entry(item_list).or_insert_with(|| BTreeSet::new())
                     .insert(item.to_string());
             }
             result_map
         }
 
-        let mut uglist_role_map0 = group_by_property_list(&role_ug_map0)
-            .into_iter()
-            .collect::<Vec<(String, HashSet<String>)>>();
-        uglist_role_map0.sort_unstable_by(|a,b| a.0.cmp(&b.0));
+        let uglist_role_map0 = group_by_property_list(&role_ug_map0);
+        let uglist_role_map1 = group_by_property_list(&role_ug_map1);
 
-        let mut uglist_role_map1 = group_by_property_list(&role_ug_map1)
-            .into_iter()
-            .collect::<Vec<(String, HashSet<String>)>>();
-        uglist_role_map1.sort_unstable_by(|a,b| a.0.cmp(&b.0));
-
-
-        for (uglist, roles) in uglist_role_map0 {
-            let mut role_list = roles.iter().map(|v| v.as_str())
-                .collect::<Vec<&str>>();
-            role_list.sort();
-            writeln!(w, "acl:0:{}:{}:{}", path, uglist, role_list.join(","))?;
+        fn role_list(roles: &BTreeSet<String>) -> String {
+            if roles.contains(ROLE_NAME_NO_ACCESS) { return String::from(ROLE_NAME_NO_ACCESS); }
+            roles.iter().fold(String::new(), |mut acc, v| {
+                if !acc.is_empty() { acc.push(','); }
+                acc.push_str(v);
+                acc
+            })
         }
 
-        for (uglist, roles) in uglist_role_map1 {
-            let mut role_list = roles.iter().map(|v| v.as_str())
-                .collect::<Vec<&str>>();
-            role_list.sort();
-            writeln!(w, "acl:1:{}:{}:{}", path, uglist, role_list.join(","))?;
+        for (uglist, roles) in &uglist_role_map0 {
+            let role_list = role_list(roles);
+            writeln!(w, "acl:0:{}:{}:{}", if path.is_empty() { "/" } else { path }, uglist, role_list)?;
         }
 
-        let mut child_names = node.children.keys().map(|v| v.as_str()).collect::<Vec<&str>>();
-        child_names.sort();
+        for (uglist, roles) in &uglist_role_map1 {
+            let role_list = role_list(roles);
+            writeln!(w, "acl:1:{}:{}:{}", if path.is_empty() { "/" } else { path }, uglist, role_list)?;
+        }
 
-        for name in child_names {
-            let child = node.children.get(name).unwrap();
+        for (name, child) in node.children.iter() {
             let child_path = format!("{}/{}", path, name);
             Self::write_node_config(child, &child_path, w)?;
         }
@@ -389,7 +468,42 @@ pub fn config() -> Result<(AclTree, [u8; 32]), Error> {
     AclTree::load(&path)
 }
 
-pub fn store_config(acl: &AclTree, filename: &Path) -> Result<(), Error> {
+pub fn cached_config() -> Result<Arc<AclTree>, Error> {
+
+    struct ConfigCache {
+        data: Option<Arc<AclTree>>,
+        last_mtime: i64,
+        last_mtime_nsec: i64,
+    }
+
+    lazy_static! {
+        static ref CACHED_CONFIG: RwLock<ConfigCache> = RwLock::new(
+            ConfigCache { data: None, last_mtime: 0, last_mtime_nsec: 0 });
+    }
+
+    let stat = nix::sys::stat::stat(ACL_CFG_FILENAME)?;
+
+    { // 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 {
+                return Ok(config.clone());
+            }
+        }
+    }
+
+    let (config, _digest) = config()?;
+    let config = Arc::new(config);
+
+    let mut cache = CACHED_CONFIG.write().unwrap();
+    cache.last_mtime = stat.st_mtime;
+    cache.last_mtime_nsec = stat.st_mtime_nsec;
+    cache.data = Some(config.clone());
+
+    Ok(config)
+}
+
+pub fn save_config(acl: &AclTree) -> Result<(), Error> {
     let mut raw: Vec<u8> = Vec::new();
 
     acl.write_config(&mut raw)?;
@@ -403,16 +517,15 @@ pub fn store_config(acl: &AclTree, filename: &Path) -> Result<(), Error> {
         .owner(nix::unistd::ROOT)
         .group(backup_user.gid);
 
-    replace_file(filename, &raw, options)?;
+    replace_file(ACL_CFG_FILENAME, &raw, options)?;
 
     Ok(())
 }
 
-
 #[cfg(test)]
 mod test {
 
-    use failure::*;
+    use anyhow::{Error};
     use super::AclTree;
 
     fn check_roles(
@@ -437,15 +550,15 @@ mod test {
         let tree = AclTree::from_raw(r###"
 acl:0:/store/store2:user1:Admin
 acl:0:/store/store2:user2:Admin
-acl:0:/store/store2:user1:Store.User
-acl:0:/store/store2:user2:Store.User
+acl:0:/store/store2:user1:Datastore.User
+acl:0:/store/store2:user2:Datastore.User
 "###)?;
 
         let mut raw: Vec<u8> = Vec::new();
         tree.write_config(&mut raw)?;
         let raw = std::str::from_utf8(&raw)?;
 
-        assert_eq!(raw, "acl:0:/store/store2:user1,user2:Admin,Store.User\n");
+        assert_eq!(raw, "acl:0:/store/store2:user1,user2:Admin,Datastore.User\n");
 
         Ok(())
     }
@@ -455,18 +568,18 @@ acl:0:/store/store2:user2:Store.User
 
         let tree = AclTree::from_raw(r###"
 acl:1:/storage:user1@pbs:Admin
-acl:1:/storage/store1:user1@pbs:Store.User
-acl:1:/storage/store2:user2@pbs:Store.User
+acl:1:/storage/store1:user1@pbs:Datastore.User
+acl:1:/storage/store2:user2@pbs:Datastore.User
 "###)?;
         check_roles(&tree, "user1@pbs", "/", "");
         check_roles(&tree, "user1@pbs", "/storage", "Admin");
-        check_roles(&tree, "user1@pbs", "/storage/store1", "Store.User");
+        check_roles(&tree, "user1@pbs", "/storage/store1", "Datastore.User");
         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", "Store.User");
+        check_roles(&tree, "user2@pbs", "/storage/store2", "Datastore.User");
 
         Ok(())
     }
@@ -477,25 +590,69 @@ acl:1:/storage/store2:user2@pbs:Store.User
         let tree = AclTree::from_raw(r###"
 acl:1:/:user1@pbs:Admin
 acl:1:/storage:user1@pbs:NoAccess
-acl:1:/storage/store1:user1@pbs:Store.User
+acl:1:/storage/store1:user1@pbs:Datastore.User
 "###)?;
         check_roles(&tree, "user1@pbs", "/", "Admin");
         check_roles(&tree, "user1@pbs", "/storage", "NoAccess");
-        check_roles(&tree, "user1@pbs", "/storage/store1", "Store.User");
+        check_roles(&tree, "user1@pbs", "/storage/store1", "Datastore.User");
         check_roles(&tree, "user1@pbs", "/storage/store2", "NoAccess");
         check_roles(&tree, "user1@pbs", "/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:Store.User
+acl:1:/storage/store1:user1@pbs:Datastore.User
 "###)?;
         check_roles(&tree, "user1@pbs", "/", "Admin");
         check_roles(&tree, "user1@pbs", "/storage", "NoAccess");
-        check_roles(&tree, "user1@pbs", "/storage/store1", "Store.User");
+        check_roles(&tree, "user1@pbs", "/storage/store1", "Datastore.User");
         check_roles(&tree, "user1@pbs", "/storage/store2", "Admin");
         check_roles(&tree, "user1@pbs", "/system", "Admin");
 
         Ok(())
     }
+
+    #[test]
+    fn test_role_add_delete() -> Result<(), Error> {
+
+        let mut tree = AclTree::new();
+
+        tree.insert_user_role("/", "user1@pbs", "Admin", true);
+        tree.insert_user_role("/", "user1@pbs", "Audit", true);
+
+        check_roles(&tree, "user1@pbs", "/", "Admin,Audit");
+
+        tree.insert_user_role("/", "user1@pbs", "NoAccess", true);
+        check_roles(&tree, "user1@pbs", "/", "NoAccess");
+
+        let mut raw: Vec<u8> = Vec::new();
+        tree.write_config(&mut raw)?;
+        let raw = std::str::from_utf8(&raw)?;
+
+        assert_eq!(raw, "acl:1:/:user1@pbs:NoAccess\n");
+
+        Ok(())
+    }
+
+    #[test]
+    fn test_no_access_overwrite() -> Result<(), Error> {
+
+        let mut tree = AclTree::new();
+
+        tree.insert_user_role("/storage", "user1@pbs", "NoAccess", true);
+
+        check_roles(&tree, "user1@pbs", "/storage", "NoAccess");
+
+        tree.insert_user_role("/storage", "user1@pbs", "Admin", true);
+        tree.insert_user_role("/storage", "user1@pbs", "Audit", true);
+
+        check_roles(&tree, "user1@pbs", "/storage", "Admin,Audit");
+
+        tree.insert_user_role("/storage", "user1@pbs", "NoAccess", true);
+
+        check_roles(&tree, "user1@pbs", "/storage", "NoAccess");
+
+        Ok(())
+    }
+
 }