acl api: implement update

This commit is contained in:
Dietmar Maurer 2020-04-14 08:40:53 +02:00
parent ed3e60ae69
commit 9765092ede
5 changed files with 208 additions and 37 deletions

View File

@ -1,8 +1,7 @@
use failure::*; use failure::*;
use serde_json::Value;
use ::serde::{Deserialize, Serialize}; use ::serde::{Deserialize, Serialize};
use proxmox::api::{api, ApiMethod, Router, RpcEnvironment}; use proxmox::api::{api, Router, RpcEnvironment};
use proxmox::api::schema::{Schema, StringSchema, BooleanSchema, ApiStringFormat}; use proxmox::api::schema::{Schema, StringSchema, BooleanSchema, ApiStringFormat};
use crate::api2::types::*; use crate::api2::types::*;
@ -27,7 +26,14 @@ pub const ACL_UGID_TYPE_SCHEMA: Schema = StringSchema::new(
pub const ACL_ROLE_SCHEMA: Schema = StringSchema::new( pub const ACL_ROLE_SCHEMA: Schema = StringSchema::new(
"Role.") "Role.")
.format(&ApiStringFormat::Enum(&["Admin", "User", "Audit", "NoAccess"])) .format(&ApiStringFormat::Enum(&[
"Admin",
"Audit",
"Datastore.Admin",
"Datastore.Audit",
"Datastore.User",
"NoAccess",
]))
.schema(); .schema();
#[api( #[api(
@ -109,7 +115,8 @@ pub fn read_acl(
//let auth_user = rpcenv.get_user().unwrap(); //let auth_user = rpcenv.get_user().unwrap();
let (tree, digest) = acl::config()?; // fixme: return digest?
let (tree, _digest) = acl::config()?;
let mut list: Vec<AclListItem> = Vec::new(); let mut list: Vec<AclListItem> = Vec::new();
extract_acl_node_data(&tree.root, "", &mut list); extract_acl_node_data(&tree.root, "", &mut list);
@ -117,5 +124,86 @@ pub fn read_acl(
Ok(list) Ok(list)
} }
#[api(
input: {
properties: {
path: {
schema: ACL_PATH_SCHEMA,
},
role: {
schema: ACL_ROLE_SCHEMA,
},
propagate: {
optional: true,
schema: ACL_PROPAGATE_SCHEMA,
},
userid: {
optional: true,
schema: PROXMOX_USER_ID_SCHEMA,
},
group: {
optional: true,
schema: PROXMOX_GROUP_ID_SCHEMA,
},
delete: {
optional: true,
description: "Remove permissions (instead of adding it).",
type: bool,
},
digest: {
optional: true,
schema: PROXMOX_CONFIG_DIGEST_SCHEMA,
},
},
},
)]
/// Update Access Control List (ACLs).
pub fn update_acl(
path: String,
role: String,
propagate: Option<bool>,
userid: Option<String>,
group: Option<String>,
delete: Option<bool>,
digest: Option<String>,
_rpcenv: &mut dyn RpcEnvironment,
) -> Result<(), Error> {
let _lock = crate::tools::open_file_locked(acl::ACL_CFG_LOCKFILE, std::time::Duration::new(10, 0))?;
let (mut tree, expected_digest) = acl::config()?;
if let Some(ref digest) = digest {
let digest = proxmox::tools::hex_to_digest(digest)?;
crate::tools::detect_modified_configuration_file(&digest, &expected_digest)?;
}
// fixme: test if user/group exists?
// fixme: let propagate = propagate.unwrap_or(api_get_default!("propagate"));
let propagate = propagate.unwrap_or(true);
let delete = delete.unwrap_or(false);
if let Some(userid) = userid {
if delete {
tree.delete_user_role(&path, &userid, &role);
} else {
tree.insert_user_role(&path, &userid, &role, propagate);
}
} else if let Some(group) = group {
if delete {
tree.delete_group_role(&path, &group, &role);
} else {
tree.insert_group_role(&path, &group, &role, propagate);
}
}
acl::save_config(&tree)?;
Ok(())
}
pub const ROUTER: Router = Router::new() pub const ROUTER: Router = Router::new()
.get(&API_METHOD_READ_ACL); .get(&API_METHOD_READ_ACL)
.put(&API_METHOD_UPDATE_ACL);

View File

