From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [212.224.123.68]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits)) (No client certificate requested) by lists.proxmox.com (Postfix) with ESMTPS id 5699562C6C for ; Wed, 28 Oct 2020 12:36:49 +0100 (CET) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 544D31E9E7 for ; Wed, 28 Oct 2020 12:36:49 +0100 (CET) Received: from proxmox-new.maurer-it.com (proxmox-new.maurer-it.com [212.186.127.180]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits)) (No client certificate requested) by firstgate.proxmox.com (Proxmox) with ESMTPS id 2869F1E98F for ; Wed, 28 Oct 2020 12:36:47 +0100 (CET) Received: from proxmox-new.maurer-it.com (localhost.localdomain [127.0.0.1]) by proxmox-new.maurer-it.com (Proxmox) with ESMTP id E30AA459B0 for ; Wed, 28 Oct 2020 12:36:46 +0100 (CET) From: =?UTF-8?q?Fabian=20Gr=C3=BCnbichler?= To: pbs-devel@lists.proxmox.com Date: Wed, 28 Oct 2020 12:36:30 +0100 Message-Id: <20201028113632.814586-10-f.gruenbichler@proxmox.com> X-Mailer: git-send-email 2.20.1 In-Reply-To: <20201028113632.814586-1-f.gruenbichler@proxmox.com> References: <20201028113632.814586-1-f.gruenbichler@proxmox.com> MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit X-SPAM-LEVEL: Spam detection results: 0 AWL 0.029 Adjusted score from AWL reputation of From: address KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment RCVD_IN_DNSWL_MED -2.3 Sender listed at https://www.dnswl.org/, medium trust SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record URIBL_BLOCKED 0.001 ADMINISTRATOR NOTICE: The query to URIBL was blocked. See http://wiki.apache.org/spamassassin/DnsBlocklists#dnsbl-block for more information. [access.rs, acl.rs] Subject: [pbs-devel] [PATCH proxmox-backup 07/16] api: add permissions endpoint X-BeenThere: pbs-devel@lists.proxmox.com X-Mailman-Version: 2.1.29 Precedence: list List-Id: Proxmox Backup Server development discussion List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , X-List-Received-Date: Wed, 28 Oct 2020 11:36:49 -0000 and adapt privilege calculation to return propagate flag Signed-off-by: Fabian Grünbichler --- changes since RFC: - drop paths field from ACL tree again - filter out paths with no permissions 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 { -- 2.20.1