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 an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.