]> git.proxmox.com Git - proxmox-backup.git/commitdiff
acl: implement roles(), add regression tests.
authorDietmar Maurer <dietmar@proxmox.com>
Sun, 12 Apr 2020 11:06:50 +0000 (13:06 +0200)
committerDietmar Maurer <dietmar@proxmox.com>
Sun, 12 Apr 2020 11:06:50 +0000 (13:06 +0200)
src/config/acl.rs

index 9120c967f36954205c53658cc38b8ef8907bb739..16f2a17d4b6af59aaae109c6a19eddfebcddd6e4 100644 (file)
@@ -18,6 +18,9 @@ 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 ROLE_ADMIN: u64 = std::u64::MAX;
+pub const ROLE_NO_ACCESS: u64 = 0;
+
 pub const ROLE_AUDIT: u64 =
 PRIV_SYS_AUDIT |
 PRIV_STORE_AUDIT;
@@ -35,9 +38,9 @@ lazy_static! {
     static ref ROLE_NAMES: HashMap<&'static str, u64> = {
         let mut map = HashMap::new();
 
-        map.insert("Admin", std::u64::MAX);
+        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);
@@ -80,6 +83,64 @@ 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() {
+            // user privs always override group privs
+            return user_roles
+        };
+
+        self.extract_group_roles(user, all)
+    }
+
+    pub fn extract_user_roles(&self, user: &str, all: bool) -> HashSet<String> {
+
+        let mut set = HashSet::new();
+
+        let roles = match self.users.get(user) {
+            Some(m) => m,
+            None => return set,
+        };
+
+        for (role, propagate) in roles {
+            if *propagate || all {
+                if role == "NoAccess" {
+                    // return a set with a single role 'NoAccess'
+                    let mut set = HashSet::new();
+                    set.insert(role.to_string());
+                    return set;
+                }
+                set.insert(role.to_string());
+            }
+        }
+
+        set
+    }
+
+    pub fn extract_group_roles(&self, _user: &str, all: bool) -> HashSet<String> {
+
+        let mut set = HashSet::new();
+
+        for (_group, roles) in &self.groups {
+            let is_member = false; // fixme: check if user is member of the group
+            if !is_member { continue; }
+
+            for (role, propagate) in roles {
+                if *propagate || all {
+                    if role == "NoAccess" {
+                        // return a set with a single role 'NoAccess'
+                        let mut set = HashSet::new();
+                        set.insert(role.to_string());
+                        return set;
+                    }
+                    set.insert(role.to_string());
+                }
+            }
+        }
+
+        set
+    }
+
     pub fn insert_group_role(&mut self, group: String, role: String, propagate: bool) {
         self.groups
             .entry(group).or_insert_with(|| HashMap::new())
@@ -275,13 +336,49 @@ impl AclTree {
         let digest = openssl::sha::sha256(raw.as_bytes());
 
         for (linenr, line) in raw.lines().enumerate() {
+            let line = line.trim();
+            if line.is_empty() { continue; }
             if let Err(err) = tree.parse_acl_line(line) {
-                bail!("unable to parse acl config {:?}, line {} - {}", filename, linenr, err);
+                bail!("unable to parse acl config {:?}, line {} - {}",
+                      filename, linenr+1, err);
             }
         }
 
         Ok((tree, digest))
     }
+
+    pub fn from_raw(raw: &str) -> Result<Self, Error> {
+        let mut tree = Self::new();
+        for (linenr, line) in raw.lines().enumerate() {
+            let line = line.trim();
+            if line.is_empty() { continue; }
+            if let Err(err) = tree.parse_acl_line(line) {
+                bail!("unable to parse acl config data, line {} - {}", linenr+1, err);
+            }
+        }
+        Ok(tree)
+    }
+
+    pub fn roles(&self, userid: &str, path: &[&str]) -> HashSet<String> {
+
+        let mut node = &self.root;
+        let mut role_set = node.extract_roles(userid, 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
+            };
+            let new_set = node.extract_roles(userid, last_comp);
+            if !new_set.is_empty() {
+                // overwrite previous settings
+                role_set = new_set;
+            }
+        }
+
+        role_set
+    }
 }
 
 pub const ACL_CFG_FILENAME: &str = "/etc/proxmox-backup/acl.cfg";
@@ -310,3 +407,95 @@ pub fn store_config(acl: &AclTree, filename: &Path) -> Result<(), Error> {
 
     Ok(())
 }
+
+
+#[cfg(test)]
+mod test {
+
+    use failure::*;
+    use super::AclTree;
+
+    fn check_roles(
+        tree: &AclTree,
+        user: &str,
+        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>>();
+        roles.sort();
+        let roles = roles.join(",");
+
+        assert_eq!(roles, expected_roles, "\nat check_roles for '{}' on '{}'", user, 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:Store.User
+acl:0:/store/store2:user2:Store.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");
+
+        Ok(())
+    }
+
+    #[test]
+    fn test_roles_1() -> Result<(), Error> {
+
+        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
+"###)?;
+        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/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");
+
+        Ok(())
+    }
+
+    #[test]
+    fn test_role_no_access() -> Result<(), Error> {
+
+        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
+"###)?;
+        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/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
+"###)?;
+        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/store2", "Admin");
+        check_roles(&tree, "user1@pbs", "/system", "Admin");
+
+        Ok(())
+    }
+}