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:
Fabian Grünbichler 2020-10-08 10:34:07 +02:00 committed by Wolfgang Bumiller
parent 6746bbb1a2
commit babab85b56
3 changed files with 181 additions and 35 deletions

View File

@ -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(&current_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)

View File

@ -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(",");

View File

@ -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 {