@ -25,6 +25,7 @@ macro_rules! DNS_NAME { () => (concat!(r"(?:", DNS_LABEL!() , r"\.)*", DNS_LABEL
// slash is not allowed because it is used as pve API delimiter // slash is not allowed because it is used as pve API delimiter
// also see "man useradd" // also see "man useradd"
macro_rules! USER_NAME_REGEX_STR { () => (r"(?:[^\s:/[:cntrl:]]+)") } macro_rules! USER_NAME_REGEX_STR { () => (r"(?:[^\s:/[:cntrl:]]+)") }
macro_rules! GROUP_NAME_REGEX_STR { () => (USER_NAME_REGEX_STR!()) }
macro_rules! PROXMOX_SAFE_ID_REGEX_STR { () => (r"(?:[A-Za-z0-9_][A-Za-z0-9._\-]*)") } macro_rules! PROXMOX_SAFE_ID_REGEX_STR { () => (r"(?:[A-Za-z0-9_][A-Za-z0-9._\-]*)") }
@ -54,9 +55,11 @@ const_regex!{
pub PROXMOX_USER_ID_REGEX = concat!(r"^", USER_NAME_REGEX_STR!(), r"@", PROXMOX_SAFE_ID_REGEX_STR!(), r"$"); pub PROXMOX_USER_ID_REGEX = concat!(r"^", USER_NAME_REGEX_STR!(), r"@", PROXMOX_SAFE_ID_REGEX_STR!(), r"$");
pub PROXMOX_GROUP_ID_REGEX = concat!(r"^", GROUP_NAME_REGEX_STR!(), r"$");
pub CERT_FINGERPRINT_SHA256_REGEX = r"^(?:[0-9a-fA-F][0-9a-fA-F])(?::[0-9a-fA-F][0-9a-fA-F]){31}$"; pub CERT_FINGERPRINT_SHA256_REGEX = r"^(?:[0-9a-fA-F][0-9a-fA-F])(?::[0-9a-fA-F][0-9a-fA-F]){31}$";
pub ACL_PATH_REGEX = concat!(r"^(?:\/|", r"(?:\/", PROXMOX_SAFE_ID_REGEX_STR!(), ")+", r")$"); pub ACL_PATH_REGEX = concat!(r"^(?:/|", r"(?:/", PROXMOX_SAFE_ID_REGEX_STR!(), ")+", r")$");
} }
pub const SYSTEMD_DATETIME_FORMAT: ApiStringFormat = pub const SYSTEMD_DATETIME_FORMAT: ApiStringFormat =
@ -89,6 +92,9 @@ pub const DNS_NAME_OR_IP_FORMAT: ApiStringFormat =
pub const PROXMOX_USER_ID_FORMAT: ApiStringFormat = pub const PROXMOX_USER_ID_FORMAT: ApiStringFormat =
ApiStringFormat::Pattern(&PROXMOX_USER_ID_REGEX); ApiStringFormat::Pattern(&PROXMOX_USER_ID_REGEX);
pub const PROXMOX_GROUP_ID_FORMAT: ApiStringFormat =
ApiStringFormat::Pattern(&PROXMOX_GROUP_ID_REGEX);
pub const PASSWORD_FORMAT: ApiStringFormat = pub const PASSWORD_FORMAT: ApiStringFormat =
ApiStringFormat::Pattern(&PASSWORD_REGEX); ApiStringFormat::Pattern(&PASSWORD_REGEX);
@ -218,6 +224,12 @@ pub const PROXMOX_USER_ID_SCHEMA: Schema = StringSchema::new("User ID")
.max_length(64) .max_length(64)
.schema(); .schema();
pub const PROXMOX_GROUP_ID_SCHEMA: Schema = StringSchema::new("Group ID")
.format(&PROXMOX_GROUP_ID_FORMAT)
.min_length(3)
.max_length(64)
.schema();
// Complex type definitions // Complex type definitions

View File

@ -220,7 +220,15 @@ fn list_acls(param: Value, rpcenv: &mut dyn RpcEnvironment) -> Result<Value, Err
fn acl_commands() -> CommandLineInterface { fn acl_commands() -> CommandLineInterface {
let cmd_def = CliCommandMap::new() let cmd_def = CliCommandMap::new()
.insert("list", CliCommand::new(&&API_METHOD_LIST_ACLS)); .insert("list", CliCommand::new(&&API_METHOD_LIST_ACLS))
.insert(
"update",
CliCommand::new(&api2::access::acl::API_METHOD_UPDATE_ACL)
.arg_param(&["path", "role"])
.completion_cb("userid", config::user::complete_user_name)
.completion_cb("path", config::datastore::complete_acl_path)
);
cmd_def.into() cmd_def.into()
} }

View File

@ -10,29 +10,31 @@ use proxmox::tools::{fs::replace_file, fs::CreateOptions};
// define Privilege bitfield // define Privilege bitfield
pub const PRIV_SYS_AUDIT: u64 = 1 << 0; pub const PRIV_SYS_AUDIT: u64 = 1 << 0;
pub const PRIV_SYS_MODIFY: u64 = 1 << 1; pub const PRIV_SYS_MODIFY: u64 = 1 << 1;
pub const PRIV_SYS_POWER_MANAGEMENT: u64 = 1 << 2; pub const PRIV_SYS_POWER_MANAGEMENT: u64 = 1 << 2;
pub const PRIV_STORE_AUDIT: u64 = 1 << 3; pub const PRIV_DATASTORE_AUDIT: u64 = 1 << 3;
pub const PRIV_STORE_ALLOCATE: u64 = 1 << 4; pub const PRIV_DATASTORE_ALLOCATE: u64 = 1 << 4;
pub const PRIV_STORE_ALLOCATE_SPACE: u64 = 1 << 5; pub const PRIV_DATASTORE_ALLOCATE_SPACE: u64 = 1 << 5;
pub const ROLE_ADMIN: u64 = std::u64::MAX; pub const ROLE_ADMIN: u64 = std::u64::MAX;
pub const ROLE_NO_ACCESS: u64 = 0; pub const ROLE_NO_ACCESS: u64 = 0;
pub const ROLE_AUDIT: u64 = pub const ROLE_AUDIT: u64 =
PRIV_SYS_AUDIT | PRIV_SYS_AUDIT |
PRIV_STORE_AUDIT; PRIV_DATASTORE_AUDIT;
pub const ROLE_STORE_ADMIN: u64 = pub const ROLE_DATASTORE_ADMIN: u64 =
PRIV_STORE_AUDIT | PRIV_DATASTORE_AUDIT |
PRIV_STORE_ALLOCATE | PRIV_DATASTORE_ALLOCATE |
PRIV_STORE_ALLOCATE_SPACE; PRIV_DATASTORE_ALLOCATE_SPACE;
pub const ROLE_STORE_USER: u64 = pub const ROLE_DATASTORE_USER: u64 =
PRIV_STORE_AUDIT | PRIV_DATASTORE_AUDIT |
PRIV_STORE_ALLOCATE_SPACE; PRIV_DATASTORE_ALLOCATE_SPACE;
pub const ROLE_DATASTORE_AUDIT: u64 = PRIV_DATASTORE_AUDIT;
lazy_static! { lazy_static! {
static ref ROLE_NAMES: HashMap<&'static str, u64> = { static ref ROLE_NAMES: HashMap<&'static str, u64> = {
@ -42,8 +44,9 @@ lazy_static! {
map.insert("Audit", ROLE_AUDIT); map.insert("Audit", ROLE_AUDIT);
map.insert("NoAccess", ROLE_NO_ACCESS); map.insert("NoAccess", ROLE_NO_ACCESS);
map.insert("Store.Admin", ROLE_STORE_ADMIN); map.insert("Datastore.Admin", ROLE_DATASTORE_ADMIN);
map.insert("Store.User", ROLE_STORE_USER); map.insert("Datastore.User", ROLE_DATASTORE_USER);
map.insert("Datastore.Audit", ROLE_DATASTORE_AUDIT);
map map
}; };
@ -141,6 +144,22 @@ impl AclTreeNode {
set set
} }
pub fn delete_group_role(&mut self, group: &str, role: &str) {
let roles = match self.groups.get_mut(group) {
Some(r) => r,
None => return,
};
roles.remove(role);
}
pub fn delete_user_role(&mut self, userid: &str, role: &str) {
let roles = match self.users.get_mut(userid) {
Some(r) => r,
None => return,
};
roles.remove(role);
}
pub fn insert_group_role(&mut self, group: String, role: String, propagate: bool) { pub fn insert_group_role(&mut self, group: String, role: String, propagate: bool) {
self.groups self.groups
.entry(group).or_insert_with(|| HashMap::new()) .entry(group).or_insert_with(|| HashMap::new())
@ -160,6 +179,17 @@ impl AclTree {
Self { root: AclTreeNode::new() } Self { root: AclTreeNode::new() }
} }
fn get_node(&mut self, path: &[&str]) -> Option<&mut AclTreeNode> {
let mut node = &mut self.root;
for comp in path {
node = match node.children.get_mut(*comp) {
Some(n) => n,
None => return None,
};
}
Some(node)
}
fn get_or_insert_node(&mut self, path: &[&str]) -> &mut AclTreeNode { fn get_or_insert_node(&mut self, path: &[&str]) -> &mut AclTreeNode {
let mut node = &mut self.root; let mut node = &mut self.root;
for comp in path { for comp in path {
@ -169,6 +199,24 @@ impl AclTree {
node node
} }
pub fn delete_group_role(&mut self, path: &str, group: &str, role: &str) {
let path = split_acl_path(path);
let node = match self.get_node(&path) {
Some(n) => n,
None => return,
};
node.delete_group_role(group, role);
}
pub fn delete_user_role(&mut self, path: &str, userid: &str, role: &str) {
let path = split_acl_path(path);
let node = match self.get_node(&path) {
Some(n) => n,
None => return,
};
node.delete_user_role(userid, role);
}
pub fn insert_group_role(&mut self, path: &str, group: &str, role: &str, propagate: bool) { pub fn insert_group_role(&mut self, path: &str, group: &str, role: &str, propagate: bool) {
let path = split_acl_path(path); let path = split_acl_path(path);
let node = self.get_or_insert_node(&path); let node = self.get_or_insert_node(&path);
@ -382,7 +430,7 @@ pub fn config() -> Result<(AclTree, [u8; 32]), Error> {
AclTree::load(&path) AclTree::load(&path)
} }
pub fn store_config(acl: &AclTree, filename: &Path) -> Result<(), Error> { pub fn save_config(acl: &AclTree) -> Result<(), Error> {
let mut raw: Vec<u8> = Vec::new(); let mut raw: Vec<u8> = Vec::new();
acl.write_config(&mut raw)?; acl.write_config(&mut raw)?;
@ -396,12 +444,11 @@ pub fn store_config(acl: &AclTree, filename: &Path) -> Result<(), Error> {
.owner(nix::unistd::ROOT) .owner(nix::unistd::ROOT)
.group(backup_user.gid); .group(backup_user.gid);
replace_file(filename, &raw, options)?; replace_file(ACL_CFG_FILENAME, &raw, options)?;
Ok(()) Ok(())
} }
#[cfg(test)] #[cfg(test)]
mod test { mod test {
@ -430,15 +477,15 @@ mod test {
let tree = AclTree::from_raw(r###" let tree = AclTree::from_raw(r###"
acl:0:/store/store2:user1:Admin acl:0:/store/store2:user1:Admin
acl:0:/store/store2:user2:Admin acl:0:/store/store2:user2:Admin
acl:0:/store/store2:user1:Store.User acl:0:/store/store2:user1:Datastore.User
acl:0:/store/store2:user2:Store.User acl:0:/store/store2:user2:Datastore.User
"###)?; "###)?;
let mut raw: Vec<u8> = Vec::new(); let mut raw: Vec<u8> = Vec::new();
tree.write_config(&mut raw)?; tree.write_config(&mut raw)?;
let raw = std::str::from_utf8(&raw)?; let raw = std::str::from_utf8(&raw)?;
assert_eq!(raw, "acl:0:/store/store2:user1,user2:Admin,Store.User\n"); assert_eq!(raw, "acl:0:/store/store2:user1,user2:Admin,Datastore.User\n");
Ok(()) Ok(())
} }
@ -448,18 +495,18 @@ acl:0:/store/store2:user2:Store.User
let tree = AclTree::from_raw(r###" let tree = AclTree::from_raw(r###"
acl:1:/storage:user1@pbs:Admin acl:1:/storage:user1@pbs:Admin
acl:1:/storage/store1:user1@pbs:Store.User acl:1:/storage/store1:user1@pbs:Datastore.User
acl:1:/storage/store2:user2@pbs:Store.User acl:1:/storage/store2:user2@pbs:Datastore.User
"###)?; "###)?;
check_roles(&tree, "user1@pbs", "/", ""); check_roles(&tree, "user1@pbs", "/", "");
check_roles(&tree, "user1@pbs", "/storage", "Admin"); check_roles(&tree, "user1@pbs", "/storage", "Admin");
check_roles(&tree, "user1@pbs", "/storage/store1", "Store.User"); check_roles(&tree, "user1@pbs", "/storage/store1", "Datastore.User");
check_roles(&tree, "user1@pbs", "/storage/store2", "Admin"); check_roles(&tree, "user1@pbs", "/storage/store2", "Admin");
check_roles(&tree, "user2@pbs", "/", ""); check_roles(&tree, "user2@pbs", "/", "");
check_roles(&tree, "user2@pbs", "/storage", ""); check_roles(&tree, "user2@pbs", "/storage", "");
check_roles(&tree, "user2@pbs", "/storage/store1", ""); check_roles(&tree, "user2@pbs", "/storage/store1", "");
check_roles(&tree, "user2@pbs", "/storage/store2", "Store.User"); check_roles(&tree, "user2@pbs", "/storage/store2", "Datastore.User");
Ok(()) Ok(())
} }
@ -470,22 +517,22 @@ acl:1:/storage/store2:user2@pbs:Store.User
let tree = AclTree::from_raw(r###" let tree = AclTree::from_raw(r###"
acl:1:/:user1@pbs:Admin acl:1:/:user1@pbs:Admin
acl:1:/storage:user1@pbs:NoAccess acl:1:/storage:user1@pbs:NoAccess
acl:1:/storage/store1:user1@pbs:Store.User acl:1:/storage/store1:user1@pbs:Datastore.User
"###)?; "###)?;
check_roles(&tree, "user1@pbs", "/", "Admin"); check_roles(&tree, "user1@pbs", "/", "Admin");
check_roles(&tree, "user1@pbs", "/storage", "NoAccess"); check_roles(&tree, "user1@pbs", "/storage", "NoAccess");
check_roles(&tree, "user1@pbs", "/storage/store1", "Store.User"); check_roles(&tree, "user1@pbs", "/storage/store1", "Datastore.User");
check_roles(&tree, "user1@pbs", "/storage/store2", "NoAccess"); check_roles(&tree, "user1@pbs", "/storage/store2", "NoAccess");
check_roles(&tree, "user1@pbs", "/system", "Admin"); check_roles(&tree, "user1@pbs", "/system", "Admin");
let tree = AclTree::from_raw(r###" let tree = AclTree::from_raw(r###"
acl:1:/:user1@pbs:Admin acl:1:/:user1@pbs:Admin
acl:0:/storage:user1@pbs:NoAccess acl:0:/storage:user1@pbs:NoAccess
acl:1:/storage/store1:user1@pbs:Store.User acl:1:/storage/store1:user1@pbs:Datastore.User
"###)?; "###)?;
check_roles(&tree, "user1@pbs", "/", "Admin"); check_roles(&tree, "user1@pbs", "/", "Admin");
check_roles(&tree, "user1@pbs", "/storage", "NoAccess"); check_roles(&tree, "user1@pbs", "/storage", "NoAccess");
check_roles(&tree, "user1@pbs", "/storage/store1", "Store.User"); check_roles(&tree, "user1@pbs", "/storage/store1", "Datastore.User");
check_roles(&tree, "user1@pbs", "/storage/store2", "Admin"); check_roles(&tree, "user1@pbs", "/storage/store2", "Admin");
check_roles(&tree, "user1@pbs", "/system", "Admin"); check_roles(&tree, "user1@pbs", "/system", "Admin");

View File

@ -101,3 +101,19 @@ pub fn complete_datastore_name(_arg: &str, _param: &HashMap<String, String>) ->
Err(_) => return vec![], Err(_) => return vec![],
} }
} }
pub fn complete_acl_path(_arg: &str, _param: &HashMap<String, String>) -> Vec<String> {
let mut list = Vec::new();
list.push(String::from("/"));
list.push(String::from("/storage"));
list.push(String::from("/storage/"));
if let Ok((data, _digest)) = config() {
for id in data.sections.keys() {
list.push(format!("/storage/{}", id));
}
}
list
}