1 use anyhow
::{bail, Error}
;
3 use ::serde
::{Deserialize, Serialize}
;
5 use proxmox
::api
::{api, Permission, Router, RpcEnvironment}
;
6 use proxmox
::tools
::fs
::open_file_locked
;
8 use crate::api2
::types
::*;
10 use crate::config
::acl
::{
12 PRIV_DATASTORE_BACKUP
,
13 PRIV_DATASTORE_MODIFY
,
19 use crate::config
::cached_user_info
::CachedUserInfo
;
20 use crate::config
::sync
::{self, SyncJobConfig}
;
22 pub fn check_sync_job_read_access(
23 user_info
: &CachedUserInfo
,
27 let datastore_privs
= user_info
.lookup_privs(&auth_id
, &["datastore", &job
.store
]);
28 if datastore_privs
& PRIV_DATASTORE_AUDIT
== 0 {
32 let remote_privs
= user_info
.lookup_privs(&auth_id
, &["remote", &job
.remote
]);
33 remote_privs
& PRIV_REMOTE_AUDIT
!= 0
36 // user can run the corresponding pull job
37 pub fn check_sync_job_modify_access(
38 user_info
: &CachedUserInfo
,
42 let datastore_privs
= user_info
.lookup_privs(&auth_id
, &["datastore", &job
.store
]);
43 if datastore_privs
& PRIV_DATASTORE_BACKUP
== 0 {
47 if let Some(true) = job
.remove_vanished
{
48 if datastore_privs
& PRIV_DATASTORE_PRUNE
== 0 {
53 let correct_owner
= match job
.owner
{
57 && !auth_id
.is_token()
58 && owner
.user() == auth_id
.user())
61 None
=> auth_id
== Authid
::root_auth_id(),
64 // same permission as changing ownership after syncing
65 if !correct_owner
&& datastore_privs
& PRIV_DATASTORE_MODIFY
== 0 {
69 let remote_privs
= user_info
.lookup_privs(&auth_id
, &["remote", &job
.remote
, &job
.remote_store
]);
70 remote_privs
& PRIV_REMOTE_READ
!= 0
78 description
: "List configured jobs.",
80 items
: { type: sync::SyncJobConfig }
,
83 description
: "Limited to sync job entries where user has Datastore.Audit on target datastore, and Remote.Audit on source remote.",
84 permission
: &Permission
::Anybody
,
87 /// List all sync jobs
88 pub fn list_sync_jobs(
90 mut rpcenv
: &mut dyn RpcEnvironment
,
91 ) -> Result
<Vec
<SyncJobConfig
>, Error
> {
92 let auth_id
: Authid
= rpcenv
.get_auth_id().unwrap().parse()?
;
93 let user_info
= CachedUserInfo
::new()?
;
95 let (config
, digest
) = sync
::config()?
;
97 let list
= config
.convert_to_typed_array("sync")?
;
99 rpcenv
["digest"] = proxmox
::tools
::digest_to_hex(&digest
).into();
103 .filter(|sync_job
| check_sync_job_read_access(&user_info
, &auth_id
, &sync_job
))
113 schema
: JOB_ID_SCHEMA
,
116 schema
: DATASTORE_SCHEMA
,
123 schema
: REMOTE_ID_SCHEMA
,
126 schema
: DATASTORE_SCHEMA
,
129 schema
: REMOVE_VANISHED_BACKUPS_SCHEMA
,
134 schema
: SINGLE_LINE_COMMENT_SCHEMA
,
138 schema
: SYNC_SCHEDULE_SCHEMA
,
143 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",
144 permission
: &Permission
::Anybody
,
147 /// Create a new sync job.
148 pub fn create_sync_job(
150 rpcenv
: &mut dyn RpcEnvironment
,
151 ) -> Result
<(), Error
> {
152 let auth_id
: Authid
= rpcenv
.get_auth_id().unwrap().parse()?
;
153 let user_info
= CachedUserInfo
::new()?
;
155 let _lock
= open_file_locked(sync
::SYNC_CFG_LOCKFILE
, std
::time
::Duration
::new(10, 0), true)?
;
157 let sync_job
: sync
::SyncJobConfig
= serde_json
::from_value(param
.clone())?
;
158 if !check_sync_job_modify_access(&user_info
, &auth_id
, &sync_job
) {
159 bail
!("permission check failed");
162 let (mut config
, _digest
) = sync
::config()?
;
164 if let Some(_
) = config
.sections
.get(&sync_job
.id
) {
165 bail
!("job '{}' already exists.", sync_job
.id
);
168 config
.set_data(&sync_job
.id
, "sync", &sync_job
)?
;
170 sync
::save_config(&config
)?
;
172 crate::server
::jobstate
::create_state_file("syncjob", &sync_job
.id
)?
;
181 schema
: JOB_ID_SCHEMA
,
186 description
: "The sync job configuration.",
187 type: sync
::SyncJobConfig
,
190 description
: "Limited to sync job entries where user has Datastore.Audit on target datastore, and Remote.Audit on source remote.",
191 permission
: &Permission
::Anybody
,
194 /// Read a sync job configuration.
195 pub fn read_sync_job(
197 mut rpcenv
: &mut dyn RpcEnvironment
,
198 ) -> Result
<SyncJobConfig
, Error
> {
199 let auth_id
: Authid
= rpcenv
.get_auth_id().unwrap().parse()?
;
200 let user_info
= CachedUserInfo
::new()?
;
202 let (config
, digest
) = sync
::config()?
;
204 let sync_job
= config
.lookup("sync", &id
)?
;
205 if !check_sync_job_read_access(&user_info
, &auth_id
, &sync_job
) {
206 bail
!("permission check failed");
209 rpcenv
["digest"] = proxmox
::tools
::digest_to_hex(&digest
).into();
215 #[derive(Serialize, Deserialize)]
216 #[serde(rename_all="kebab-case")]
217 #[allow(non_camel_case_types)]
218 /// Deletable property name
219 pub enum DeletableProperty
{
220 /// Delete the owner property.
222 /// Delete the comment property.
224 /// Delete the job schedule.
226 /// Delete the remove-vanished flag.
235 schema
: JOB_ID_SCHEMA
,
238 schema
: DATASTORE_SCHEMA
,
246 schema
: REMOTE_ID_SCHEMA
,
250 schema
: DATASTORE_SCHEMA
,
254 schema
: REMOVE_VANISHED_BACKUPS_SCHEMA
,
259 schema
: SINGLE_LINE_COMMENT_SCHEMA
,
263 schema
: SYNC_SCHEDULE_SCHEMA
,
266 description
: "List of properties to delete.",
270 type: DeletableProperty
,
275 schema
: PROXMOX_CONFIG_DIGEST_SCHEMA
,
280 permission
: &Permission
::Anybody
,
281 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",
284 /// Update sync job config.
285 pub fn update_sync_job(
287 store
: Option
<String
>,
288 owner
: Option
<Authid
>,
289 remote
: Option
<String
>,
290 remote_store
: Option
<String
>,
291 remove_vanished
: Option
<bool
>,
292 comment
: Option
<String
>,
293 schedule
: Option
<String
>,
294 delete
: Option
<Vec
<DeletableProperty
>>,
295 digest
: Option
<String
>,
296 rpcenv
: &mut dyn RpcEnvironment
,
297 ) -> Result
<(), Error
> {
298 let auth_id
: Authid
= rpcenv
.get_auth_id().unwrap().parse()?
;
299 let user_info
= CachedUserInfo
::new()?
;
301 let _lock
= open_file_locked(sync
::SYNC_CFG_LOCKFILE
, std
::time
::Duration
::new(10, 0), true)?
;
303 // pass/compare digest
304 let (mut config
, expected_digest
) = sync
::config()?
;
306 if let Some(ref digest
) = digest
{
307 let digest
= proxmox
::tools
::hex_to_digest(digest
)?
;
308 crate::tools
::detect_modified_configuration_file(&digest
, &expected_digest
)?
;
311 let mut data
: sync
::SyncJobConfig
= config
.lookup("sync", &id
)?
;
313 if let Some(delete
) = delete
{
314 for delete_prop
in delete
{
316 DeletableProperty
::owner
=> { data.owner = None; }
,
317 DeletableProperty
::comment
=> { data.comment = None; }
,
318 DeletableProperty
::schedule
=> { data.schedule = None; }
,
319 DeletableProperty
::remove_vanished
=> { data.remove_vanished = None; }
,
324 if let Some(comment
) = comment
{
325 let comment
= comment
.trim().to_string();
326 if comment
.is_empty() {
329 data
.comment
= Some(comment
);
333 if let Some(store
) = store { data.store = store; }
334 if let Some(remote
) = remote { data.remote = remote; }
335 if let Some(remote_store
) = remote_store { data.remote_store = remote_store; }
336 if let Some(owner
) = owner { data.owner = Some(owner); }
338 if schedule
.is_some() { data.schedule = schedule; }
339 if remove_vanished
.is_some() { data.remove_vanished = remove_vanished; }
341 if !check_sync_job_modify_access(&user_info
, &auth_id
, &data
) {
342 bail
!("permission check failed");
345 config
.set_data(&id
, "sync", &data
)?
;
347 sync
::save_config(&config
)?
;
357 schema
: JOB_ID_SCHEMA
,
361 schema
: PROXMOX_CONFIG_DIGEST_SCHEMA
,
366 permission
: &Permission
::Anybody
,
367 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",
370 /// Remove a sync job configuration
371 pub fn delete_sync_job(
373 digest
: Option
<String
>,
374 rpcenv
: &mut dyn RpcEnvironment
,
375 ) -> Result
<(), Error
> {
376 let auth_id
: Authid
= rpcenv
.get_auth_id().unwrap().parse()?
;
377 let user_info
= CachedUserInfo
::new()?
;
379 let _lock
= open_file_locked(sync
::SYNC_CFG_LOCKFILE
, std
::time
::Duration
::new(10, 0), true)?
;
381 let (mut config
, expected_digest
) = sync
::config()?
;
383 if let Some(ref digest
) = digest
{
384 let digest
= proxmox
::tools
::hex_to_digest(digest
)?
;
385 crate::tools
::detect_modified_configuration_file(&digest
, &expected_digest
)?
;
388 match config
.lookup("sync", &id
) {
390 if !check_sync_job_modify_access(&user_info
, &auth_id
, &job
) {
391 bail
!("permission check failed");
393 config
.sections
.remove(&id
);
395 Err(_
) => { bail!("job '{}' does not exist
.", id) },
398 sync::save_config(&config)?;
400 crate::server::jobstate::remove_state_file("syncjob
", &id)?;
405 const ITEM_ROUTER: Router = Router::new()
406 .get(&API_METHOD_READ_SYNC_JOB)
407 .put(&API_METHOD_UPDATE_SYNC_JOB)
408 .delete(&API_METHOD_DELETE_SYNC_JOB);
410 pub const ROUTER: Router = Router::new()
411 .get(&API_METHOD_LIST_SYNC_JOBS)
412 .post(&API_METHOD_CREATE_SYNC_JOB)
413 .match_all("id
", &ITEM_ROUTER);
417 fn sync_job_access_test() -> Result<(), Error> {
418 let (user_cfg, _) = crate::config::user::test_cfg_from_str(r###"
425 "###).expect("test user
.cfg is not parsable
");
426 let acl_tree = crate::config::acl::AclTree::from_raw(r###"
427 acl
:1:/datastore
/localstore1
:read@pbs
,write@pbs
:DatastoreAudit
428 acl
:1:/datastore
/localstore1
:write@pbs
:DatastoreBackup
429 acl
:1:/datastore
/localstore2
:write@pbs
:DatastorePowerUser
430 acl
:1:/datastore
/localstore3
:write@pbs
:DatastoreAdmin
431 acl
:1:/remote
/remote1
:read@pbs
,write@pbs
:RemoteAudit
432 acl
:1:/remote
/remote1
/remotestore1
:write@pbs
:RemoteSyncOperator
433 "###).expect("test acl
.cfg is not parsable
");
435 let user_info = CachedUserInfo::test_new(user_cfg, acl_tree);
437 let root_auth_id = Authid::root_auth_id();
439 let no_perm_auth_id: Authid = "noperm@pbs
".parse()?;
440 let read_auth_id: Authid = "read@pbs
".parse()?;
441 let write_auth_id: Authid = "write@pbs
".parse()?;
443 let mut job = SyncJobConfig {
444 id: "regular
".to_string(),
445 remote: "remote0
".to_string(),
446 remote_store: "remotestore1
".to_string(),
447 store: "localstore0
".to_string(),
448 owner: Some(write_auth_id.clone()),
450 remove_vanished: None,
454 // should work without ACLs
455 assert_eq!(check_sync_job_read_access(&user_info, &root_auth_id, &job), true);
456 assert_eq!(check_sync_job_modify_access(&user_info, &root_auth_id, &job), true);
458 // user without permissions must fail
459 assert_eq!(check_sync_job_read_access(&user_info, &no_perm_auth_id, &job), false);
460 assert_eq!(check_sync_job_modify_access(&user_info, &no_perm_auth_id, &job), false);
462 // reading without proper read permissions on either remote or local must fail
463 assert_eq!(check_sync_job_read_access(&user_info, &read_auth_id, &job), false);
465 // reading without proper read permissions on local end must fail
466 job.remote = "remote1
".to_string();
467 assert_eq!(check_sync_job_read_access(&user_info, &read_auth_id, &job), false);
469 // reading without proper read permissions on remote end must fail
470 job.remote = "remote0
".to_string();
471 job.store = "localstore1
".to_string();
472 assert_eq!(check_sync_job_read_access(&user_info, &read_auth_id, &job), false);
474 // writing without proper write permissions on either end must fail
475 job.store = "localstore0
".to_string();
476 assert_eq!(check_sync_job_modify_access(&user_info, &write_auth_id, &job), false);
478 // writing without proper write permissions on local end must fail
479 job.remote = "remote1
".to_string();
481 // writing without proper write permissions on remote end must fail
482 job.remote = "remote0
".to_string();
483 job.store = "localstore1
".to_string();
484 assert_eq!(check_sync_job_modify_access(&user_info, &write_auth_id, &job), false);
486 // reset remote to one where users have access
487 job.remote = "remote1
".to_string();
489 // user with read permission can only read, but not modify/run
490 assert_eq!(check_sync_job_read_access(&user_info, &read_auth_id, &job), true);
491 job.owner = Some(read_auth_id.clone());
492 assert_eq!(check_sync_job_modify_access(&user_info, &read_auth_id, &job), false);
494 assert_eq!(check_sync_job_modify_access(&user_info, &read_auth_id, &job), false);
495 job.owner = Some(write_auth_id.clone());
496 assert_eq!(check_sync_job_modify_access(&user_info, &read_auth_id, &job), false);
498 // user with simple write permission can modify/run
499 assert_eq!(check_sync_job_read_access(&user_info, &write_auth_id, &job), true);
500 assert_eq!(check_sync_job_modify_access(&user_info, &write_auth_id, &job), true);
502 // but can't modify/run with deletion
503 job.remove_vanished = Some(true);
504 assert_eq!(check_sync_job_modify_access(&user_info, &write_auth_id, &job), false);
506 // unless they have Datastore.Prune as well
507 job.store = "localstore2
".to_string();
508 assert_eq!(check_sync_job_modify_access(&user_info, &write_auth_id, &job), true);
510 // changing owner is not possible
511 job.owner = Some(read_auth_id.clone());
512 assert_eq!(check_sync_job_modify_access(&user_info, &write_auth_id, &job), false);
514 // also not to the default 'root@pam'
516 assert_eq!(check_sync_job_modify_access(&user_info, &write_auth_id, &job), false);
518 // unless they have Datastore.Modify as well
519 job.store = "localstore3
".to_string();
520 job.owner = Some(read_auth_id.clone());
521 assert_eq!(check_sync_job_modify_access(&user_info, &write_auth_id, &job), true);
523 assert_eq!(check_sync_job_modify_access(&user_info, &write_auth_id, &job), true);