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
)?
;
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 config
.sections
.get(&sync_job
.id
).is_some() {
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
,
185 returns
: { type: sync::SyncJobConfig }
,
187 description
: "Limited to sync job entries where user has Datastore.Audit on target datastore, and Remote.Audit on source remote.",
188 permission
: &Permission
::Anybody
,
191 /// Read a sync job configuration.
192 pub fn read_sync_job(
194 mut rpcenv
: &mut dyn RpcEnvironment
,
195 ) -> Result
<SyncJobConfig
, Error
> {
196 let auth_id
: Authid
= rpcenv
.get_auth_id().unwrap().parse()?
;
197 let user_info
= CachedUserInfo
::new()?
;
199 let (config
, digest
) = sync
::config()?
;
201 let sync_job
= config
.lookup("sync", &id
)?
;
202 if !check_sync_job_read_access(&user_info
, &auth_id
, &sync_job
) {
203 bail
!("permission check failed");
206 rpcenv
["digest"] = proxmox
::tools
::digest_to_hex(&digest
).into();
212 #[derive(Serialize, Deserialize)]
213 #[serde(rename_all="kebab-case")]
214 #[allow(non_camel_case_types)]
215 /// Deletable property name
216 pub enum DeletableProperty
{
217 /// Delete the owner property.
219 /// Delete the comment property.
221 /// Delete the job schedule.
223 /// Delete the remove-vanished flag.
232 schema
: JOB_ID_SCHEMA
,
235 schema
: DATASTORE_SCHEMA
,
243 schema
: REMOTE_ID_SCHEMA
,
247 schema
: DATASTORE_SCHEMA
,
251 schema
: REMOVE_VANISHED_BACKUPS_SCHEMA
,
256 schema
: SINGLE_LINE_COMMENT_SCHEMA
,
260 schema
: SYNC_SCHEDULE_SCHEMA
,
263 description
: "List of properties to delete.",
267 type: DeletableProperty
,
272 schema
: PROXMOX_CONFIG_DIGEST_SCHEMA
,
277 permission
: &Permission
::Anybody
,
278 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",
281 /// Update sync job config.
282 #[allow(clippy::too_many_arguments)]
283 pub fn update_sync_job(
285 store
: Option
<String
>,
286 owner
: Option
<Authid
>,
287 remote
: Option
<String
>,
288 remote_store
: Option
<String
>,
289 remove_vanished
: Option
<bool
>,
290 comment
: Option
<String
>,
291 schedule
: Option
<String
>,
292 delete
: Option
<Vec
<DeletableProperty
>>,
293 digest
: Option
<String
>,
294 rpcenv
: &mut dyn RpcEnvironment
,
295 ) -> Result
<(), Error
> {
296 let auth_id
: Authid
= rpcenv
.get_auth_id().unwrap().parse()?
;
297 let user_info
= CachedUserInfo
::new()?
;
299 let _lock
= open_file_locked(sync
::SYNC_CFG_LOCKFILE
, std
::time
::Duration
::new(10, 0), true)?
;
301 // pass/compare digest
302 let (mut config
, expected_digest
) = sync
::config()?
;
304 if let Some(ref digest
) = digest
{
305 let digest
= proxmox
::tools
::hex_to_digest(digest
)?
;
306 crate::tools
::detect_modified_configuration_file(&digest
, &expected_digest
)?
;
309 let mut data
: sync
::SyncJobConfig
= config
.lookup("sync", &id
)?
;
311 if let Some(delete
) = delete
{
312 for delete_prop
in delete
{
314 DeletableProperty
::owner
=> { data.owner = None; }
,
315 DeletableProperty
::comment
=> { data.comment = None; }
,
316 DeletableProperty
::schedule
=> { data.schedule = None; }
,
317 DeletableProperty
::remove_vanished
=> { data.remove_vanished = None; }
,
322 if let Some(comment
) = comment
{
323 let comment
= comment
.trim().to_string();
324 if comment
.is_empty() {
327 data
.comment
= Some(comment
);
331 if let Some(store
) = store { data.store = store; }
332 if let Some(remote
) = remote { data.remote = remote; }
333 if let Some(remote_store
) = remote_store { data.remote_store = remote_store; }
334 if let Some(owner
) = owner { data.owner = Some(owner); }
336 if schedule
.is_some() { data.schedule = schedule; }
337 if remove_vanished
.is_some() { data.remove_vanished = remove_vanished; }
339 if !check_sync_job_modify_access(&user_info
, &auth_id
, &data
) {
340 bail
!("permission check failed");
343 config
.set_data(&id
, "sync", &data
)?
;
345 sync
::save_config(&config
)?
;
355 schema
: JOB_ID_SCHEMA
,
359 schema
: PROXMOX_CONFIG_DIGEST_SCHEMA
,
364 permission
: &Permission
::Anybody
,
365 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",
368 /// Remove a sync job configuration
369 pub fn delete_sync_job(
371 digest
: Option
<String
>,
372 rpcenv
: &mut dyn RpcEnvironment
,
373 ) -> Result
<(), Error
> {
374 let auth_id
: Authid
= rpcenv
.get_auth_id().unwrap().parse()?
;
375 let user_info
= CachedUserInfo
::new()?
;
377 let _lock
= open_file_locked(sync
::SYNC_CFG_LOCKFILE
, std
::time
::Duration
::new(10, 0), true)?
;
379 let (mut config
, expected_digest
) = sync
::config()?
;
381 if let Some(ref digest
) = digest
{
382 let digest
= proxmox
::tools
::hex_to_digest(digest
)?
;
383 crate::tools
::detect_modified_configuration_file(&digest
, &expected_digest
)?
;
386 match config
.lookup("sync", &id
) {
388 if !check_sync_job_modify_access(&user_info
, &auth_id
, &job
) {
389 bail
!("permission check failed");
391 config
.sections
.remove(&id
);
393 Err(_
) => { bail!("job '{}' does not exist
.", id) },
396 sync::save_config(&config)?;
398 crate::server::jobstate::remove_state_file("syncjob
", &id)?;
403 const ITEM_ROUTER: Router = Router::new()
404 .get(&API_METHOD_READ_SYNC_JOB)
405 .put(&API_METHOD_UPDATE_SYNC_JOB)
406 .delete(&API_METHOD_DELETE_SYNC_JOB);
408 pub const ROUTER: Router = Router::new()
409 .get(&API_METHOD_LIST_SYNC_JOBS)
410 .post(&API_METHOD_CREATE_SYNC_JOB)
411 .match_all("id
", &ITEM_ROUTER);
415 fn sync_job_access_test() -> Result<(), Error> {
416 let (user_cfg, _) = crate::config::user::test_cfg_from_str(r###"
423 "###).expect("test user
.cfg is not parsable
");
424 let acl_tree = crate::config::acl::AclTree::from_raw(r###"
425 acl
:1:/datastore
/localstore1
:read@pbs
,write@pbs
:DatastoreAudit
426 acl
:1:/datastore
/localstore1
:write@pbs
:DatastoreBackup
427 acl
:1:/datastore
/localstore2
:write@pbs
:DatastorePowerUser
428 acl
:1:/datastore
/localstore3
:write@pbs
:DatastoreAdmin
429 acl
:1:/remote
/remote1
:read@pbs
,write@pbs
:RemoteAudit
430 acl
:1:/remote
/remote1
/remotestore1
:write@pbs
:RemoteSyncOperator
431 "###).expect("test acl
.cfg is not parsable
");
433 let user_info = CachedUserInfo::test_new(user_cfg, acl_tree);
435 let root_auth_id = Authid::root_auth_id();
437 let no_perm_auth_id: Authid = "noperm@pbs
".parse()?;
438 let read_auth_id: Authid = "read@pbs
".parse()?;
439 let write_auth_id: Authid = "write@pbs
".parse()?;
441 let mut job = SyncJobConfig {
442 id: "regular
".to_string(),
443 remote: "remote0
".to_string(),
444 remote_store: "remotestore1
".to_string(),
445 store: "localstore0
".to_string(),
446 owner: Some(write_auth_id.clone()),
448 remove_vanished: None,
452 // should work without ACLs
453 assert_eq!(check_sync_job_read_access(&user_info, &root_auth_id, &job), true);
454 assert_eq!(check_sync_job_modify_access(&user_info, &root_auth_id, &job), true);
456 // user without permissions must fail
457 assert_eq!(check_sync_job_read_access(&user_info, &no_perm_auth_id, &job), false);
458 assert_eq!(check_sync_job_modify_access(&user_info, &no_perm_auth_id, &job), false);
460 // reading without proper read permissions on either remote or local must fail
461 assert_eq!(check_sync_job_read_access(&user_info, &read_auth_id, &job), false);
463 // reading without proper read permissions on local end must fail
464 job.remote = "remote1
".to_string();
465 assert_eq!(check_sync_job_read_access(&user_info, &read_auth_id, &job), false);
467 // reading without proper read permissions on remote end must fail
468 job.remote = "remote0
".to_string();
469 job.store = "localstore1
".to_string();
470 assert_eq!(check_sync_job_read_access(&user_info, &read_auth_id, &job), false);
472 // writing without proper write permissions on either end must fail
473 job.store = "localstore0
".to_string();
474 assert_eq!(check_sync_job_modify_access(&user_info, &write_auth_id, &job), false);
476 // writing without proper write permissions on local end must fail
477 job.remote = "remote1
".to_string();
479 // writing without proper write permissions on remote end must fail
480 job.remote = "remote0
".to_string();
481 job.store = "localstore1
".to_string();
482 assert_eq!(check_sync_job_modify_access(&user_info, &write_auth_id, &job), false);
484 // reset remote to one where users have access
485 job.remote = "remote1
".to_string();
487 // user with read permission can only read, but not modify/run
488 assert_eq!(check_sync_job_read_access(&user_info, &read_auth_id, &job), true);
489 job.owner = Some(read_auth_id.clone());
490 assert_eq!(check_sync_job_modify_access(&user_info, &read_auth_id, &job), false);
492 assert_eq!(check_sync_job_modify_access(&user_info, &read_auth_id, &job), false);
493 job.owner = Some(write_auth_id.clone());
494 assert_eq!(check_sync_job_modify_access(&user_info, &read_auth_id, &job), false);
496 // user with simple write permission can modify/run
497 assert_eq!(check_sync_job_read_access(&user_info, &write_auth_id, &job), true);
498 assert_eq!(check_sync_job_modify_access(&user_info, &write_auth_id, &job), true);
500 // but can't modify/run with deletion
501 job.remove_vanished = Some(true);
502 assert_eq!(check_sync_job_modify_access(&user_info, &write_auth_id, &job), false);
504 // unless they have Datastore.Prune as well
505 job.store = "localstore2
".to_string();
506 assert_eq!(check_sync_job_modify_access(&user_info, &write_auth_id, &job), true);
508 // changing owner is not possible
509 job.owner = Some(read_auth_id.clone());
510 assert_eq!(check_sync_job_modify_access(&user_info, &write_auth_id, &job), false);
512 // also not to the default 'root@pam'
514 assert_eq!(check_sync_job_modify_access(&user_info, &write_auth_id, &job), false);
516 // unless they have Datastore.Modify as well
517 job.store = "localstore3
".to_string();
518 job.owner = Some(read_auth_id);
519 assert_eq!(check_sync_job_modify_access(&user_info, &write_auth_id, &job), true);
521 assert_eq!(check_sync_job_modify_access(&user_info, &write_auth_id, &job), true);