From babab85b56159a7fe63de761bc6336b49957a6d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20Gr=C3=BCnbichler?= Date: Thu, 8 Oct 2020 10:34:07 +0200 Subject: [PATCH] api: add permissions endpoint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit and adapt privilege calculation to return propagate flag Signed-off-by: Fabian Grünbichler --- src/api2/access.rs | 131 ++++++++++++++++++++++++++++++++- src/config/acl.rs | 66 +++++++++-------- src/config/cached_user_info.rs | 19 ++++- 3 files changed, 181 insertions(+), 35 deletions(-) diff --git a/src/api2/access.rs b/src/api2/access.rs index d0494c9a..5e74a6ee 100644 --- a/src/api2/access.rs +++ b/src/api2/access.rs @@ -1,6 +1,8 @@ use anyhow::{bail, format_err, Error}; use serde_json::{json, Value}; +use std::collections::HashMap; +use std::collections::HashSet; use proxmox::api::{api, RpcEnvironment, Permission}; use proxmox::api::router::{Router, SubdirMap}; @@ -12,8 +14,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; @@ -238,6 +241,128 @@ fn change_password( Ok(Value::Null) } +#[api( + input: { + properties: { + auth_id: { + type: Authid, + optional: true, + }, + path: { + schema: ACL_PATH_SCHEMA, + optional: true, + }, + }, + }, + access: { + permission: &Permission::Anybody, + description: "Requires Sys.Audit on '/access', limited to own privileges otherwise.", + }, + 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( + auth_id: Option, + path: Option, + rpcenv: &dyn RpcEnvironment, +) -> Result>, Error> { + let current_auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?; + + let user_info = CachedUserInfo::new()?; + let user_privs = user_info.lookup_privs(¤t_auth_id, &["access"]); + + let auth_id = if user_privs & PRIV_SYS_AUDIT == 0 { + match auth_id { + Some(auth_id) => { + if auth_id == current_auth_id { + auth_id + } else if auth_id.is_token() + && !current_auth_id.is_token() + && auth_id.user() == current_auth_id.user() { + auth_id + } else { + bail!("not allowed to list permissions of {}", auth_id); + } + }, + None => current_auth_id, + } + } else { + match auth_id { + Some(auth_id) => auth_id, + None => current_auth_id, + } + }; + + + fn populate_acl_paths( + mut paths: HashSet, + node: acl_config::AclTreeNode, + path: &str + ) -> HashSet { + for (sub_path, child_node) in node.children { + let sub_path = format!("{}/{}", path, &sub_path); + paths = populate_acl_paths(paths, child_node, &sub_path); + paths.insert(sub_path); + } + paths + } + + let paths = match path { + Some(path) => { + let mut paths = HashSet::new(); + paths.insert(path); + paths + }, + None => { + let mut paths = HashSet::new(); + + let (acl_tree, _) = acl_config::config()?; + paths = populate_acl_paths(paths, acl_tree.root, ""); + + // 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()); + + paths + }, + }; + + let map = paths + .into_iter() + .fold(HashMap::new(), |mut map: HashMap>, path: String| { + let split_path = acl_config::split_acl_path(path.as_str()); + let (privs, propagated_privs) = user_info.lookup_privs_details(&auth_id, &split_path); + + match privs { + 0 => map, // Don't leak ACL paths where we don't have any privileges + _ => { + let priv_map = 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), @@ -245,6 +370,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 12b5a851..f82d5903 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; @@ -246,9 +246,9 @@ impl AclTreeNode { } } - pub fn extract_roles(&self, auth_id: &Authid, all: bool) -> HashSet { + pub fn extract_roles(&self, auth_id: &Authid, all: bool) -> HashMap { let user_roles = self.extract_user_roles(auth_id, all); - if !user_roles.is_empty() { + if !user_roles.is_empty() || auth_id.is_token() { // user privs always override group privs return user_roles }; @@ -256,33 +256,33 @@ impl AclTreeNode { self.extract_group_roles(auth_id.user(), all) } - pub fn extract_user_roles(&self, auth_id: &Authid, all: bool) -> HashSet { + pub fn extract_user_roles(&self, auth_id: &Authid, all: bool) -> HashMap { - let mut set = HashSet::new(); + let mut map = HashMap::new(); 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: &Userid, all: bool) -> HashSet { + pub fn extract_group_roles(&self, _user: &Userid, all: bool) -> HashMap { - 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 +291,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 +346,9 @@ 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> { @@ -512,7 +514,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(); @@ -576,25 +579,26 @@ impl AclTree { Ok(tree) } - pub fn roles(&self, auth_id: &Authid, path: &[&str]) -> HashSet { + pub fn roles(&self, auth_id: &Authid, path: &[&str]) -> HashMap { let mut node = &self.root; - let mut role_set = node.extract_roles(auth_id, 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(auth_id, 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 } } @@ -686,7 +690,7 @@ mod test { let path_vec = super::split_acl_path(path); let mut roles = tree.roles(auth_id, &path_vec) - .iter().map(|v| v.clone()).collect::>(); + .iter().map(|(v, _)| v.clone()).collect::>(); roles.sort(); let roles = roles.join(","); diff --git a/src/config/cached_user_info.rs b/src/config/cached_user_info.rs index 57d53aac..f56c07a8 100644 --- a/src/config/cached_user_info.rs +++ b/src/config/cached_user_info.rs @@ -123,14 +123,23 @@ impl CachedUserInfo { } pub fn lookup_privs(&self, auth_id: &Authid, path: &[&str]) -> u64 { + let (privs, _) = self.lookup_privs_details(auth_id, path); + privs + } + + pub fn lookup_privs_details(&self, auth_id: &Authid, path: &[&str]) -> (u64, u64) { if self.is_superuser(auth_id) { - return ROLE_ADMIN; + return (ROLE_ADMIN, ROLE_ADMIN); } let roles = self.acl_tree.roles(auth_id, 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; } } @@ -139,10 +148,14 @@ impl CachedUserInfo { // limit privs to that of owning user let user_auth_id = Authid::from(auth_id.user().clone()); privs &= self.lookup_privs(&user_auth_id, path); + let (owner_privs, owner_propagated_privs) = self.lookup_privs_details(&user_auth_id, path); + privs &= owner_privs; + propagated_privs &= owner_propagated_privs; } - privs + (privs, propagated_privs) } + } impl UserInformation for CachedUserInfo {