diff --git a/src/api2/access/acl.rs b/src/api2/access/acl.rs index 04be51f6..0f80314c 100644 --- a/src/api2/access/acl.rs +++ b/src/api2/access/acl.rs @@ -1,8 +1,7 @@ use failure::*; -use serde_json::Value; 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 crate::api2::types::*; @@ -27,7 +26,14 @@ pub const ACL_UGID_TYPE_SCHEMA: Schema = StringSchema::new( pub const ACL_ROLE_SCHEMA: Schema = StringSchema::new( "Role.") - .format(&ApiStringFormat::Enum(&["Admin", "User", "Audit", "NoAccess"])) + .format(&ApiStringFormat::Enum(&[ + "Admin", + "Audit", + "Datastore.Admin", + "Datastore.Audit", + "Datastore.User", + "NoAccess", + ])) .schema(); #[api( @@ -109,7 +115,8 @@ pub fn read_acl( //let auth_user = rpcenv.get_user().unwrap(); - let (tree, digest) = acl::config()?; + // fixme: return digest? + let (tree, _digest) = acl::config()?; let mut list: Vec = Vec::new(); extract_acl_node_data(&tree.root, "", &mut list); @@ -117,5 +124,86 @@ pub fn read_acl( 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, + userid: Option, + group: Option, + delete: Option, + digest: Option, + _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() - .get(&API_METHOD_READ_ACL); + .get(&API_METHOD_READ_ACL) + .put(&API_METHOD_UPDATE_ACL); diff --git a/src/api2/types.rs b/src/api2/types.rs index 7132b4be..9e153d89 100644 --- a/src/api2/types.rs +++ b/src/api2/types.rs @@ -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 // also see "man useradd" 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._\-]*)") } @@ -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_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 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 = @@ -89,6 +92,9 @@ pub const DNS_NAME_OR_IP_FORMAT: ApiStringFormat = pub const PROXMOX_USER_ID_FORMAT: ApiStringFormat = ApiStringFormat::Pattern(&PROXMOX_USER_ID_REGEX); +pub const PROXMOX_GROUP_ID_FORMAT: ApiStringFormat = + ApiStringFormat::Pattern(&PROXMOX_GROUP_ID_REGEX); + pub const PASSWORD_FORMAT: ApiStringFormat = ApiStringFormat::Pattern(&PASSWORD_REGEX); @@ -218,6 +224,12 @@ pub const PROXMOX_USER_ID_SCHEMA: Schema = StringSchema::new("User ID") .max_length(64) .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 diff --git a/src/bin/proxmox-backup-manager.rs b/src/bin/proxmox-backup-manager.rs index 0229d1ce..d1b91a56 100644 --- a/src/bin/proxmox-backup-manager.rs +++ b/src/bin/proxmox-backup-manager.rs @@ -220,7 +220,15 @@ fn list_acls(param: Value, rpcenv: &mut dyn RpcEnvironment) -> Result CommandLineInterface { 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() } diff --git a/src/config/acl.rs b/src/config/acl.rs index 27b9b51a..f12aa21e 100644 --- a/src/config/acl.rs +++ b/src/config/acl.rs @@ -10,29 +10,31 @@ use proxmox::tools::{fs::replace_file, fs::CreateOptions}; // define Privilege bitfield -pub const PRIV_SYS_AUDIT: u64 = 1 << 0; -pub const PRIV_SYS_MODIFY: u64 = 1 << 1; -pub const PRIV_SYS_POWER_MANAGEMENT: u64 = 1 << 2; +pub const PRIV_SYS_AUDIT: u64 = 1 << 0; +pub const PRIV_SYS_MODIFY: u64 = 1 << 1; +pub const PRIV_SYS_POWER_MANAGEMENT: u64 = 1 << 2; -pub const PRIV_STORE_AUDIT: u64 = 1 << 3; -pub const PRIV_STORE_ALLOCATE: u64 = 1 << 4; -pub const PRIV_STORE_ALLOCATE_SPACE: u64 = 1 << 5; +pub const PRIV_DATASTORE_AUDIT: u64 = 1 << 3; +pub const PRIV_DATASTORE_ALLOCATE: u64 = 1 << 4; +pub const PRIV_DATASTORE_ALLOCATE_SPACE: u64 = 1 << 5; pub const ROLE_ADMIN: u64 = std::u64::MAX; pub const ROLE_NO_ACCESS: u64 = 0; pub const ROLE_AUDIT: u64 = PRIV_SYS_AUDIT | -PRIV_STORE_AUDIT; +PRIV_DATASTORE_AUDIT; -pub const ROLE_STORE_ADMIN: u64 = -PRIV_STORE_AUDIT | -PRIV_STORE_ALLOCATE | -PRIV_STORE_ALLOCATE_SPACE; +pub const ROLE_DATASTORE_ADMIN: u64 = +PRIV_DATASTORE_AUDIT | +PRIV_DATASTORE_ALLOCATE | +PRIV_DATASTORE_ALLOCATE_SPACE; -pub const ROLE_STORE_USER: u64 = -PRIV_STORE_AUDIT | -PRIV_STORE_ALLOCATE_SPACE; +pub const ROLE_DATASTORE_USER: u64 = +PRIV_DATASTORE_AUDIT | +PRIV_DATASTORE_ALLOCATE_SPACE; + +pub const ROLE_DATASTORE_AUDIT: u64 = PRIV_DATASTORE_AUDIT; lazy_static! { static ref ROLE_NAMES: HashMap<&'static str, u64> = { @@ -42,8 +44,9 @@ lazy_static! { map.insert("Audit", ROLE_AUDIT); map.insert("NoAccess", ROLE_NO_ACCESS); - map.insert("Store.Admin", ROLE_STORE_ADMIN); - map.insert("Store.User", ROLE_STORE_USER); + map.insert("Datastore.Admin", ROLE_DATASTORE_ADMIN); + map.insert("Datastore.User", ROLE_DATASTORE_USER); + map.insert("Datastore.Audit", ROLE_DATASTORE_AUDIT); map }; @@ -141,6 +144,22 @@ impl AclTreeNode { 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) { self.groups .entry(group).or_insert_with(|| HashMap::new()) @@ -160,6 +179,17 @@ impl AclTree { 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 { let mut node = &mut self.root; for comp in path { @@ -169,6 +199,24 @@ impl AclTree { 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) { let path = split_acl_path(path); let node = self.get_or_insert_node(&path); @@ -382,7 +430,7 @@ pub fn config() -> Result<(AclTree, [u8; 32]), Error> { 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 = Vec::new(); acl.write_config(&mut raw)?; @@ -396,12 +444,11 @@ pub fn store_config(acl: &AclTree, filename: &Path) -> Result<(), Error> { .owner(nix::unistd::ROOT) .group(backup_user.gid); - replace_file(filename, &raw, options)?; + replace_file(ACL_CFG_FILENAME, &raw, options)?; Ok(()) } - #[cfg(test)] mod test { @@ -430,15 +477,15 @@ mod test { let tree = AclTree::from_raw(r###" acl:0:/store/store2:user1:Admin acl:0:/store/store2:user2:Admin -acl:0:/store/store2:user1:Store.User -acl:0:/store/store2:user2:Store.User +acl:0:/store/store2:user1:Datastore.User +acl:0:/store/store2:user2:Datastore.User "###)?; let mut raw: Vec = Vec::new(); tree.write_config(&mut 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(()) } @@ -448,18 +495,18 @@ acl:0:/store/store2:user2:Store.User let tree = AclTree::from_raw(r###" acl:1:/storage:user1@pbs:Admin -acl:1:/storage/store1:user1@pbs:Store.User -acl:1:/storage/store2:user2@pbs:Store.User +acl:1:/storage/store1:user1@pbs:Datastore.User +acl:1:/storage/store2:user2@pbs:Datastore.User "###)?; check_roles(&tree, "user1@pbs", "/", ""); 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, "user2@pbs", "/", ""); check_roles(&tree, "user2@pbs", "/storage", ""); 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(()) } @@ -470,22 +517,22 @@ acl:1:/storage/store2:user2@pbs:Store.User let tree = AclTree::from_raw(r###" acl:1:/:user1@pbs:Admin 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", "/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", "/system", "Admin"); let tree = AclTree::from_raw(r###" acl:1:/:user1@pbs:Admin 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", "/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", "/system", "Admin"); diff --git a/src/config/datastore.rs b/src/config/datastore.rs index 61505aae..d3dad4db 100644 --- a/src/config/datastore.rs +++ b/src/config/datastore.rs @@ -101,3 +101,19 @@ pub fn complete_datastore_name(_arg: &str, _param: &HashMap) -> Err(_) => return vec![], } } + +pub fn complete_acl_path(_arg: &str, _param: &HashMap) -> Vec { + 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 +}