[pbs-devel] [RFC proxmox-backup 10/15] api: add permissions endpoint

Fabian Grünbichler f.gruenbichler at proxmox.com
Mon Oct 19 09:39:14 CEST 2020


and adapt privilege calculation to return propagate flag

Signed-off-by: Fabian Grünbichler <f.gruenbichler at proxmox.com>
---

Notes:
    not too happy with the paths addition to AclTree. could probably be solved with
    some recursive function..
    
    this is compatible with the permissions API call in PVE, except that true is encoded as 1 there.

 src/api2/access.rs             | 95 +++++++++++++++++++++++++++++++++-
 src/config/acl.rs              | 67 +++++++++++++-----------
 src/config/cached_user_info.rs | 19 +++++--
 3 files changed, 146 insertions(+), 35 deletions(-)

diff --git a/src/api2/access.rs b/src/api2/access.rs
index 0c19dab6..c898fd7d 100644
--- a/src/api2/access.rs
+++ b/src/api2/access.rs
@@ -1,6 +1,9 @@
 use anyhow::{bail, format_err, Error};
 
 use serde_json::{json, Value};
+use std::collections::HashMap;
+use std::collections::HashSet;
+use std::convert::TryFrom;
 
 use proxmox::api::{api, RpcEnvironment, Permission};
 use proxmox::api::router::{Router, SubdirMap};
@@ -12,8 +15,9 @@ use crate::auth_helpers::*;
 use crate::api2::types::*;
 use crate::tools::{FileLogOptions, FileLogger};
 
+use crate::config::acl as acl_config;
+use crate::config::acl::{PRIVILEGES, PRIV_SYS_AUDIT, PRIV_PERMISSIONS_MODIFY};
 use crate::config::cached_user_info::CachedUserInfo;
-use crate::config::acl::{PRIVILEGES, PRIV_PERMISSIONS_MODIFY};
 
 pub mod user;
 pub mod domain;
@@ -237,6 +241,91 @@ fn change_password(
     Ok(Value::Null)
 }
 
