all lists on lists.proxmox.com
 help / color / mirror / Atom feed
From: "Fabian Grünbichler" <f.gruenbichler@proxmox.com>
To: pbs-devel@lists.proxmox.com
Subject: [pbs-devel] [RFC proxmox-backup 12/15] owner checks: handle backups owned by API tokens
Date: Mon, 19 Oct 2020 09:39:16 +0200	[thread overview]
Message-ID: <20201019073919.588521-13-f.gruenbichler@proxmox.com> (raw)
In-Reply-To: <20201019073919.588521-1-f.gruenbichler@proxmox.com>

a user should be allowed to read/list/overwrite backups owned by their
own tokens, but a token should not be able to read/list/overwrite
backups owned by their owning user.

when changing ownership of a backup group, a user should be able to
transfer ownership to/from their own tokens if the backup is owned by
them (or one of their tokens).

Signed-off-by: Fabian Grünbichler <f.gruenbichler@proxmox.com>
---
 src/api2/admin/datastore.rs | 124 +++++++++++++++++++++++-------------
 src/api2/backup.rs          |   3 +-
 src/api2/reader.rs          |   3 +-
 3 files changed, 82 insertions(+), 48 deletions(-)

diff --git a/src/api2/admin/datastore.rs b/src/api2/admin/datastore.rs
index 5c9902e1..a279d382 100644
--- a/src/api2/admin/datastore.rs
+++ b/src/api2/admin/datastore.rs
@@ -37,13 +37,29 @@ use crate::config::acl::{
     PRIV_DATASTORE_BACKUP,
 };
 
