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 B26D862E19 for ; Wed, 28 Oct 2020 12:37:24 +0100 (CET) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id B038E1ED8C for ; Wed, 28 Oct 2020 12:37:24 +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 B48DE1ED7C for ; Wed, 28 Oct 2020 12:37:23 +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 7EA45434E4 for ; Wed, 28 Oct 2020 12:37:23 +0100 (CET) From: =?UTF-8?q?Fabian=20Gr=C3=BCnbichler?= To: pbs-devel@lists.proxmox.com Date: Wed, 28 Oct 2020 12:37:16 +0100 Message-Id: <20201028113717.827644-6-f.gruenbichler@proxmox.com> X-Mailer: git-send-email 2.20.1 In-Reply-To: <20201028113717.827644-1-f.gruenbichler@proxmox.com> References: <20201028113632.814586-1-f.gruenbichler@proxmox.com> <20201028113717.827644-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.028 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. [acl.rs] Subject: [pbs-devel] [PATCH proxmox-backup 15/16] acls: allow viewing/editing user's token ACLs 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:37:24 -0000 even for otherwise unprivileged users. since effective privileges of an API token are always intersected with those of their owning user, this does not allow an unprivileged user to elevate their privileges in practice, but avoids the need to involve a privileged user to deploy API tokens. Signed-off-by: Fabian Grünbichler --- src/api2/access/acl.rs | 67 +++++++++++++++++++++++++++++++++++++----- 1 file changed, 60 insertions(+), 7 deletions(-) diff --git a/src/api2/access/acl.rs b/src/api2/access/acl.rs index 7211a6be..a776ceaa 100644 --- a/src/api2/access/acl.rs +++ b/src/api2/access/acl.rs @@ -7,6 +7,7 @@ use proxmox::tools::fs::open_file_locked; use crate::api2::types::*; use crate::config::acl; use crate::config::acl::{Role, PRIV_SYS_AUDIT, PRIV_PERMISSIONS_MODIFY}; +use crate::config::cached_user_info::CachedUserInfo; #[api( properties: { @@ -43,8 +44,23 @@ fn extract_acl_node_data( path: &str, list: &mut Vec, exact: bool, + token_user: &Option, ) { + // tokens can't have tokens, so we can early return + if let Some(token_user) = token_user { + if token_user.is_token() { + return; + } + } + for (user, roles) in &node.users { + if let Some(token_user) = token_user { + if !user.is_token() + || user.user() != token_user.user() { + continue; + } + } + for (role, propagate) in roles { list.push(AclListItem { path: if path.is_empty() { String::from("/") } else { path.to_string() }, @@ -56,6 +72,10 @@ fn extract_acl_node_data( } } for (group, roles) in &node.groups { + if let Some(_) = token_user { + continue; + } + for (role, propagate) in roles { list.push(AclListItem { path: if path.is_empty() { String::from("/") } else { path.to_string() }, @@ -71,7 +91,7 @@ fn extract_acl_node_data( } for (comp, child) in &node.children { let new_path = format!("{}/{}", path, comp); - extract_acl_node_data(child, &new_path, list, exact); + extract_acl_node_data(child, &new_path, list, exact, token_user); } } @@ -98,7 +118,8 @@ fn extract_acl_node_data( } }, access: { - permission: &Permission::Privilege(&["access", "acl"], PRIV_SYS_AUDIT, false), + permission: &Permission::Anybody, + description: "Returns all ACLs if user has Sys.Audit on '/access/acl', or just the ACLs containing the user's API tokens.", }, )] /// Read Access Control List (ACLs). @@ -107,18 +128,26 @@ pub fn read_acl( exact: bool, mut rpcenv: &mut dyn RpcEnvironment, ) -> Result, Error> { + let auth_id = rpcenv.get_auth_id().unwrap().parse()?; - //let auth_user = rpcenv.get_user().unwrap(); + let user_info = CachedUserInfo::new()?; + + let top_level_privs = user_info.lookup_privs(&auth_id, &["access", "acl"]); + let auth_id_filter = if (top_level_privs & PRIV_SYS_AUDIT) == 0 { + Some(auth_id) + } else { + None + }; let (mut tree, digest) = acl::config()?; let mut list: Vec = Vec::new(); if let Some(path) = &path { if let Some(node) = &tree.find_node(path) { - extract_acl_node_data(&node, path, &mut list, exact); + extract_acl_node_data(&node, path, &mut list, exact, &auth_id_filter); } } else { - extract_acl_node_data(&tree.root, "", &mut list, exact); + extract_acl_node_data(&tree.root, "", &mut list, exact, &auth_id_filter); } rpcenv["digest"] = proxmox::tools::digest_to_hex(&digest).into(); @@ -160,7 +189,8 @@ pub fn read_acl( }, }, access: { - permission: &Permission::Privilege(&["access", "acl"], PRIV_PERMISSIONS_MODIFY, false), + permission: &Permission::Anybody, + description: "Requires Permissions.Modify on '/access/acl', limited to updating ACLs of the user's API tokens otherwise." }, )] /// Update Access Control List (ACLs). @@ -172,8 +202,31 @@ pub fn update_acl( group: Option, delete: Option, digest: Option, - _rpcenv: &mut dyn RpcEnvironment, + rpcenv: &mut dyn RpcEnvironment, ) -> Result<(), Error> { + let current_auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?; + + let user_info = CachedUserInfo::new()?; + + let top_level_privs = user_info.lookup_privs(¤t_auth_id, &["access", "acl"]); + if top_level_privs & PRIV_PERMISSIONS_MODIFY == 0 { + if let Some(_) = group { + bail!("Unprivileged users are not allowed to create group ACL item."); + } + + match &auth_id { + Some(auth_id) => { + if current_auth_id.is_token() { + bail!("Unprivileged API tokens can't set ACL items."); + } else if !auth_id.is_token() { + bail!("Unprivileged users can only set ACL items for API tokens."); + } else if auth_id.user() != current_auth_id.user() { + bail!("Unprivileged users can only set ACL items for their own API tokens."); + } + }, + None => { bail!("Unprivileged user needs to provide auth_id to update ACL item."); }, + }; + } let _lock = open_file_locked(acl::ACL_CFG_LOCKFILE, std::time::Duration::new(10, 0), true)?; -- 2.20.1