+#[api(
+    input: {
+        properties: {
+            userid: {
+                schema: PROXMOX_USER_OR_TOKEN_ID_SCHEMA,
+                optional: true,
+            },
+            path: {
+                schema: ACL_PATH_SCHEMA,
+                optional: true,
+            },
+        },
+    },
+    access: {
+        permission: &Permission::Or(&[
+            &Permission::Privilege(&["access"], PRIV_SYS_AUDIT, false),
+            &Permission::UserParam("userid"),
+        ]),
+    },
+    returns: {
+        description: "Map of ACL path to Map of privilege to propagate bit",
+        type: Object,
+        properties: {},
+        additional_properties: true,
+    },
+)]
+/// List permissions of given or currently authenticated user / API token.
+///
+/// Optionally limited to specific path.
+pub fn list_permissions(
+    userid: Option<Userid>,
+    path: Option<String>,
+    rpcenv: &dyn RpcEnvironment,
+) -> Result<HashMap<String, HashMap<String, bool>>, Error> {
+    let user = match userid {
+        Some(user) => user,
+        None => Userid::try_from(rpcenv.get_user().unwrap())?
+    };
+
+    let user_info = CachedUserInfo::new()?;
+
+    let paths = match path {
+        Some(path) => {
+            let mut paths = HashSet::new();
+            paths.insert(path);
+            paths
+        },
+        None => {
+            let mut paths = HashSet::new();
+
+            // default paths, returned even if no ACL exists
+            paths.insert("/".to_string());
+            paths.insert("/access".to_string());
+            paths.insert("/datastore".to_string());
+            paths.insert("/remote".to_string());
+            paths.insert("/system".to_string());
+
+            let (acl_tree, _) = acl_config::config()?;
+            paths.extend(acl_tree.paths);
+
+            paths
+        },
+    };
+
+    let map = paths.into_iter().fold(HashMap::new(), |mut map: HashMap<String, HashMap<String, bool>>, path: String| {
+        let (privs, propagated_privs) = user_info.lookup_privs_details(&user, &acl_config::split_acl_path(path.as_str()));
+
+        let priv_map = match privs {
+            0 => HashMap::new(),
+            _ => PRIVILEGES.iter().fold(HashMap::new(), |mut priv_map, (name, value)| {
+                if value & privs != 0 {
+                    priv_map.insert(name.to_string(), value & propagated_privs != 0);
+                }
+                priv_map
+            }),
+        };
+
+        map.insert(path, priv_map);
+
+        map
+    });
+
+    Ok(map)
+}
+
 #[sortable]
 const SUBDIRS: SubdirMap = &sorted!([
     ("acl", &acl::ROUTER),
@@ -244,6 +333,10 @@ const SUBDIRS: SubdirMap = &sorted!([
         "password", &Router::new()
             .put(&API_METHOD_CHANGE_PASSWORD)
     ),
+    (
+        "permissions", &Router::new()
+            .get(&API_METHOD_LIST_PERMISSIONS)
+    ),
     (
         "ticket", &Router::new()
             .post(&API_METHOD_CREATE_TICKET)
diff --git a/src/config/acl.rs b/src/config/acl.rs
index 39f9d030..b162b875 100644
--- a/src/config/acl.rs
+++ b/src/config/acl.rs
@@ -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;
@@ -228,6 +228,7 @@ pub fn check_acl_path(path: &str) -> Result<(), Error> {
 
 pub struct AclTree {
     pub root: AclTreeNode,
+    pub paths: Vec<String>,
 }
 
 pub struct AclTreeNode {
@@ -246,7 +247,7 @@ impl AclTreeNode {
         }
     }
 
-    pub fn extract_roles(&self, user: &Userid, all: bool) -> HashSet<String> {
+    pub fn extract_roles(&self, user: &Userid, all: bool) -> HashMap<String, bool> {
         let user_roles = self.extract_user_roles(user, all);
         if !user_roles.is_empty() {
             // user privs always override group privs
@@ -256,33 +257,33 @@ impl AclTreeNode {
         self.extract_group_roles(user, all)
     }
 
-    pub fn extract_user_roles(&self, user: &Userid, all: bool) -> HashSet<String> {
+    pub fn extract_user_roles(&self, user: &Userid, all: bool) -> HashMap<String, bool> {
 
-        let mut set = HashSet::new();
+        let mut map = HashMap::new();
 
         let roles = match self.users.get(user) {
             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: &Userid, 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
@@ -291,17 +292,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) {
@@ -346,7 +347,10 @@ impl AclTreeNode {
 impl AclTree {
 
     pub fn new() -> Self {
-        Self { root: AclTreeNode::new() }
+        Self {
+            root: AclTreeNode::new(),
+            paths: Vec::new(),
+        }
     }
 
     pub fn find_node(&mut self, path: &str) -> Option<&mut AclTreeNode> {
@@ -512,7 +516,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();
@@ -533,6 +538,8 @@ impl AclTree {
             }
         }
 
+        self.paths.push(path_str.to_string());
+
         Ok(())
     }
 
@@ -576,25 +583,25 @@ impl AclTree {
         Ok(tree)
     }
 
-    pub fn roles(&self, userid: &Userid, path: &[&str]) -> HashSet<String> {
+    pub fn roles(&self, userid: &Userid, 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(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
+                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(userid, last_comp);
+            if !new_map.is_empty() {
+                // overwrite previous maptings
+                role_map = new_map;
             }
         }
 
-        role_set
+        role_map
     }
 }
 
@@ -686,7 +693,7 @@ mod test {
 
         let path_vec = super::split_acl_path(path);
         let mut roles = tree.roles(user, &path_vec)
-            .iter().map(|v| v.clone()).collect::<Vec<String>>();
+            .iter().map(|(role, _)| role.clone()).collect::<Vec<String>>();
         roles.sort();
         let roles = roles.join(",");
 
diff --git a/src/config/cached_user_info.rs b/src/config/cached_user_info.rs
index 93f360c8..99e96e67 100644
--- a/src/config/cached_user_info.rs
+++ b/src/config/cached_user_info.rs
@@ -128,26 +128,37 @@ impl CachedUserInfo {
     }
 
     pub fn lookup_privs(&self, userid: &Userid, path: &[&str]) -> u64 {
+        let (privs, _) = self.lookup_privs_details(userid, path);
+        privs
+    }
 
+    pub fn lookup_privs_details(&self, userid: &Userid, path: &[&str]) -> (u64, u64) {
         if self.is_superuser(userid) {
-            return ROLE_ADMIN;
+            return (ROLE_ADMIN, ROLE_ADMIN);
         }
 
         let roles = self.acl_tree.roles(userid, path);
         let mut privs: u64 = 0;
-        for role in roles {
+        let mut propagated_privs: u64 = 0;
+        for (role, propagate) in roles {
             if let Some((role_privs, _)) = ROLE_NAMES.get(role.as_str()) {
+                if propagate {
+                    propagated_privs |= role_privs;
+                }
                 privs |= role_privs;
             }
         }
 
         if userid.is_tokenid() {
             // limit privs to that of owning user
-            privs &= self.lookup_privs(&userid.owner().unwrap(), path);
+            let (owner_privs, owner_propagated_privs) = self.lookup_privs_details(&userid.owner().unwrap(), path);
+            privs &= owner_privs;
+            propagated_privs &= owner_propagated_privs;
         }
 
-        privs
+        (privs, propagated_privs)
     }
+
 }
 
 impl UserInformation for CachedUserInfo {
-- 
2.20.1






More information about the pbs-devel mailing list