From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from gate001.proxmox.com (gate001.proxmox.com [IPv6:2a0f:8001:1:32::40]) by lore.proxmox.com (Postfix) with ESMTPS id 994B31FF0E7 for ; Thu, 13 Aug 2026 19:11:07 +0200 (CEST) Received: from gate001.proxmox.com (localhost.localdomain [127.0.0.1]) by gate001.proxmox.com (Proxmox) with ESMTP id 6677821B28; Thu, 13 Aug 2026 19:10:37 +0200 (CEST) From: Christian Ebner To: pbs-devel@lists.proxmox.com Subject: [PATCH proxmox-backup 12/28] api: allow namespace creation on append permissions Date: Thu, 13 Aug 2026 19:09:46 +0200 Message-ID: <20260813171002.809441-13-c.ebner@proxmox.com> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20260813171002.809441-1-c.ebner@proxmox.com> References: <20260813171002.809441-1-c.ebner@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Bm-Milter-Handled: 55990f41-d878-4baa-be0a-ee34c49e34d2 X-Bm-Transport-Timestamp: 1786641015445 X-SPAM-LEVEL: Spam detection results: 0 AWL 0.207 Adjusted score from AWL reputation of From: address DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment (newer systems) RCVD_IN_DNSWL_MED -2.3 Sender listed at https://www.dnswl.org/, medium trust RDNS_NONE 1.274 Delivered to internal network by a host with no rDNS SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record Message-ID-Hash: MCMRX7KQ2ZCX4HNV7AV6AKRF3N6BZFJO X-Message-ID-Hash: MCMRX7KQ2ZCX4HNV7AV6AKRF3N6BZFJO X-MailFrom: c.ebner@proxmox.com X-Mailman-Rule-Misses: dmarc-mitigation; no-senders; approved; loop; banned-address; emergency; member-moderation; nonmember-moderation; administrivia; implicit-dest; max-recipients; max-size; news-moderation; no-subject; digests; suspicious-header X-Mailman-Version: 3.3.10 Precedence: list List-Id: Proxmox Backup Server development discussion List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: Only require full modify permissions for moves and namespace destruction, allow namespace creation also with append permissions. Signed-off-by: Christian Ebner --- src/api2/admin/namespace.rs | 8 ++++---- src/api2/tape/restore.rs | 6 +++--- src/backup/hierarchy.rs | 12 +++++++++++- src/server/pull.rs | 10 +++++----- 4 files changed, 23 insertions(+), 13 deletions(-) diff --git a/src/api2/admin/namespace.rs b/src/api2/admin/namespace.rs index 19e1e8cd0..f874f8129 100644 --- a/src/api2/admin/namespace.rs +++ b/src/api2/admin/namespace.rs @@ -56,7 +56,7 @@ pub fn create_namespace( let mut ns = parent.clone(); ns.push(name.clone())?; - check_ns_modification_privs(&store, &ns, &auth_id)?; + check_ns_modification_privs(&store, &ns, &auth_id, false)?; let lookup = crate::tools::lookup_with(&store, Operation::Write); let datastore = DataStore::lookup_datastore(lookup)?; @@ -166,7 +166,7 @@ pub fn delete_namespace( ) -> Result { let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?; - check_ns_modification_privs(&store, &ns, &auth_id)?; + check_ns_modification_privs(&store, &ns, &auth_id, true)?; let lookup = crate::tools::lookup_with(&store, Operation::Write); let datastore = DataStore::lookup_datastore(lookup)?; @@ -247,8 +247,8 @@ pub fn move_namespace( ) -> Result { let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?; - check_ns_modification_privs(&store, &ns, &auth_id)?; - check_ns_modification_privs(&store, &target_ns, &auth_id)?; + check_ns_modification_privs(&store, &ns, &auth_id, true)?; + check_ns_modification_privs(&store, &target_ns, &auth_id, true)?; let datastore = DataStore::lookup_datastore(crate::tools::lookup_with(&store, Operation::Write))?; diff --git a/src/api2/tape/restore.rs b/src/api2/tape/restore.rs index bb3825aae..df383a810 100644 --- a/src/api2/tape/restore.rs +++ b/src/api2/tape/restore.rs @@ -254,9 +254,9 @@ fn check_and_create_namespaces( for comp in ns.components() { tmp_ns.push(comp.to_string())?; if !store.namespace_exists(&tmp_ns) { - check_ns_modification_privs(store.name(), &tmp_ns, auth_id).map_err(|_err| { - format_err!("no permission to create namespace '{}'", tmp_ns) - })?; + check_ns_modification_privs(store.name(), &tmp_ns, auth_id, false).map_err( + |_err| format_err!("no permission to create namespace '{}'", tmp_ns), + )?; store.create_namespace(&tmp_ns.parent(), comp.to_string())?; } diff --git a/src/backup/hierarchy.rs b/src/backup/hierarchy.rs index d81dc3565..5cc8fd778 100644 --- a/src/backup/hierarchy.rs +++ b/src/backup/hierarchy.rs @@ -20,10 +20,14 @@ pub fn check_ns_privs( } /// Asserts that `privs` for creating/destroying namespace in datastore are fulfilled. +/// +/// If `needs_full_modify_priv` is not set, `Datastore.Append` is sufficient which must +/// allow namespace creation only, never deletion. pub fn check_ns_modification_privs( store: &str, ns: &BackupNamespace, auth_id: &Authid, + needs_full_modify_priv: bool, ) -> Result<(), Error> { // we could allow it as easy purge-whole datastore, but lets be more restrictive for now if ns.is_root() { @@ -33,7 +37,13 @@ pub fn check_ns_modification_privs( let parent = ns.parent(); - check_ns_privs(store, &parent, auth_id, PRIV_DATASTORE_MODIFY) + let required_privs = if needs_full_modify_priv { + PRIV_DATASTORE_MODIFY + } else { + PRIV_DATASTORE_APPEND | PRIV_DATASTORE_MODIFY + }; + + check_ns_privs(store, &parent, auth_id, required_privs) } /// Asserts that either either `full_access_privs` or `partial_access_privs` are fulfilled on diff --git a/src/server/pull.rs b/src/server/pull.rs index 4eb5bcf11..8c6253604 100644 --- a/src/server/pull.rs +++ b/src/server/pull.rs @@ -22,8 +22,8 @@ use tokio::io::AsyncWriteExt; use pbs_api_types::{ ArchiveType, Authid, BackupDir, BackupGroup, BackupNamespace, CLIENT_LOG_BLOB_NAME, CryptMode, Fingerprint, GroupFilter, MANIFEST_BLOB_NAME, MAX_NAMESPACE_DEPTH, Operation, - PRIV_DATASTORE_AUDIT, PRIV_DATASTORE_BACKUP, RateLimitConfig, Remote, SnapshotListItem, - VerifyState, print_store_and_ns, + PRIV_DATASTORE_APPEND, PRIV_DATASTORE_AUDIT, PRIV_DATASTORE_BACKUP, RateLimitConfig, Remote, + SnapshotListItem, VerifyState, print_store_and_ns, }; use pbs_client::BackupRepository; use pbs_config::CachedUserInfo; @@ -1426,7 +1426,7 @@ fn check_and_create_ns(params: &PullParameters, ns: &BackupNamespace) -> Result< let store_ns_str = print_store_and_ns(params.target.store.name(), ns); if !ns.is_root() && !params.target.store.namespace_path(ns).exists() { - check_ns_modification_privs(params.target.store.name(), ns, ¶ms.owner) + check_ns_modification_privs(params.target.store.name(), ns, ¶ms.owner, false) .map_err(|err| format_err!("Creating {ns} not allowed - {err}"))?; let name = match ns.components().last() { @@ -1446,7 +1446,7 @@ fn check_and_create_ns(params: &PullParameters, ns: &BackupNamespace) -> Result< params.target.store.name(), ns, ¶ms.owner, - PRIV_DATASTORE_BACKUP, + PRIV_DATASTORE_BACKUP | PRIV_DATASTORE_APPEND, ) .map_err(|err| format_err!("sync into {store_ns_str} not allowed - {err}"))?; @@ -1454,7 +1454,7 @@ fn check_and_create_ns(params: &PullParameters, ns: &BackupNamespace) -> Result< } fn check_and_remove_ns(params: &PullParameters, local_ns: &BackupNamespace) -> Result { - check_ns_modification_privs(params.target.store.name(), local_ns, ¶ms.owner) + check_ns_modification_privs(params.target.store.name(), local_ns, ¶ms.owner, true) .map_err(|err| format_err!("Removing {local_ns} not allowed - {err}"))?; // The outer loop (check_and_remove_vanished_ns) iterates children first, so we only need -- 2.47.3