diff --git a/src/api2/admin/datastore.rs b/src/api2/admin/datastore.rs index ddf35564..5aa9834f 100644 --- a/src/api2/admin/datastore.rs +++ b/src/api2/admin/datastore.rs @@ -10,7 +10,7 @@ use serde_json::{json, Value}; use proxmox::api::{ api, ApiResponseFuture, ApiHandler, ApiMethod, Router, - RpcEnvironment, RpcEnvironmentType, Permission}; + RpcEnvironment, RpcEnvironmentType, Permission, UserInformation}; use proxmox::api::router::SubdirMap; use proxmox::api::schema::*; use proxmox::tools::fs::{file_get_contents, replace_file, CreateOptions}; @@ -20,15 +20,26 @@ use proxmox::{http_err, identity, list_subdirs_api_method, sortable}; use crate::api2::types::*; use crate::backup::*; use crate::config::datastore; +use crate::config::cached_user_info::CachedUserInfo; + use crate::server::WorkerTask; use crate::tools; use crate::config::acl::{ PRIV_DATASTORE_AUDIT, + PRIV_DATASTORE_MODIFY, PRIV_DATASTORE_READ, PRIV_DATASTORE_PRUNE, - PRIV_DATASTORE_CREATE_BACKUP, + PRIV_DATASTORE_BACKUP, }; +fn check_backup_owner(store: &DataStore, group: &BackupGroup, userid: &str) -> Result<(), Error> { + let owner = store.get_owner(group)?; + if &owner != userid { + bail!("backup owner check failed ({} != {})", userid, owner); + } + Ok(()) +} + fn read_backup_index(store: &DataStore, backup_dir: &BackupDir) -> Result, Error> { let mut path = store.base_path(); @@ -86,14 +97,22 @@ fn group_backups(backup_list: Vec) -> HashMap Result, Error> { + let username = rpcenv.get_user().unwrap(); + let user_info = CachedUserInfo::new()?; + let user_privs = user_info.lookup_privs(&username, &["datastore", &store]); + let datastore = DataStore::lookup_datastore(&store)?; let backup_list = BackupInfo::list_backups(&datastore.base_path())?; @@ -107,8 +126,15 @@ fn list_groups( BackupInfo::sort_list(&mut list, false); let info = &list[0]; + let group = info.backup_dir.group(); + let list_all = (user_privs & PRIV_DATASTORE_AUDIT) != 0; + if !list_all { + let owner = datastore.get_owner(group)?; + if owner != username { continue; } + } + let result_item = GroupListItem { backup_type: group.backup_type().to_string(), backup_id: group.backup_id().to_string(), @@ -147,7 +173,10 @@ fn list_groups( } }, access: { - permission: &Permission::Privilege(&["datastore", "{store}"], PRIV_DATASTORE_AUDIT, false), + permission: &Permission::Privilege( + &["datastore", "{store}"], + PRIV_DATASTORE_AUDIT | PRIV_DATASTORE_READ | PRIV_DATASTORE_BACKUP, + true), }, )] /// List snapshot files. @@ -157,12 +186,20 @@ pub fn list_snapshot_files( backup_id: String, backup_time: i64, _info: &ApiMethod, - _rpcenv: &mut dyn RpcEnvironment, + rpcenv: &mut dyn RpcEnvironment, ) -> Result, Error> { + let username = rpcenv.get_user().unwrap(); + let user_info = CachedUserInfo::new()?; + let user_privs = user_info.lookup_privs(&username, &["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(), &username)?; } + let mut files = read_backup_index(&datastore, &snapshot)?; let info = BackupInfo::new(&datastore.base_path(), snapshot)?; @@ -198,7 +235,10 @@ pub fn list_snapshot_files( }, }, access: { - permission: &Permission::Privilege(&["datastore", "{store}"], PRIV_DATASTORE_PRUNE, false), + permission: &Permission::Privilege( + &["datastore", "{store}"], + PRIV_DATASTORE_MODIFY| PRIV_DATASTORE_PRUNE, + true), }, )] /// Delete backup snapshot. @@ -208,13 +248,20 @@ fn delete_snapshot( backup_id: String, backup_time: i64, _info: &ApiMethod, - _rpcenv: &mut dyn RpcEnvironment, + rpcenv: &mut dyn RpcEnvironment, ) -> Result { + let username = rpcenv.get_user().unwrap(); + let user_info = CachedUserInfo::new()?; + let user_privs = user_info.lookup_privs(&username, &["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(), &username)?; } + datastore.remove_backup_dir(&snapshot)?; Ok(Value::Null) @@ -244,21 +291,27 @@ fn delete_snapshot( } }, access: { - permission: &Permission::Privilege(&["datastore", "{store}"], PRIV_DATASTORE_AUDIT, false), + permission: &Permission::Privilege( + &["datastore", "{store}"], + PRIV_DATASTORE_AUDIT | PRIV_DATASTORE_BACKUP, + true), }, )] /// List backup snapshots. pub fn list_snapshots ( - param: Value, + store: String, + backup_type: Option, + backup_id: Option, + _param: Value, _info: &ApiMethod, - _rpcenv: &mut dyn RpcEnvironment, + rpcenv: &mut dyn RpcEnvironment, ) -> Result, Error> { - let store = tools::required_string_param(¶m, "store")?; - let backup_type = param["backup-type"].as_str(); - let backup_id = param["backup-id"].as_str(); + let username = rpcenv.get_user().unwrap(); + let user_info = CachedUserInfo::new()?; + let user_privs = user_info.lookup_privs(&username, &["datastore", &store]); - let datastore = DataStore::lookup_datastore(store)?; + let datastore = DataStore::lookup_datastore(&store)?; let base_path = datastore.base_path(); @@ -268,13 +321,19 @@ pub fn list_snapshots ( for info in backup_list { let group = info.backup_dir.group(); - if let Some(backup_type) = backup_type { + if let Some(ref backup_type) = backup_type { if backup_type != group.backup_type() { continue; } } - if let Some(backup_id) = backup_id { + if let Some(ref backup_id) = backup_id { if backup_id != group.backup_id() { continue; } } + let list_all = (user_privs & PRIV_DATASTORE_AUDIT) != 0; + if !list_all { + let owner = datastore.get_owner(group)?; + if owner != username { continue; } + } + let mut result_item = SnapshotListItem { backup_type: group.backup_type().to_string(), backup_id: group.backup_id().to_string(), @@ -311,7 +370,7 @@ pub fn list_snapshots ( type: StorageStatus, }, access: { - permission: &Permission::Privilege(&["datastore", "{store}"], PRIV_DATASTORE_AUDIT, false), + permission: &Permission::Privilege(&["datastore", "{store}"], PRIV_DATASTORE_AUDIT | PRIV_DATASTORE_BACKUP, true), }, )] /// Get datastore status. @@ -411,24 +470,34 @@ const API_METHOD_PRUNE: ApiMethod = ApiMethod::new( ("store", false, &DATASTORE_SCHEMA), ]) ) -).access(None, &Permission::Privilege(&["datastore", "{store}"], PRIV_DATASTORE_PRUNE, false)); +).access(None, &Permission::Privilege( + &["datastore", "{store}"], + PRIV_DATASTORE_MODIFY | PRIV_DATASTORE_PRUNE, + true) +); fn prune( param: Value, _info: &ApiMethod, - _rpcenv: &mut dyn RpcEnvironment, + rpcenv: &mut dyn RpcEnvironment, ) -> Result { - let store = param["store"].as_str().unwrap(); - + let store = tools::required_string_param(¶m, "store")?; let backup_type = tools::required_string_param(¶m, "backup-type")?; let backup_id = tools::required_string_param(¶m, "backup-id")?; + let username = rpcenv.get_user().unwrap(); + let user_info = CachedUserInfo::new()?; + let user_privs = user_info.lookup_privs(&username, &["datastore", &store]); + let dry_run = param["dry-run"].as_bool().unwrap_or(false); let group = BackupGroup::new(backup_type, backup_id); - let datastore = DataStore::lookup_datastore(store)?; + let datastore = DataStore::lookup_datastore(&store)?; + + let allowed = (user_privs & PRIV_DATASTORE_MODIFY) != 0; + if !allowed { check_backup_owner(&datastore, &group, &username)?; } let prune_options = PruneOptions { keep_last: param["keep-last"].as_u64(), @@ -535,7 +604,7 @@ fn prune( schema: UPID_SCHEMA, }, access: { - permission: &Permission::Privilege(&["datastore", "{store}"], PRIV_DATASTORE_PRUNE, false), + permission: &Permission::Privilege(&["datastore", "{store}"], PRIV_DATASTORE_MODIFY, false), }, )] /// Start garbage collection. @@ -592,19 +661,30 @@ pub fn garbage_collection_status( #[api( access: { - permission: &Permission::Privilege(&["datastore"], PRIV_DATASTORE_AUDIT, false), + permission: &Permission::Anybody, }, )] /// Datastore list fn get_datastore_list( _param: Value, _info: &ApiMethod, - _rpcenv: &mut dyn RpcEnvironment, + rpcenv: &mut dyn RpcEnvironment, ) -> Result { let (config, _digest) = datastore::config()?; - Ok(config.convert_to_array("store", None, &[])) + let username = rpcenv.get_user().unwrap(); + let user_info = CachedUserInfo::new()?; + + let mut skip: Vec<&str> = Vec::new(); + + for (store, _) in &config.sections { + let user_privs = user_info.lookup_privs(&username, &["datastore", &store]); + let allowed = (user_privs & (PRIV_DATASTORE_AUDIT| PRIV_DATASTORE_BACKUP)) != 0; + if !allowed { skip.push(store); } + } + + Ok(config.convert_to_array("store", None, &skip)) } #[sortable] @@ -620,32 +700,42 @@ pub const API_METHOD_DOWNLOAD_FILE: ApiMethod = ApiMethod::new( ("file-name", false, &BACKUP_ARCHIVE_NAME_SCHEMA), ]), ) -).access(None, &Permission::Privilege(&["datastore", "{store}"], PRIV_DATASTORE_READ, false)); +).access(None, &Permission::Privilege( + &["datastore", "{store}"], + PRIV_DATASTORE_READ | PRIV_DATASTORE_BACKUP, + true) +); fn download_file( _parts: Parts, _req_body: Body, param: Value, _info: &ApiMethod, - _rpcenv: Box, + rpcenv: Box, ) -> ApiResponseFuture { async move { let store = tools::required_string_param(¶m, "store")?; - let datastore = DataStore::lookup_datastore(store)?; + let username = rpcenv.get_user().unwrap(); + let user_info = CachedUserInfo::new()?; + let user_privs = user_info.lookup_privs(&username, &["datastore", &store]); + let file_name = tools::required_string_param(¶m, "file-name")?.to_owned(); let backup_type = tools::required_string_param(¶m, "backup-type")?; let backup_id = tools::required_string_param(¶m, "backup-id")?; let backup_time = tools::required_integer_param(¶m, "backup-time")?; + 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(), &username)?; } + println!("Download {} from {} ({}/{}/{}/{})", file_name, store, backup_type, backup_id, Local.timestamp(backup_time, 0), file_name); - let backup_dir = BackupDir::new(backup_type, backup_id, backup_time); - let mut path = datastore.base_path(); path.push(backup_dir.relative_path()); path.push(&file_name); @@ -671,7 +761,7 @@ fn download_file( pub const API_METHOD_UPLOAD_BACKUP_LOG: ApiMethod = ApiMethod::new( &ApiHandler::AsyncHttp(&upload_backup_log), &ObjectSchema::new( - "Download single raw file from backup snapshot.", + "Upload the client backup log file into a backup snapshot ('client.log.blob').", &sorted!([ ("store", false, &DATASTORE_SCHEMA), ("backup-type", false, &BACKUP_TYPE_SCHEMA), @@ -679,19 +769,21 @@ pub const API_METHOD_UPLOAD_BACKUP_LOG: ApiMethod = ApiMethod::new( ("backup-time", false, &BACKUP_TIME_SCHEMA), ]), ) -).access(None, &Permission::Privilege(&["datastore", "{store}"], PRIV_DATASTORE_CREATE_BACKUP, false)); +).access( + Some("Only the backup creator/owner is allowed to do this."), + &Permission::Privilege(&["datastore", "{store}"], PRIV_DATASTORE_BACKUP, false) +); fn upload_backup_log( _parts: Parts, req_body: Body, param: Value, _info: &ApiMethod, - _rpcenv: Box, + rpcenv: Box, ) -> ApiResponseFuture { async move { let store = tools::required_string_param(¶m, "store")?; - let datastore = DataStore::lookup_datastore(store)?; let file_name = "client.log.blob"; @@ -702,6 +794,9 @@ fn upload_backup_log( let backup_dir = BackupDir::new(backup_type, backup_id, backup_time); + let username = rpcenv.get_user().unwrap(); + check_backup_owner(&datastore, backup_dir.group(), &username)?; + let mut path = datastore.base_path(); path.push(backup_dir.relative_path()); path.push(&file_name); diff --git a/src/api2/backup.rs b/src/api2/backup.rs index b3e594eb..3c9cfd87 100644 --- a/src/api2/backup.rs +++ b/src/api2/backup.rs @@ -14,7 +14,7 @@ use crate::tools::{self, WrappedReaderStream}; use crate::server::{WorkerTask, H2Service}; use crate::backup::*; use crate::api2::types::*; -use crate::config::acl::PRIV_DATASTORE_CREATE_BACKUP; +use crate::config::acl::PRIV_DATASTORE_BACKUP; use crate::config::cached_user_info::CachedUserInfo; mod environment; @@ -41,7 +41,7 @@ pub const API_METHOD_UPGRADE_BACKUP: ApiMethod = ApiMethod::new( ) ).access( // Note: parameter 'store' is no uri parameter, so we need to test inside function body - Some("The user needs Datastore.CreateBackup privilege on /datastore/{store}."), + Some("The user needs Datastore.Backup privilege on /datastore/{store} and needs to own the backup group."), &Permission::Anybody ); @@ -53,7 +53,7 @@ fn upgrade_to_backup_protocol( rpcenv: Box, ) -> ApiResponseFuture { - async move { +async move { let debug = param["debug"].as_bool().unwrap_or(false); let username = rpcenv.get_user().unwrap(); @@ -61,7 +61,7 @@ fn upgrade_to_backup_protocol( let store = tools::required_string_param(¶m, "store")?.to_owned(); let user_info = CachedUserInfo::new()?; - user_info.check_privs(&username, &["datastore", &store], PRIV_DATASTORE_CREATE_BACKUP, false)?; + user_info.check_privs(&username, &["datastore", &store], PRIV_DATASTORE_BACKUP, false)?; let datastore = DataStore::lookup_datastore(&store)?; @@ -88,6 +88,12 @@ fn upgrade_to_backup_protocol( let env_type = rpcenv.env_type(); let backup_group = BackupGroup::new(backup_type, backup_id); + let owner = datastore.create_backup_group(&backup_group, &username)?; + // permission check + if owner != username { // only the owner is allowed to create additional snapshots + bail!("backup owner check failed ({} != {})", username, owner); + } + let last_backup = BackupInfo::last_backup(&datastore.base_path(), &backup_group).unwrap_or(None); let backup_dir = BackupDir::new_with_group(backup_group, backup_time); diff --git a/src/api2/pull.rs b/src/api2/pull.rs index 8e989275..de394419 100644 --- a/src/api2/pull.rs +++ b/src/api2/pull.rs @@ -16,7 +16,7 @@ use crate::backup::*; use crate::client::*; use crate::config::remote; use crate::api2::types::*; -use crate::config::acl::{PRIV_DATASTORE_CREATE_BACKUP, PRIV_DATASTORE_READ}; +use crate::config::acl::{PRIV_DATASTORE_BACKUP, PRIV_DATASTORE_READ}; use crate::config::cached_user_info::CachedUserInfo; // fixme: implement filters @@ -312,6 +312,7 @@ pub async fn pull_store( src_repo: &BackupRepository, tgt_store: Arc, delete: bool, + username: String, ) -> Result<(), Error> { let path = format!("api2/json/admin/datastore/{}/groups", src_repo.store()); @@ -332,15 +333,27 @@ pub async fn pull_store( let mut errors = false; let mut new_groups = std::collections::HashSet::new(); + for item in list.iter() { + new_groups.insert(BackupGroup::new(&item.backup_type, &item.backup_id)); + } for item in list { let group = BackupGroup::new(&item.backup_type, &item.backup_id); + + let owner = tgt_store.create_backup_group(&group, &username)?; + // permission check + if owner != username { // only the owner is allowed to create additional snapshots + worker.log(format!("sync group {}/{} failed - owner check failed ({} != {})", + item.backup_type, item.backup_id, username, owner)); + errors = true; + continue; // do not stop here, instead continue + } + if let Err(err) = pull_group(worker, client, src_repo, tgt_store.clone(), &group, delete).await { worker.log(format!("sync group {}/{} failed - {}", item.backup_type, item.backup_id, err)); errors = true; - // do not stop here, instead continue + continue; // do not stop here, instead continue } - new_groups.insert(group); } if delete { @@ -391,7 +404,9 @@ pub async fn pull_store( }, access: { // Note: used parameters are no uri parameters, so we need to test inside function body - description: "The user needs Datastore.CreateBackup privilege on '/datastore/{store}' and Datastore.Read on '/remote/{remote}/{remote-store}'.", + description: r###"The user needs Datastore.Backup privilege on '/datastore/{store}', +and needs to own the backup group. Datastore.Read is required on '/remote/{remote}/{remote-store}'. +"###, permission: &Permission::Anybody, }, )] @@ -408,7 +423,7 @@ async fn pull ( let user_info = CachedUserInfo::new()?; let username = rpcenv.get_user().unwrap(); - user_info.check_privs(&username, &["datastore", &store], PRIV_DATASTORE_CREATE_BACKUP, false)?; + user_info.check_privs(&username, &["datastore", &store], PRIV_DATASTORE_BACKUP, false)?; user_info.check_privs(&username, &["remote", &remote, &remote_store], PRIV_DATASTORE_READ, false)?; let delete = delete.unwrap_or(true); @@ -437,7 +452,7 @@ async fn pull ( // explicit create shared lock to prevent GC on newly created chunks let _shared_store_lock = tgt_store.try_shared_chunk_store_lock()?; - pull_store(&worker, &client, &src_repo, tgt_store.clone(), delete).await?; + pull_store(&worker, &client, &src_repo, tgt_store.clone(), delete, username).await?; worker.log(format!("sync datastore '{}' end", store)); diff --git a/src/backup/datastore.rs b/src/backup/datastore.rs index 24ae4e0c..790f4d17 100644 --- a/src/backup/datastore.rs +++ b/src/backup/datastore.rs @@ -1,5 +1,5 @@ use std::collections::{HashSet, HashMap}; -use std::io; +use std::io::{self, Write}; use std::path::{Path, PathBuf}; use std::sync::{Arc, Mutex}; @@ -236,18 +236,80 @@ impl DataStore { } } - pub fn create_backup_dir(&self, backup_dir: &BackupDir) -> Result<(PathBuf, bool), io::Error> { + /// Returns the backup owner. + /// + /// The backup owner is the user who first created the backup group. + pub fn get_owner(&self, backup_group: &BackupGroup) -> Result { + let mut full_path = self.base_path(); + full_path.push(backup_group.group_path()); + full_path.push("owner"); + let owner = proxmox::tools::fs::file_read_firstline(full_path)?; + Ok(owner.trim_end().to_string()) // remove trailing newline + } + + /// Set the backup owner. + pub fn set_owner(&self, backup_group: &BackupGroup, userid: &str, force: bool) -> Result<(), Error> { + let mut path = self.base_path(); + path.push(backup_group.group_path()); + path.push("owner"); + + let mut open_options = std::fs::OpenOptions::new(); + open_options.write(true); + open_options.truncate(true); + + if force { + open_options.create(true); + } else { + open_options.create_new(true); + } + + let mut file = open_options.open(&path) + .map_err(|err| format_err!("unable to create owner file {:?} - {}", path, err))?; + + write!(file, "{}\n", userid) + .map_err(|err| format_err!("unable to write owner file {:?} - {}", path, err))?; + + Ok(()) + } + + /// Create a backup group if it does not already exists. + /// + /// And set the owner to 'userid'. If the group already exists, it returns the + /// current owner (instead of setting the owner). + pub fn create_backup_group(&self, backup_group: &BackupGroup, userid: &str) -> Result { // create intermediate path first: - let mut full_path = self.base_path(); - full_path.push(backup_dir.group().group_path()); + let base_path = self.base_path(); + + let mut full_path = base_path.clone(); + full_path.push(backup_group.backup_type()); std::fs::create_dir_all(&full_path)?; + full_path.push(backup_group.backup_id()); + + // create the last component now + match std::fs::create_dir(&full_path) { + Ok(_) => { + self.set_owner(backup_group, userid, false)?; + let owner = self.get_owner(backup_group)?; // just to be sure + Ok(owner) + } + Err(ref err) if err.kind() == io::ErrorKind::AlreadyExists => { + let owner = self.get_owner(backup_group)?; // just to be sure + Ok(owner) + } + Err(err) => bail!("unable to create backup group {:?} - {}", full_path, err), + } + } + + /// Creates a new backup snapshot inside a BackupGroup + /// + /// The BackupGroup directory needs to exist. + pub fn create_backup_dir(&self, backup_dir: &BackupDir) -> Result<(PathBuf, bool), io::Error> { let relative_path = backup_dir.relative_path(); let mut full_path = self.base_path(); full_path.push(&relative_path); - // create the last component now match std::fs::create_dir(&full_path) { Ok(_) => Ok((relative_path, true)), Err(ref e) if e.kind() == io::ErrorKind::AlreadyExists => Ok((relative_path, false)), diff --git a/src/config/acl.rs b/src/config/acl.rs index 2cae1c74..7b500d7d 100644 --- a/src/config/acl.rs +++ b/src/config/acl.rs @@ -17,8 +17,11 @@ pub const PRIV_SYS_POWER_MANAGEMENT: u64 = 1 << 2; pub const PRIV_DATASTORE_AUDIT: u64 = 1 << 3; pub const PRIV_DATASTORE_MODIFY: u64 = 1 << 4; -pub const PRIV_DATASTORE_CREATE_BACKUP: u64 = 1 << 5; -pub const PRIV_DATASTORE_READ: u64 = 1 << 6; +pub const PRIV_DATASTORE_READ: u64 = 1 << 5; + +/// Datastore.Backup also requires backup ownership +pub const PRIV_DATASTORE_BACKUP: u64 = 1 << 6; +/// Datastore.Prune also requires backup ownership pub const PRIV_DATASTORE_PRUNE: u64 = 1 << 7; pub const PRIV_PERMISSIONS_MODIFY: u64 = 1 << 8; @@ -33,12 +36,12 @@ PRIV_DATASTORE_AUDIT; pub const ROLE_DATASTORE_ADMIN: u64 = PRIV_DATASTORE_AUDIT | PRIV_DATASTORE_MODIFY | -PRIV_DATASTORE_CREATE_BACKUP | PRIV_DATASTORE_READ | +PRIV_DATASTORE_BACKUP | PRIV_DATASTORE_PRUNE; pub const ROLE_DATASTORE_USER: u64 = -PRIV_DATASTORE_CREATE_BACKUP; +PRIV_DATASTORE_BACKUP; pub const ROLE_DATASTORE_AUDIT: u64 = PRIV_DATASTORE_AUDIT;