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);