sync: allow sync for non-superusers

by requiring
- Datastore.Backup permission for target datastore
- Remote.Read permission for source remote/datastore
- Datastore.Prune if vanished snapshots should be removed
- Datastore.Modify if another user should own the freshly synced
snapshots

reading a sync job entry only requires knowing about both the source
remote and the target datastore.

note that this does not affect the Authid used to authenticate with the
remote, which of course also needs permissions to access the source
datastore.

Signed-off-by: Fabian Grünbichler <f.gruenbichler@proxmox.com>
This commit is contained in:
Fabian Grünbichler 2020-10-30 12:36:42 +01:00 committed by Dietmar Maurer
parent f1694b062d
commit 59af9ca98e
4 changed files with 182 additions and 17 deletions

View File

@ -1,12 +1,15 @@
use anyhow::{format_err, Error}; use anyhow::{bail, format_err, Error};
use serde_json::Value; use serde_json::Value;
use proxmox::api::{api, ApiMethod, Router, RpcEnvironment}; use proxmox::api::{api, ApiMethod, Permission, Router, RpcEnvironment};
use proxmox::api::router::SubdirMap; use proxmox::api::router::SubdirMap;
use proxmox::{list_subdirs_api_method, sortable}; use proxmox::{list_subdirs_api_method, sortable};
use crate::api2::types::*; use crate::api2::types::*;
use crate::api2::pull::do_sync_job; use crate::api2::pull::do_sync_job;
use crate::api2::config::sync::{check_sync_job_modify_access, check_sync_job_read_access};
use crate::config::cached_user_info::CachedUserInfo;
use crate::config::sync::{self, SyncJobStatus, SyncJobConfig}; use crate::config::sync::{self, SyncJobStatus, SyncJobConfig};
use crate::server::UPID; use crate::server::UPID;
use crate::server::jobstate::{Job, JobState}; use crate::server::jobstate::{Job, JobState};
@ -27,6 +30,10 @@ use crate::tools::systemd::time::{
type: Array, type: Array,
items: { type: sync::SyncJobStatus }, items: { type: sync::SyncJobStatus },
}, },
access: {
description: "Limited to sync jobs where user has Datastore.Audit on target datastore, and Remote.Audit on source remote.",
permission: &Permission::Anybody,
},
)] )]
/// List all sync jobs /// List all sync jobs
pub fn list_sync_jobs( pub fn list_sync_jobs(
@ -35,6 +42,9 @@ pub fn list_sync_jobs(
mut rpcenv: &mut dyn RpcEnvironment, mut rpcenv: &mut dyn RpcEnvironment,
) -> Result<Vec<SyncJobStatus>, Error> { ) -> Result<Vec<SyncJobStatus>, Error> {
let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
let user_info = CachedUserInfo::new()?;
let (config, digest) = sync::config()?; let (config, digest) = sync::config()?;
let mut list: Vec<SyncJobStatus> = config let mut list: Vec<SyncJobStatus> = config
@ -46,6 +56,10 @@ pub fn list_sync_jobs(
} else { } else {
true true
} }
})
.filter(|job: &SyncJobStatus| {
let as_config: SyncJobConfig = job.clone().into();
check_sync_job_read_access(&user_info, &auth_id, &as_config)
}).collect(); }).collect();
for job in &mut list { for job in &mut list {
@ -89,7 +103,11 @@ pub fn list_sync_jobs(
schema: JOB_ID_SCHEMA, schema: JOB_ID_SCHEMA,
} }
} }
} },
access: {
description: "User needs Datastore.Backup on target datastore, and Remote.Read on source remote. Additionally, remove_vanished requires Datastore.Prune, and any owner other than the user themselves requires Datastore.Modify",
permission: &Permission::Anybody,
},
)] )]
/// Runs the sync jobs manually. /// Runs the sync jobs manually.
fn run_sync_job( fn run_sync_job(
@ -97,11 +115,15 @@ fn run_sync_job(
_info: &ApiMethod, _info: &ApiMethod,
rpcenv: &mut dyn RpcEnvironment, rpcenv: &mut dyn RpcEnvironment,
) -> Result<String, Error> { ) -> Result<String, Error> {
let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
let user_info = CachedUserInfo::new()?;
let (config, _digest) = sync::config()?; let (config, _digest) = sync::config()?;
let sync_job: SyncJobConfig = config.lookup("sync", &id)?; let sync_job: SyncJobConfig = config.lookup("sync", &id)?;
let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?; if !check_sync_job_modify_access(&user_info, &auth_id, &sync_job) {
bail!("permission check failed");
}
let job = Job::new("syncjob", &id)?; let job = Job::new("syncjob", &id)?;

View File

@ -6,6 +6,7 @@ use proxmox::api::{api, ApiMethod, Router, RpcEnvironment, Permission};
use proxmox::tools::fs::open_file_locked; use proxmox::tools::fs::open_file_locked;
use crate::api2::types::*; use crate::api2::types::*;
use crate::config::cached_user_info::CachedUserInfo;
use crate::config::remote; use crate::config::remote;
use crate::config::acl::{PRIV_REMOTE_AUDIT, PRIV_REMOTE_MODIFY}; use crate::config::acl::{PRIV_REMOTE_AUDIT, PRIV_REMOTE_MODIFY};
@ -22,7 +23,8 @@ use crate::config::acl::{PRIV_REMOTE_AUDIT, PRIV_REMOTE_MODIFY};
}, },
}, },
access: { access: {
permission: &Permission::Privilege(&["remote"], PRIV_REMOTE_AUDIT, false), description: "List configured remotes filtered by Remote.Audit privileges",
permission: &Permission::Anybody,
}, },
)] )]
/// List all remotes /// List all remotes
@ -31,16 +33,25 @@ pub fn list_remotes(
_info: &ApiMethod, _info: &ApiMethod,
mut rpcenv: &mut dyn RpcEnvironment, mut rpcenv: &mut dyn RpcEnvironment,
) -> Result<Vec<remote::Remote>, Error> { ) -> Result<Vec<remote::Remote>, Error> {
let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
let user_info = CachedUserInfo::new()?;
let (config, digest) = remote::config()?; let (config, digest) = remote::config()?;
let mut list: Vec<remote::Remote> = config.convert_to_typed_array("remote")?; let mut list: Vec<remote::Remote> = config.convert_to_typed_array("remote")?;
// don't return password in api // don't return password in api
for remote in &mut list { for remote in &mut list {
remote.password = "".to_string(); remote.password = "".to_string();
} }
let list = list
.into_iter()
.filter(|remote| {
let privs = user_info.lookup_privs(&auth_id, &["remote", &remote.name]);
privs & PRIV_REMOTE_AUDIT != 0
})
.collect();
rpcenv["digest"] = proxmox::tools::digest_to_hex(&digest).into(); rpcenv["digest"] = proxmox::tools::digest_to_hex(&digest).into();
Ok(list) Ok(list)
} }

