acl api: implement update
This commit is contained in:
		| @ -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<AclListItem> = 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<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() | ||||
|     .get(&API_METHOD_READ_ACL); | ||||
|     .get(&API_METHOD_READ_ACL) | ||||
|     .put(&API_METHOD_UPDATE_ACL); | ||||
|  | ||||
| @ -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 | ||||
|  | ||||
|  | ||||
| @ -220,7 +220,15 @@ fn list_acls(param: Value, rpcenv: &mut dyn RpcEnvironment) -> Result<Value, Err | ||||
| fn acl_commands() -> 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() | ||||
| } | ||||
|  | ||||
| @ -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<u8> = 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<u8> = 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"); | ||||
|  | ||||
|  | ||||
| @ -101,3 +101,19 @@ pub fn complete_datastore_name(_arg: &str, _param: &HashMap<String, String>) -> | ||||
|         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 | ||||
| } | ||||
|  | ||||
		Reference in New Issue
	
	Block a user