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 anyhow::{bail, format_err, Error};
|
||||||
|
|
||||||
use serde_json::{json, Value};
|
use serde_json::{json, Value};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
use proxmox::api::{api, RpcEnvironment, Permission};
|
use proxmox::api::{api, RpcEnvironment, Permission};
|
||||||
use proxmox::api::router::{Router, SubdirMap};
|
use proxmox::api::router::{Router, SubdirMap};
|
||||||
|
@ -12,8 +14,9 @@ use crate::auth_helpers::*;
|
||||||
use crate::api2::types::*;
|
use crate::api2::types::*;
|
||||||
use crate::tools::{FileLogOptions, FileLogger};
|
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::cached_user_info::CachedUserInfo;
|
||||||
use crate::config::acl::{PRIVILEGES, PRIV_PERMISSIONS_MODIFY};
|
|
||||||
|
|
||||||
pub mod user;
|
pub mod user;
|
||||||
pub mod domain;
|
pub mod domain;
|
||||||
|
@ -238,6 +241,128 @@ fn change_password(
|
||||||
Ok(Value::Null)
|
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]
|
#[sortable]
|
||||||
const SUBDIRS: SubdirMap = &sorted!([
|
const SUBDIRS: SubdirMap = &sorted!([
|
||||||
("acl", &acl::ROUTER),
|
("acl", &acl::ROUTER),
|
||||||
|
@ -245,6 +370,10 @@ const SUBDIRS: SubdirMap = &sorted!([
|
||||||
"password", &Router::new()
|
"password", &Router::new()
|
||||||
.put(&API_METHOD_CHANGE_PASSWORD)
|
.put(&API_METHOD_CHANGE_PASSWORD)
|
||||||
),
|
),
|
||||||
|
(
|
||||||
|
"permissions", &Router::new()
|
||||||
|
.get(&API_METHOD_LIST_PERMISSIONS)
|
||||||
|
),
|
||||||
(
|
(
|
||||||
"ticket", &Router::new()
|
"ticket", &Router::new()
|
||||||
.post(&API_METHOD_CREATE_TICKET)
|
.post(&API_METHOD_CREATE_TICKET)
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
use std::collections::{HashMap, HashSet, BTreeMap, BTreeSet};
|
use std::collections::{HashMap, BTreeMap, BTreeSet};
|
||||||
use std::path::{PathBuf, Path};
|
use std::path::{PathBuf, Path};
|
||||||
use std::sync::{Arc, RwLock};
|
use std::sync::{Arc, RwLock};
|
||||||
use std::str::FromStr;
|
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);
|
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
|
// user privs always override group privs
|
||||||
return user_roles
|
return user_roles
|
||||||
};
|
};
|
||||||
|
@ -256,33 +256,33 @@ impl AclTreeNode {
|
||||||
self.extract_group_roles(auth_id.user(), all)
|
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) {
|
let roles = match self.users.get(auth_id) {
|
||||||
Some(m) => m,
|
Some(m) => m,
|
||||||
None => return set,
|
None => return map,
|
||||||
};
|
};
|
||||||
|
|
||||||
for (role, propagate) in roles {
|
for (role, propagate) in roles {
|
||||||
if *propagate || all {
|
if *propagate || all {
|
||||||
if role == ROLE_NAME_NO_ACCESS {
|
if role == ROLE_NAME_NO_ACCESS {
|
||||||
// return a set with a single role 'NoAccess'
|
// return a map with a single role 'NoAccess'
|
||||||
let mut set = HashSet::new();
|
let mut map = HashMap::new();
|
||||||
set.insert(role.to_string());
|
map.insert(role.to_string(), false);
|
||||||
return set;
|
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 {
|
for (_group, roles) in &self.groups {
|
||||||
let is_member = false; // fixme: check if user is member of the group
|
let is_member = false; // fixme: check if user is member of the group
|
||||||
|
@ -291,17 +291,17 @@ impl AclTreeNode {
|
||||||
for (role, propagate) in roles {
|
for (role, propagate) in roles {
|
||||||
if *propagate || all {
|
if *propagate || all {
|
||||||
if role == ROLE_NAME_NO_ACCESS {
|
if role == ROLE_NAME_NO_ACCESS {
|
||||||
// return a set with a single role 'NoAccess'
|
// return a map with a single role 'NoAccess'
|
||||||
let mut set = HashSet::new();
|
let mut map = HashMap::new();
|
||||||
set.insert(role.to_string());
|
map.insert(role.to_string(), false);
|
||||||
return set;
|
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) {
|
pub fn delete_group_role(&mut self, group: &str, role: &str) {
|
||||||
|
@ -346,7 +346,9 @@ impl AclTreeNode {
|
||||||
impl AclTree {
|
impl AclTree {
|
||||||
|
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self { root: AclTreeNode::new() }
|
Self {
|
||||||
|
root: AclTreeNode::new(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn find_node(&mut self, path: &str) -> Option<&mut AclTreeNode> {
|
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.");
|
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 node = self.get_or_insert_node(&path);
|
||||||
|
|
||||||
let uglist: Vec<&str> = items[3].split(',').map(|v| v.trim()).collect();
|
let uglist: Vec<&str> = items[3].split(',').map(|v| v.trim()).collect();
|
||||||
|
@ -576,25 +579,26 @@ impl AclTree {
|
||||||
Ok(tree)
|
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 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() {
|
for (pos, comp) in path.iter().enumerate() {
|
||||||
let last_comp = (pos + 1) == path.len();
|
let last_comp = (pos + 1) == path.len();
|
||||||
node = match node.children.get(*comp) {
|
node = match node.children.get(*comp) {
|
||||||
Some(n) => n,
|
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() {
|
let new_map = node.extract_roles(auth_id, last_comp);
|
||||||
// overwrite previous settings
|
if !new_map.is_empty() {
|
||||||
role_set = new_set;
|
// 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 path_vec = super::split_acl_path(path);
|
||||||
let mut roles = tree.roles(auth_id, &path_vec)
|
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();
|
roles.sort();
|
||||||
let roles = roles.join(",");
|
let roles = roles.join(",");
|
||||||
|
|
||||||
|
|
|
@ -123,14 +123,23 @@ impl CachedUserInfo {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn lookup_privs(&self, auth_id: &Authid, path: &[&str]) -> u64 {
|
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) {
|
if self.is_superuser(auth_id) {
|
||||||
return ROLE_ADMIN;
|
return (ROLE_ADMIN, ROLE_ADMIN);
|
||||||
}
|
}
|
||||||
|
|
||||||
let roles = self.acl_tree.roles(auth_id, path);
|
let roles = self.acl_tree.roles(auth_id, path);
|
||||||
let mut privs: u64 = 0;
|
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 let Some((role_privs, _)) = ROLE_NAMES.get(role.as_str()) {
|
||||||
|
if propagate {
|
||||||
|
propagated_privs |= role_privs;
|
||||||
|
}
|
||||||
privs |= role_privs;
|
privs |= role_privs;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -139,10 +148,14 @@ impl CachedUserInfo {
|
||||||
// limit privs to that of owning user
|
// limit privs to that of owning user
|
||||||
let user_auth_id = Authid::from(auth_id.user().clone());
|
let user_auth_id = Authid::from(auth_id.user().clone());
|
||||||
privs &= self.lookup_privs(&user_auth_id, path);
|
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 {
|
impl UserInformation for CachedUserInfo {
|
||||||
|
|
Loading…
Reference in New Issue