View File

@ -2,13 +2,72 @@ use anyhow::{bail, Error};
use serde_json::Value; use serde_json::Value;
use ::serde::{Deserialize, Serialize}; use ::serde::{Deserialize, Serialize};
use proxmox::api::{api, Router, RpcEnvironment}; use proxmox::api::{api, Permission, Router, RpcEnvironment};
use proxmox::tools::fs::open_file_locked; use proxmox::tools::fs::open_file_locked;
use crate::api2::types::*; use crate::api2::types::*;
use crate::config::acl::{
PRIV_DATASTORE_AUDIT,
PRIV_DATASTORE_BACKUP,
PRIV_DATASTORE_MODIFY,
PRIV_DATASTORE_PRUNE,
PRIV_REMOTE_AUDIT,
PRIV_REMOTE_READ,
};
use crate::config::cached_user_info::CachedUserInfo;
use crate::config::sync::{self, SyncJobConfig}; use crate::config::sync::{self, SyncJobConfig};
// fixme: add access permissions pub fn check_sync_job_read_access(
user_info: &CachedUserInfo,
auth_id: &Authid,
job: &SyncJobConfig,
) -> bool {
let datastore_privs = user_info.lookup_privs(&auth_id, &["datastore", &job.store]);
if datastore_privs & PRIV_DATASTORE_AUDIT == 0 {
return false;
}
let remote_privs = user_info.lookup_privs(&auth_id, &["remote", &job.remote]);
remote_privs & PRIV_REMOTE_AUDIT != 0
}
// user can run the corresponding pull job
pub fn check_sync_job_modify_access(
user_info: &CachedUserInfo,
auth_id: &Authid,
job: &SyncJobConfig,
) -> bool {
let datastore_privs = user_info.lookup_privs(&auth_id, &["datastore", &job.store]);
if datastore_privs & PRIV_DATASTORE_BACKUP == 0 {
return false;
}
if let Some(true) = job.remove_vanished {
if datastore_privs & PRIV_DATASTORE_PRUNE == 0 {
return false;
}
}
let correct_owner = match job.owner {
Some(ref owner) => {
owner == auth_id
|| (owner.is_token()
&& !auth_id.is_token()
&& owner.user() == auth_id.user())
},
// default sync owner
None => auth_id == Authid::backup_auth_id(),
};
// same permission as changing ownership after syncing
if !correct_owner && datastore_privs & PRIV_DATASTORE_MODIFY == 0 {
return false;
}
let remote_privs = user_info.lookup_privs(&auth_id, &["remote", &job.remote, &job.remote_store]);
remote_privs & PRIV_REMOTE_READ != 0
}
#[api( #[api(
input: { input: {
@ -19,12 +78,18 @@ use crate::config::sync::{self, SyncJobConfig};
type: Array, type: Array,
items: { type: sync::SyncJobConfig }, items: { type: sync::SyncJobConfig },
}, },
access: {
description: "Limited to sync job entries where user has Datastore.Audit on target datastore, and Remote.Audit on source remote.",
permission: &Permission::Anybody,
},
)] )]
/// List all sync jobs /// List all sync jobs
pub fn list_sync_jobs( pub fn list_sync_jobs(
_param: Value, _param: Value,
mut rpcenv: &mut dyn RpcEnvironment, mut rpcenv: &mut dyn RpcEnvironment,
) -> Result<Vec<SyncJobConfig>, Error> { ) -> Result<Vec<SyncJobConfig>, Error> {
let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
let user_info = CachedUserInfo::new()?;
let (config, digest) = sync::config()?; let (config, digest) = sync::config()?;
@ -32,7 +97,11 @@ pub fn list_sync_jobs(
rpcenv["digest"] = proxmox::tools::digest_to_hex(&digest).into(); rpcenv["digest"] = proxmox::tools::digest_to_hex(&digest).into();
Ok(list) let list = list
.into_iter()
.filter(|sync_job| check_sync_job_read_access(&user_info, &auth_id, &sync_job))
.collect();
Ok(list)
} }
#[api( #[api(
@ -69,13 +138,25 @@ pub fn list_sync_jobs(
}, },
}, },
}, },
access: {
description: "User needs Datastore.Backup on target datastore, and Remote.Read on source remote. Additionally, remove_vanished requires Datastore.Prune, and any owner other than the user themselves requires Datastore.Modify",
permission: &Permission::Anybody,
},
)] )]
/// Create a new sync job. /// Create a new sync job.
pub fn create_sync_job(param: Value) -> Result<(), Error> { pub fn create_sync_job(
param: Value,
rpcenv: &mut dyn RpcEnvironment,
) -> Result<(), Error> {
let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
let user_info = CachedUserInfo::new()?;
let _lock = open_file_locked(sync::SYNC_CFG_LOCKFILE, std::time::Duration::new(10, 0), true)?; let _lock = open_file_locked(sync::SYNC_CFG_LOCKFILE, std::time::Duration::new(10, 0), true)?;
let sync_job: sync::SyncJobConfig = serde_json::from_value(param.clone())?; let sync_job: sync::SyncJobConfig = serde_json::from_value(param.clone())?;
if !check_sync_job_modify_access(&user_info, &auth_id, &sync_job) {
bail!("permission check failed");
}
let (mut config, _digest) = sync::config()?; let (mut config, _digest) = sync::config()?;
@ -104,15 +185,26 @@ pub fn create_sync_job(param: Value) -> Result<(), Error> {
description: "The sync job configuration.", description: "The sync job configuration.",
type: sync::SyncJobConfig, type: sync::SyncJobConfig,
}, },
access: {
description: "Limited to sync job entries where user has Datastore.Audit on target datastore, and Remote.Audit on source remote.",
permission: &Permission::Anybody,
},
)] )]
/// Read a sync job configuration. /// Read a sync job configuration.
pub fn read_sync_job( pub fn read_sync_job(
id: String, id: String,
mut rpcenv: &mut dyn RpcEnvironment, mut rpcenv: &mut dyn RpcEnvironment,
) -> Result<SyncJobConfig, Error> { ) -> Result<SyncJobConfig, Error> {
let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
let user_info = CachedUserInfo::new()?;
let (config, digest) = sync::config()?; let (config, digest) = sync::config()?;
let sync_job = config.lookup("sync", &id)?; let sync_job = config.lookup("sync", &id)?;
if !check_sync_job_read_access(&user_info, &auth_id, &sync_job) {
bail!("permission check failed");
}
rpcenv["digest"] = proxmox::tools::digest_to_hex(&digest).into(); rpcenv["digest"] = proxmox::tools::digest_to_hex(&digest).into();
Ok(sync_job) Ok(sync_job)
@ -183,6 +275,10 @@ pub enum DeletableProperty {
}, },
}, },
}, },
access: {
permission: &Permission::Anybody,
description: "User needs Datastore.Backup on target datastore, and Remote.Read on source remote. Additionally, remove_vanished requires Datastore.Prune, and any owner other than the user themselves requires Datastore.Modify",
},
)] )]
/// Update sync job config. /// Update sync job config.
pub fn update_sync_job( pub fn update_sync_job(
@ -196,7 +292,10 @@ pub fn update_sync_job(
schedule: Option<String>, schedule: Option<String>,
delete: Option<Vec<DeletableProperty>>, delete: Option<Vec<DeletableProperty>>,
digest: Option<String>, digest: Option<String>,
rpcenv: &mut dyn RpcEnvironment,
) -> Result<(), Error> { ) -> Result<(), Error> {
let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
let user_info = CachedUserInfo::new()?;
let _lock = open_file_locked(sync::SYNC_CFG_LOCKFILE, std::time::Duration::new(10, 0), true)?; let _lock = open_file_locked(sync::SYNC_CFG_LOCKFILE, std::time::Duration::new(10, 0), true)?;
@ -233,11 +332,15 @@ pub fn update_sync_job(
if let Some(store) = store { data.store = store; } if let Some(store) = store { data.store = store; }
if let Some(remote) = remote { data.remote = remote; } if let Some(remote) = remote { data.remote = remote; }
if let Some(remote_store) = remote_store { data.remote_store = remote_store; } if let Some(remote_store) = remote_store { data.remote_store = remote_store; }
if let Some(owner) = owner { data.owner = owner; } if let Some(owner) = owner { data.owner = Some(owner); }
if schedule.is_some() { data.schedule = schedule; } if schedule.is_some() { data.schedule = schedule; }
if remove_vanished.is_some() { data.remove_vanished = remove_vanished; } if remove_vanished.is_some() { data.remove_vanished = remove_vanished; }
if !check_sync_job_modify_access(&user_info, &auth_id, &data) {
bail!("permission check failed");
}
config.set_data(&id, "sync", &data)?; config.set_data(&id, "sync", &data)?;
sync::save_config(&config)?; sync::save_config(&config)?;
@ -258,9 +361,19 @@ pub fn update_sync_job(
}, },
}, },
}, },
access: {
permission: &Permission::Anybody,
description: "User needs Datastore.Backup on target datastore, and Remote.Read on source remote. Additionally, remove_vanished requires Datastore.Prune, and any owner other than the user themselves requires Datastore.Modify",
},
)] )]
/// Remove a sync job configuration /// Remove a sync job configuration
pub fn delete_sync_job(id: String, digest: Option<String>) -> Result<(), Error> { pub fn delete_sync_job(
id: String,
digest: Option<String>,
rpcenv: &mut dyn RpcEnvironment,
) -> Result<(), Error> {
let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
let user_info = CachedUserInfo::new()?;
let _lock = open_file_locked(sync::SYNC_CFG_LOCKFILE, std::time::Duration::new(10, 0), true)?; let _lock = open_file_locked(sync::SYNC_CFG_LOCKFILE, std::time::Duration::new(10, 0), true)?;
@ -271,10 +384,15 @@ pub fn delete_sync_job(id: String, digest: Option<String>) -> Result<(), Error>
crate::tools::detect_modified_configuration_file(&digest, &expected_digest)?; crate::tools::detect_modified_configuration_file(&digest, &expected_digest)?;
} }
match config.sections.get(&id) { match config.lookup("sync", &id) {
Some(_) => { config.sections.remove(&id); }, Ok(job) => {
None => bail!("job '{}' does not exist.", id), if !check_sync_job_modify_access(&user_info, &auth_id, &job) {
} bail!("permission check failed");
}
config.sections.remove(&id);
},
Err(_) => { bail!("job '{}' does not exist.", id) },
};
sync::save_config(&config)?; sync::save_config(&config)?;

View File

@ -21,7 +21,6 @@ lazy_static! {
static ref CONFIG: SectionConfig = init(); static ref CONFIG: SectionConfig = init();
} }
#[api( #[api(
properties: { properties: {
id: { id: {
@ -72,6 +71,21 @@ pub struct SyncJobConfig {
pub schedule: Option<String>, pub schedule: Option<String>,
} }
impl From<&SyncJobStatus> for SyncJobConfig {
fn from(job_status: &SyncJobStatus) -> Self {
Self {
id: job_status.id.clone(),
store: job_status.store.clone(),
owner: job_status.owner.clone(),
remote: job_status.remote.clone(),
remote_store: job_status.remote_store.clone(),
remove_vanished: job_status.remove_vanished.clone(),
comment: job_status.comment.clone(),
schedule: job_status.schedule.clone(),
}
}
}
// FIXME: generate duplicate schemas/structs from one listing? // FIXME: generate duplicate schemas/structs from one listing?
#[api( #[api(
properties: { properties: {