From 18934ae56bcd7a46f4cb7bfb720e84914b5cb4d9 Mon Sep 17 00:00:00 2001 From: Thomas Lamprecht Date: Sun, 24 Apr 2022 20:24:42 +0200 Subject: [PATCH] api: namespace management endpoints allow to list any namespace with privileges on it and allow to create and delete namespaces if the user has modify permissions on the parent namespace. Creation is only allowed if the parent NS already exists. Signed-off-by: Thomas Lamprecht --- pbs-api-types/src/datastore.rs | 25 +++++ pbs-config/src/acl.rs | 3 + src/api2/admin/datastore.rs | 5 + src/api2/admin/mod.rs | 1 + src/api2/admin/namespace.rs | 178 +++++++++++++++++++++++++++++++++ 5 files changed, 212 insertions(+) create mode 100644 src/api2/admin/namespace.rs diff --git a/pbs-api-types/src/datastore.rs b/pbs-api-types/src/datastore.rs index 010fcc6e..af60d435 100644 --- a/pbs-api-types/src/datastore.rs +++ b/pbs-api-types/src/datastore.rs @@ -1213,6 +1213,22 @@ pub struct GroupListItem { pub comment: Option, } +#[api()] +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] +/// Basic information about a backup namespace. +pub struct NamespaceListItem { + /// A backup namespace + pub ns: BackupNamespace, + + // TODO? + //pub group_count: u64, + //pub ns_count: u64, + /// The first line from the namespace's "notes" + #[serde(skip_serializing_if = "Option::is_none")] + pub comment: Option, +} + #[api( properties: { "backup": { type: BackupDir }, @@ -1431,6 +1447,15 @@ pub const ADMIN_DATASTORE_LIST_GROUPS_RETURN_TYPE: ReturnType = ReturnType { .schema(), }; +pub const ADMIN_DATASTORE_LIST_NAMESPACE_RETURN_TYPE: ReturnType = ReturnType { + optional: false, + schema: &ArraySchema::new( + "Returns the list of backup namespaces.", + &NamespaceListItem::API_SCHEMA, + ) + .schema(), +}; + pub const ADMIN_DATASTORE_PRUNE_RETURN_TYPE: ReturnType = ReturnType { optional: false, schema: &ArraySchema::new( diff --git a/pbs-config/src/acl.rs b/pbs-config/src/acl.rs index 25e81926..3362612d 100644 --- a/pbs-config/src/acl.rs +++ b/pbs-config/src/acl.rs @@ -85,6 +85,9 @@ pub fn check_acl_path(path: &str) -> Result<(), Error> { if components_len <= 2 { return Ok(()); } + if components_len > 2 && components_len <= 2 + pbs_api_types::MAX_NAMESPACE_DEPTH { + return Ok(()); + } } "remote" => { // /remote/{remote}/{store} diff --git a/src/api2/admin/datastore.rs b/src/api2/admin/datastore.rs index 49fa01e8..206d0604 100644 --- a/src/api2/admin/datastore.rs +++ b/src/api2/admin/datastore.rs @@ -1997,6 +1997,11 @@ const DATASTORE_INFO_SUBDIRS: SubdirMap = &[ .get(&API_METHOD_LIST_GROUPS) .delete(&API_METHOD_DELETE_GROUP), ), + ( + "namespace", + // FIXME: move into datastore:: sub-module?! + &crate::api2::admin::namespace::ROUTER, + ), ( "notes", &Router::new() diff --git a/src/api2/admin/mod.rs b/src/api2/admin/mod.rs index 43973af5..bbe88f6a 100644 --- a/src/api2/admin/mod.rs +++ b/src/api2/admin/mod.rs @@ -4,6 +4,7 @@ use proxmox_router::list_subdirs_api_method; use proxmox_router::{Router, SubdirMap}; pub mod datastore; +pub mod namespace; pub mod sync; pub mod traffic_control; pub mod verify; diff --git a/src/api2/admin/namespace.rs b/src/api2/admin/namespace.rs new file mode 100644 index 00000000..626a5cd5 --- /dev/null +++ b/src/api2/admin/namespace.rs @@ -0,0 +1,178 @@ +use anyhow::{bail, Error}; +use serde_json::Value; + +use pbs_config::CachedUserInfo; +use proxmox_router::{http_bail, ApiMethod, Permission, Router, RpcEnvironment}; +use proxmox_schema::*; + +use pbs_api_types::{ + Authid, BackupNamespace, NamespaceListItem, Operation, DATASTORE_SCHEMA, NS_MAX_DEPTH_SCHEMA, + PRIV_DATASTORE_AUDIT, PRIV_DATASTORE_BACKUP, PRIV_DATASTORE_MODIFY, PROXMOX_SAFE_ID_FORMAT, +}; + +use pbs_datastore::DataStore; + +#[api( + input: { + properties: { + store: { + schema: DATASTORE_SCHEMA, + }, + name: { + type: String, + description: "The name of the new namespace to add at the parent.", + format: &PROXMOX_SAFE_ID_FORMAT, + min_length: 1, + max_length: 32, + }, + parent: { + type: BackupNamespace, + //description: "To list only namespaces below the passed one.", + optional: true, + }, + }, + }, + returns: pbs_api_types::ADMIN_DATASTORE_LIST_NAMESPACE_RETURN_TYPE, + access: { + permission: &Permission::Or(&[ + &Permission::Privilege( + &["datastore", "{store}"], + PRIV_DATASTORE_MODIFY, + true, + ), + &Permission::Privilege( + &["datastore", "{store}", "{parent}"], + PRIV_DATASTORE_MODIFY, + true, + ), + ]) + }, +)] +/// List the namespaces of a datastore. +pub fn create_namespace( + store: String, + name: String, + parent: Option, + _rpcenv: &mut dyn RpcEnvironment, +) -> Result { + let parent = parent.unwrap_or_default(); + + let datastore = DataStore::lookup_datastore(&store, Some(Operation::Read))?; + + datastore.create_namespace(&parent, name) +} + +#[api( + input: { + properties: { + store: { + schema: DATASTORE_SCHEMA, + }, + parent: { + type: BackupNamespace, + // FIXME: fix the api macro stuff to finally allow that ... -.- + //description: "To list only namespaces below the passed one.", + optional: true, + }, + "max-depth": { + schema: NS_MAX_DEPTH_SCHEMA, + optional: true, + }, + }, + }, + returns: pbs_api_types::ADMIN_DATASTORE_LIST_NAMESPACE_RETURN_TYPE, + access: { + permission: &Permission::Or(&[ + &Permission::Privilege( + &["datastore", "{store}"], + PRIV_DATASTORE_BACKUP | PRIV_DATASTORE_AUDIT, + true, + ), + &Permission::Privilege( + &["datastore", "{store}", "{parent}"], + PRIV_DATASTORE_BACKUP | PRIV_DATASTORE_AUDIT, + true, + ), + ]) + }, +)] +/// List the namespaces of a datastore. +pub fn list_namespaces( + store: String, + parent: Option, + max_depth: Option, + rpcenv: &mut dyn RpcEnvironment, +) -> Result, Error> { + let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?; + let user_info = CachedUserInfo::new()?; + + let datastore = DataStore::lookup_datastore(&store, Some(Operation::Read))?; + let parent = parent.unwrap_or_default(); + + let ns_to_item = + |ns: BackupNamespace| -> NamespaceListItem { NamespaceListItem { ns, comment: None } }; + + Ok(datastore + .recursive_iter_backup_ns_ok(parent, max_depth)? + .filter(|ns| { + if ns.is_root() { + return true; // already covered by access permission above + } + let privs = user_info.lookup_privs(&auth_id, &["datastore", &store, &ns.to_string()]); + privs & (PRIV_DATASTORE_BACKUP | PRIV_DATASTORE_AUDIT) != 0 + }) + .map(ns_to_item) + .collect()) +} + +#[api( + input: { + properties: { + store: { schema: DATASTORE_SCHEMA }, + ns: { + type: BackupNamespace, + }, + }, + }, + access: { + permission: &Permission::Anybody, + }, +)] +/// Delete a backup namespace including all snapshots. +pub fn delete_namespace( + store: String, + ns: BackupNamespace, + _info: &ApiMethod, + rpcenv: &mut dyn RpcEnvironment, +) -> Result { + let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?; + let user_info = CachedUserInfo::new()?; + + // we could allow it as easy purge-whole datastore, but lets be more restrictive for now + if ns.is_root() { + bail!("cannot delete root namespace!"); + }; + + let parent = ns.parent(); // must have MODIFY permission on parent to allow deletion + let user_privs = if parent.is_root() { + user_info.lookup_privs(&auth_id, &["datastore", &store]) + } else { + user_info.lookup_privs(&auth_id, &["datastore", &store, &parent.to_string()]) + }; + if (user_privs & PRIV_DATASTORE_MODIFY) == 0 { + http_bail!(FORBIDDEN, "permission check failed"); + } + + let datastore = DataStore::lookup_datastore(&store, Some(Operation::Write))?; + + if !datastore.remove_namespace_recursive(&ns)? { + bail!("group only partially deleted due to protected snapshots"); + } + + Ok(Value::Null) +} + +pub const ROUTER: Router = Router::new() + .get(&API_METHOD_LIST_NAMESPACES) + .post(&API_METHOD_CREATE_NAMESPACE) + .delete(&API_METHOD_DELETE_NAMESPACE);