-fn check_backup_owner(
+fn check_priv_or_backup_owner(
     store: &DataStore,
     group: &BackupGroup,
     userid: &Userid,
+    required_privs: u64,
 ) -> Result<(), Error> {
-    let owner = store.get_owner(group)?;
-    if &owner != userid {
+    let user_info = CachedUserInfo::new()?;
+    let user_privs = user_info.lookup_privs(&userid, &["datastore", store.name()]);
+
+    if user_privs & required_privs == 0 {
+        let owner = store.get_owner(group)?;
+        check_backup_owner(&owner, userid)?;
+    }
+
+    Ok(())
+}
+
+fn check_backup_owner(
+    owner: &Userid,
+    userid: &Userid,
+) -> Result<(), Error> {
+    let correct_owner = owner == userid || (owner.is_tokenid() && &owner.owner()? == userid);
+    if !correct_owner {
         bail!("backup owner check failed ({} != {})", userid, owner);
     }
     Ok(())
@@ -164,7 +180,7 @@ fn list_groups(
 
         let list_all = (user_privs & PRIV_DATASTORE_AUDIT) != 0;
         let owner = datastore.get_owner(group)?;
-        if !list_all && owner != userid {
+        if !list_all && check_backup_owner(&owner, &userid).is_err() {
             continue;
         }
 
@@ -224,15 +240,11 @@ pub fn list_snapshot_files(
 ) -> Result<Vec<BackupContent>, Error> {
 
     let userid: Userid = rpcenv.get_user().unwrap().parse()?;
-    let user_info = CachedUserInfo::new()?;
-    let user_privs = user_info.lookup_privs(&userid, &["datastore", &store]);
-
     let datastore = DataStore::lookup_datastore(&store)?;
 
     let snapshot = BackupDir::new(backup_type, backup_id, backup_time)?;
 
-    let allowed = (user_privs & (PRIV_DATASTORE_AUDIT | PRIV_DATASTORE_READ)) != 0;
-    if !allowed { check_backup_owner(&datastore, snapshot.group(), &userid)?; }
+    check_priv_or_backup_owner(&datastore, snapshot.group(), &userid, PRIV_DATASTORE_AUDIT | PRIV_DATASTORE_READ)?;
 
     let info = BackupInfo::new(&datastore.base_path(), snapshot)?;
 
@@ -276,15 +288,11 @@ fn delete_snapshot(
 ) -> Result<Value, Error> {
 
     let userid: Userid = rpcenv.get_user().unwrap().parse()?;
-    let user_info = CachedUserInfo::new()?;
-    let user_privs = user_info.lookup_privs(&userid, &["datastore", &store]);
 
     let snapshot = BackupDir::new(backup_type, backup_id, backup_time)?;
-
     let datastore = DataStore::lookup_datastore(&store)?;
 
-    let allowed = (user_privs & PRIV_DATASTORE_MODIFY) != 0;
-    if !allowed { check_backup_owner(&datastore, snapshot.group(), &userid)?; }
+    check_priv_or_backup_owner(&datastore, snapshot.group(), &userid, PRIV_DATASTORE_MODIFY)?;
 
     datastore.remove_backup_dir(&snapshot, false)?;
 
@@ -355,7 +363,7 @@ pub fn list_snapshots (
         let list_all = (user_privs & PRIV_DATASTORE_AUDIT) != 0;
         let owner = datastore.get_owner(group)?;
 
-        if !list_all && owner != userid {
+        if !list_all && check_backup_owner(&owner, &userid).is_err() {
             continue;
         }
 
@@ -637,8 +645,6 @@ fn prune(
     let backup_id = tools::required_string_param(&param, "backup-id")?;
 
     let userid: Userid = rpcenv.get_user().unwrap().parse()?;
-    let user_info = CachedUserInfo::new()?;
-    let user_privs = user_info.lookup_privs(&userid, &["datastore", &store]);
 
     let dry_run = param["dry-run"].as_bool().unwrap_or(false);
 
@@ -646,8 +652,7 @@ fn prune(
 
     let datastore = DataStore::lookup_datastore(&store)?;
 
-    let allowed = (user_privs & PRIV_DATASTORE_MODIFY) != 0;
-    if !allowed { check_backup_owner(&datastore, &group, &userid)?; }
+    check_priv_or_backup_owner(&datastore, &group, &userid, PRIV_DATASTORE_MODIFY)?;
 
     let prune_options = PruneOptions {
         keep_last: param["keep-last"].as_u64(),
@@ -894,8 +899,6 @@ fn download_file(
         let datastore = DataStore::lookup_datastore(store)?;
 
         let userid: Userid = rpcenv.get_user().unwrap().parse()?;
-        let user_info = CachedUserInfo::new()?;
-        let user_privs = user_info.lookup_privs(&userid, &["datastore", &store]);
 
         let file_name = tools::required_string_param(&param, "file-name")?.to_owned();
 
@@ -905,8 +908,7 @@ fn download_file(
 
         let backup_dir = BackupDir::new(backup_type, backup_id, backup_time)?;
 
-        let allowed = (user_privs & PRIV_DATASTORE_READ) != 0;
-        if !allowed { check_backup_owner(&datastore, backup_dir.group(), &userid)?; }
+        check_priv_or_backup_owner(&datastore, backup_dir.group(), &userid, PRIV_DATASTORE_READ)?;
 
         println!("Download {} from {} ({}/{})", file_name, store, backup_dir, file_name);
 
@@ -967,8 +969,6 @@ fn download_file_decoded(
         let datastore = DataStore::lookup_datastore(store)?;
 
         let userid: Userid = rpcenv.get_user().unwrap().parse()?;
-        let user_info = CachedUserInfo::new()?;
-        let user_privs = user_info.lookup_privs(&userid, &["datastore", &store]);
 
         let file_name = tools::required_string_param(&param, "file-name")?.to_owned();
 
@@ -978,8 +978,7 @@ fn download_file_decoded(
 
         let backup_dir = BackupDir::new(backup_type, backup_id, backup_time)?;
 
-        let allowed = (user_privs & PRIV_DATASTORE_READ) != 0;
-        if !allowed { check_backup_owner(&datastore, backup_dir.group(), &userid)?; }
+        check_priv_or_backup_owner(&datastore, backup_dir.group(), &userid, PRIV_DATASTORE_READ)?;
 
         let (manifest, files) = read_backup_index(&datastore, &backup_dir)?;
         for file in files {
@@ -1092,7 +1091,8 @@ fn upload_backup_log(
         let backup_dir = BackupDir::new(backup_type, backup_id, backup_time)?;
 
         let userid: Userid = rpcenv.get_user().unwrap().parse()?;
-        check_backup_owner(&datastore, backup_dir.group(), &userid)?;
+        let owner = datastore.get_owner(backup_dir.group())?;
+        check_backup_owner(&owner, &userid)?;
 
         let mut path = datastore.base_path();
         path.push(backup_dir.relative_path());
@@ -1162,13 +1162,10 @@ fn catalog(
     let datastore = DataStore::lookup_datastore(&store)?;
 
     let userid: Userid = rpcenv.get_user().unwrap().parse()?;
-    let user_info = CachedUserInfo::new()?;
-    let user_privs = user_info.lookup_privs(&userid, &["datastore", &store]);
 
     let backup_dir = BackupDir::new(backup_type, backup_id, backup_time)?;
 
-    let allowed = (user_privs & PRIV_DATASTORE_READ) != 0;
-    if !allowed { check_backup_owner(&datastore, backup_dir.group(), &userid)?; }
+    check_priv_or_backup_owner(&datastore, backup_dir.group(), &userid, PRIV_DATASTORE_READ)?;
 
     let file_name = CATALOG_NAME;
 
@@ -1273,8 +1270,6 @@ fn pxar_file_download(
         let datastore = DataStore::lookup_datastore(&store)?;
 
         let userid: Userid = rpcenv.get_user().unwrap().parse()?;
-        let user_info = CachedUserInfo::new()?;
-        let user_privs = user_info.lookup_privs(&userid, &["datastore", &store]);
 
         let filepath = tools::required_string_param(&param, "filepath")?.to_owned();
 
@@ -1284,8 +1279,7 @@ fn pxar_file_download(
 
         let backup_dir = BackupDir::new(backup_type, backup_id, backup_time)?;
 
-        let allowed = (user_privs & PRIV_DATASTORE_READ) != 0;
-        if !allowed { check_backup_owner(&datastore, backup_dir.group(), &userid)?; }
+        check_priv_or_backup_owner(&datastore, backup_dir.group(), &userid, PRIV_DATASTORE_READ)?;
 
         let mut components = base64::decode(&filepath)?;
         if components.len() > 0 && components[0] == '/' as u8 {
@@ -1420,13 +1414,10 @@ fn get_notes(
     let datastore = DataStore::lookup_datastore(&store)?;
 
     let userid: Userid = rpcenv.get_user().unwrap().parse()?;
-    let user_info = CachedUserInfo::new()?;
-    let user_privs = user_info.lookup_privs(&userid, &["datastore", &store]);
 
     let backup_dir = BackupDir::new(backup_type, backup_id, backup_time)?;
 
-    let allowed = (user_privs & PRIV_DATASTORE_READ) != 0;
-    if !allowed { check_backup_owner(&datastore, backup_dir.group(), &userid)?; }
+    check_priv_or_backup_owner(&datastore, backup_dir.group(), &userid, PRIV_DATASTORE_READ)?;
 
     let (manifest, _) = datastore.load_manifest(&backup_dir)?;
 
@@ -1473,13 +1464,10 @@ fn set_notes(
     let datastore = DataStore::lookup_datastore(&store)?;
 
     let userid: Userid = rpcenv.get_user().unwrap().parse()?;
-    let user_info = CachedUserInfo::new()?;
-    let user_privs = user_info.lookup_privs(&userid, &["datastore", &store]);
 
     let backup_dir = BackupDir::new(backup_type, backup_id, backup_time)?;
 
-    let allowed = (user_privs & PRIV_DATASTORE_READ) != 0;
-    if !allowed { check_backup_owner(&datastore, backup_dir.group(), &userid)?; }
+    check_priv_or_backup_owner(&datastore, backup_dir.group(), &userid, PRIV_DATASTORE_READ)?;
 
     datastore.update_manifest(&backup_dir,|manifest| {
         manifest.unprotected["notes"] = notes.into();
@@ -1506,7 +1494,8 @@ fn set_notes(
         },
     },
     access: {
-        permission: &Permission::Privilege(&["datastore", "{store}"], PRIV_DATASTORE_MODIFY, true),
+        permission: &Permission::Anybody,
+        description: "Datastore.Modify on whole datastore, or changing ownership between user and a user's token for owned backups with Datastore.Backup"
     },
 )]
 /// Change owner of a backup group
@@ -1515,15 +1504,58 @@ fn set_backup_owner(
     backup_type: String,
     backup_id: String,
     new_owner: Userid,
-    _rpcenv: &mut dyn RpcEnvironment,
+    rpcenv: &mut dyn RpcEnvironment,
 ) -> Result<(), Error> {
 
     let datastore = DataStore::lookup_datastore(&store)?;
 
     let backup_group = BackupGroup::new(backup_type, backup_id);
 
+    let userid:Userid = rpcenv.get_user().unwrap().parse()?;
+
     let user_info = CachedUserInfo::new()?;
 
+    let user_privs = user_info.lookup_privs(&userid, &["datastore", &store]);
+
+    let allowed = if (user_privs & PRIV_DATASTORE_MODIFY) != 0 {
+        // High-privilege user/token
+        true
+    } else if (user_privs & PRIV_DATASTORE_BACKUP) != 0 {
+        let owner = datastore.get_owner(&backup_group)?;
+
+        match (owner.is_tokenid(), new_owner.is_tokenid()) {
+            (true, true) => {
+                // API token to API token, owned by same user
+                let owner = owner.owner().unwrap();
+                let new_owner = new_owner.owner().unwrap();
+                owner == new_owner && owner == userid
+            },
+            (true, false) => {
+                // API token to API token owner
+                owner.owner().unwrap() == userid && new_owner == userid
+            },
+            (false, true) => {
+                // API token owner to API token
+                owner == userid && new_owner.owner().unwrap() == userid
+            },
+            (false, false) => {
+                // User to User, not allowed for unprivileged users
+                false
+            },
+        }
+    } else {
+        false
+    };
+
+    if !allowed {
+        return Err(http_err!(UNAUTHORIZED,
+                  "{} does not have permission to change owner of backup group '{}' to {}",
+                  userid,
+                  backup_group,
+                  new_owner,
+        ));
+    }
+
     if !user_info.is_active_user(&new_owner) {
         bail!("user '{}' is inactive or non-existent", new_owner);
     }
diff --git a/src/api2/backup.rs b/src/api2/backup.rs
index 8b2b0e1a..17da0558 100644
--- a/src/api2/backup.rs
+++ b/src/api2/backup.rs
@@ -108,7 +108,8 @@ async move {
     let (owner, _group_guard) = datastore.create_locked_backup_group(&backup_group, &userid)?;
 
     // permission check
-    if owner != userid && worker_type != "benchmark" {
+    let correct_owner = owner == userid || (owner.is_tokenid() && owner.owner()? == userid);
+    if !correct_owner && worker_type != "benchmark" {
         // only the owner is allowed to create additional snapshots
         bail!("backup owner check failed ({} != {})", userid, owner);
     }
diff --git a/src/api2/reader.rs b/src/api2/reader.rs
index 8f47047e..7ea066c4 100644
--- a/src/api2/reader.rs
+++ b/src/api2/reader.rs
@@ -94,7 +94,8 @@ fn upgrade_to_backup_reader_protocol(
         let backup_dir = BackupDir::new(backup_type, backup_id, backup_time)?;
         if !priv_read {
             let owner = datastore.get_owner(backup_dir.group())?;
-            if owner != userid {
+            let correct_owner = owner == userid || (owner.is_tokenid() && owner.owner()? == userid);
+            if !correct_owner {
                 bail!("backup owner check failed!");
             }
         }
-- 
2.20.1





  parent reply	other threads:[~2020-10-19  7:40 UTC|newest]

Thread overview: 23+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2020-10-19  7:39 [pbs-devel] [RFC proxmox-backup 00/15] " Fabian Grünbichler
2020-10-19  7:39 ` [pbs-devel] [PATCH proxmox-backup 01/15] fix indentation Fabian Grünbichler
2020-10-19 12:00   ` [pbs-devel] applied: " Thomas Lamprecht
2020-10-19  7:39 ` [pbs-devel] [PATCH proxmox-backup 02/15] fix typos Fabian Grünbichler
2020-10-19 12:01   ` [pbs-devel] applied: " Thomas Lamprecht
2020-10-19  7:39 ` [pbs-devel] [PATCH proxmox-backup 03/15] REST: rename token to csrf_token Fabian Grünbichler
2020-10-19 12:02   ` [pbs-devel] applied: " Thomas Lamprecht
2020-10-19  7:39 ` [pbs-devel] [RFC proxmox-backup 04/15] Userid: extend schema with token name Fabian Grünbichler
2020-10-19  7:39 ` [pbs-devel] [RFC proxmox-backup 05/15] add ApiToken to user.cfg and CachedUserInfo Fabian Grünbichler
2020-10-19  7:39 ` [pbs-devel] [RFC proxmox-backup 06/15] config: add token.shadow file Fabian Grünbichler
2020-10-19  7:39 ` [pbs-devel] [RFC proxmox-backup 07/15] REST: extract and handle API tokens Fabian Grünbichler
2020-10-20  8:34   ` Wolfgang Bumiller
2020-10-19  7:39 ` [pbs-devel] [RFC proxmox-backup 08/15] api: add API token endpoints Fabian Grünbichler
2020-10-20  9:42   ` Wolfgang Bumiller
2020-10-20 10:15     ` Wolfgang Bumiller
2020-10-19  7:39 ` [pbs-devel] [RFC proxmox-backup 09/15] api: allow listing users + tokens Fabian Grünbichler
2020-10-20 10:10   ` Wolfgang Bumiller
2020-10-19  7:39 ` [pbs-devel] [RFC proxmox-backup 10/15] api: add permissions endpoint Fabian Grünbichler
2020-10-19  7:39 ` [pbs-devel] [RFC proxmox-backup 11/15] client: allow using ApiToken + secret Fabian Grünbichler
2020-10-19  7:39 ` Fabian Grünbichler [this message]
2020-10-19  7:39 ` [pbs-devel] [RFC proxmox-backup 13/15] tasks: allow unpriv users to read their tokens' tasks Fabian Grünbichler
2020-10-19  7:39 ` [pbs-devel] [RFC proxmox-backup 14/15] manager: add token commands Fabian Grünbichler
2020-10-19  7:39 ` [pbs-devel] [RFC proxmox-backup 15/15] manager: add user permissions command Fabian Grünbichler

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20201019073919.588521-13-f.gruenbichler@proxmox.com \
    --to=f.gruenbichler@proxmox.com \
    --cc=pbs-devel@lists.proxmox.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal