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 <t.lamprecht@proxmox.com>
This commit is contained in:
Thomas Lamprecht 2022-04-24 20:24:42 +02:00
parent 15a9272495
commit 18934ae56b
5 changed files with 212 additions and 0 deletions

View File

@ -1213,6 +1213,22 @@ pub struct GroupListItem {
pub comment: Option<String>, pub comment: Option<String>,
} }
#[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<String>,
}
#[api( #[api(
properties: { properties: {
"backup": { type: BackupDir }, "backup": { type: BackupDir },
@ -1431,6 +1447,15 @@ pub const ADMIN_DATASTORE_LIST_GROUPS_RETURN_TYPE: ReturnType = ReturnType {
.schema(), .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 { pub const ADMIN_DATASTORE_PRUNE_RETURN_TYPE: ReturnType = ReturnType {
optional: false, optional: false,
schema: &ArraySchema::new( schema: &ArraySchema::new(

View File

@ -85,6 +85,9 @@ pub fn check_acl_path(path: &str) -> Result<(), Error> {
if components_len <= 2 { if components_len <= 2 {
return Ok(()); return Ok(());
} }
if components_len > 2 && components_len <= 2 + pbs_api_types::MAX_NAMESPACE_DEPTH {
return Ok(());
}
} }
"remote" => { "remote" => {
// /remote/{remote}/{store} // /remote/{remote}/{store}

View File

@ -1997,6 +1997,11 @@ const DATASTORE_INFO_SUBDIRS: SubdirMap = &[
.get(&API_METHOD_LIST_GROUPS) .get(&API_METHOD_LIST_GROUPS)
.delete(&API_METHOD_DELETE_GROUP), .delete(&API_METHOD_DELETE_GROUP),
), ),
(
"namespace",
// FIXME: move into datastore:: sub-module?!
&crate::api2::admin::namespace::ROUTER,
),
( (
"notes", "notes",
&Router::new() &Router::new()

View File

@ -4,6 +4,7 @@ use proxmox_router::list_subdirs_api_method;
use proxmox_router::{Router, SubdirMap}; use proxmox_router::{Router, SubdirMap};
pub mod datastore; pub mod datastore;
pub mod namespace;
pub mod sync; pub mod sync;
pub mod traffic_control; pub mod traffic_control;
pub mod verify; pub mod verify;

178
src/api2/admin/namespace.rs Normal file
View File

@ -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<BackupNamespace>,
_rpcenv: &mut dyn RpcEnvironment,
) -> Result<BackupNamespace, Error> {
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<BackupNamespace>,
max_depth: Option<usize>,
rpcenv: &mut dyn RpcEnvironment,
) -> Result<Vec<NamespaceListItem>, 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<Value, Error> {
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);