api: add permissions endpoint
and adapt privilege calculation to return propagate flag Signed-off-by: Fabian Grünbichler <f.gruenbichler@proxmox.com>
This commit is contained in:
parent
6746bbb1a2
commit
babab85b56
@ -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)
|
||||
|
@ -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(",");
|
||||
|
||||
|
@ -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 {
|
||||
|
Loading…
Reference in New Issue
Block a user