From: "Fabian Grünbichler" <f.gruenbichler@proxmox.com>
To: pbs-devel@lists.proxmox.com
Subject: [pbs-devel] [PATCH proxmox-backup 07/16] api: add permissions endpoint
Date: Wed, 28 Oct 2020 12:36:30 +0100 [thread overview]
Message-ID: <20201028113632.814586-10-f.gruenbichler@proxmox.com> (raw)
In-Reply-To: <20201028113632.814586-1-f.gruenbichler@proxmox.com>
and adapt privilege calculation to return propagate flag
Signed-off-by: Fabian Grünbichler <f.gruenbichler@proxmox.com>
---
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<Authid>,
+ path: Option<String>,
+ rpcenv: &dyn RpcEnvironment,
+) -> Result<HashMap<String, HashMap<String, bool>>, 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<String>,
+ node: acl_config::AclTreeNode,
+ path: &str
+ ) -> HashSet<String> {
+ 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<String, HashMap<String, bool>>, 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<String> {
+ pub fn extract_roles(&self, auth_id: &Authid, all: bool) -> HashMap<String, bool> {
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<String> {
+ pub fn extract_user_roles(&self, auth_id: &Authid, all: bool) -> HashMap<String, bool> {
- 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<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 +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<String> {
+ pub fn roles(&self, auth_id: &Authid, path: &[&str]) -> HashMap<String, bool> {
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::<Vec<String>>();
+ .iter().map(|(v, _)| v.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 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
next prev parent reply other threads:[~2020-10-28 11:36 UTC|newest]
Thread overview: 25+ messages / expand[flat|nested] mbox.gz Atom feed top
2020-10-28 11:36 [pbs-devel] [PATCH proxmox-backup 00/16] API tokens Fabian Grünbichler
2020-10-28 11:36 ` [pbs-devel] [PATCH proxmox-widget-toolkit] add PermissionView Fabian Grünbichler
2020-10-28 16:18 ` [pbs-devel] applied: " Thomas Lamprecht
2020-10-28 11:36 ` [pbs-devel] [PATCH proxmox-backup 01/16] api: add Authid as wrapper around Userid Fabian Grünbichler
2020-10-28 11:36 ` [pbs-devel] [PATCH proxmox] rpcenv: rename user to auth_id Fabian Grünbichler
2020-10-28 11:36 ` [pbs-devel] [PATCH proxmox-backup 02/16] config: add token.shadow file Fabian Grünbichler
2020-10-28 11:36 ` [pbs-devel] [PATCH proxmox-backup 03/16] replace Userid with Authid Fabian Grünbichler
2020-10-28 11:36 ` [pbs-devel] [PATCH proxmox-backup 04/16] REST: extract and handle API tokens Fabian Grünbichler
2020-10-28 11:36 ` [pbs-devel] [PATCH proxmox-backup 05/16] api: add API token endpoints Fabian Grünbichler
2020-10-28 11:36 ` [pbs-devel] [PATCH proxmox-backup 06/16] api: allow listing users + tokens Fabian Grünbichler
2020-10-28 11:36 ` Fabian Grünbichler [this message]
2020-10-28 11:36 ` [pbs-devel] [PATCH proxmox-backup 08/16] client/remote: allow using ApiToken + secret Fabian Grünbichler
2020-10-28 11:36 ` [pbs-devel] [PATCH proxmox-backup 09/16] owner checks: handle backups owned by API tokens Fabian Grünbichler
2020-10-28 11:37 ` [pbs-devel] [PATCH proxmox-backup 10/16] tasks: allow unpriv users to read their tokens' tasks Fabian Grünbichler
2020-10-28 11:37 ` [pbs-devel] [PATCH proxmox-backup 11/16] manager: add token commands Fabian Grünbichler
2020-10-28 11:37 ` [pbs-devel] [PATCH proxmox-backup 12/16] manager: add user permissions command Fabian Grünbichler
2020-10-28 11:37 ` [pbs-devel] [PATCH proxmox-backup 13/16] gui: add permissions button to user view Fabian Grünbichler
2020-10-28 11:37 ` [pbs-devel] [PATCH proxmox-backup 14/16] gui: add API token UI Fabian Grünbichler
2020-10-28 11:37 ` [pbs-devel] [PATCH proxmox-backup 15/16] acls: allow viewing/editing user's token ACLs Fabian Grünbichler
2020-10-28 11:37 ` [pbs-devel] [PATCH proxmox-backup 16/16] gui: add API " Fabian Grünbichler
2020-10-29 14:23 ` [pbs-devel] applied: [PATCH proxmox-backup 00/16] API tokens Wolfgang Bumiller
2020-10-29 19:50 ` [pbs-devel] " Thomas Lamprecht
2020-10-30 8:03 ` Fabian Grünbichler
2020-10-30 8:48 ` Thomas Lamprecht
2020-10-30 9:55 ` Fabian Grünbichler
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20201028113632.814586-10-f.gruenbichler@proxmox.com \
--to=f.gruenbichler@proxmox.com \
--cc=pbs-devel@lists.proxmox.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox