1 use anyhow
::{bail, Error}
;
3 use ::serde
::{Deserialize, Serialize}
;
6 use proxmox_router
::{http_bail, Router, RpcEnvironment, Permission}
;
7 use proxmox_schema
::{api, param_bail}
;
10 Authid
, SyncJobConfig
, SyncJobConfigUpdater
, JOB_ID_SCHEMA
, PROXMOX_CONFIG_DIGEST_SCHEMA
,
11 PRIV_DATASTORE_AUDIT
, PRIV_DATASTORE_BACKUP
, PRIV_DATASTORE_MODIFY
, PRIV_DATASTORE_PRUNE
,
12 PRIV_REMOTE_AUDIT
, PRIV_REMOTE_READ
,
16 use pbs_config
::CachedUserInfo
;
18 pub fn check_sync_job_read_access(
19 user_info
: &CachedUserInfo
,
23 let datastore_privs
= user_info
.lookup_privs(auth_id
, &["datastore", &job
.store
]);
24 if datastore_privs
& PRIV_DATASTORE_AUDIT
== 0 {
28 let remote_privs
= user_info
.lookup_privs(auth_id
, &["remote", &job
.remote
]);
29 remote_privs
& PRIV_REMOTE_AUDIT
!= 0
32 // user can run the corresponding pull job
33 pub fn check_sync_job_modify_access(
34 user_info
: &CachedUserInfo
,
38 let datastore_privs
= user_info
.lookup_privs(auth_id
, &["datastore", &job
.store
]);
39 if datastore_privs
& PRIV_DATASTORE_BACKUP
== 0 {
43 if let Some(true) = job
.remove_vanished
{
44 if datastore_privs
& PRIV_DATASTORE_PRUNE
== 0 {
49 let correct_owner
= match job
.owner
{
53 && !auth_id
.is_token()
54 && owner
.user() == auth_id
.user())
57 None
=> auth_id
== Authid
::root_auth_id(),
60 // same permission as changing ownership after syncing
61 if !correct_owner
&& datastore_privs
& PRIV_DATASTORE_MODIFY
== 0 {
65 let remote_privs
= user_info
.lookup_privs(auth_id
, &["remote", &job
.remote
, &job
.remote_store
]);
66 remote_privs
& PRIV_REMOTE_READ
!= 0
74 description
: "List configured jobs.",
76 items
: { type: SyncJobConfig }
,
79 description
: "Limited to sync job entries where user has Datastore.Audit on target datastore, and Remote.Audit on source remote.",
80 permission
: &Permission
::Anybody
,
83 /// List all sync jobs
84 pub fn list_sync_jobs(
86 mut rpcenv
: &mut dyn RpcEnvironment
,
87 ) -> Result
<Vec
<SyncJobConfig
>, Error
> {
88 let auth_id
: Authid
= rpcenv
.get_auth_id().unwrap().parse()?
;
89 let user_info
= CachedUserInfo
::new()?
;
91 let (config
, digest
) = sync
::config()?
;
93 let list
= config
.convert_to_typed_array("sync")?
;
95 rpcenv
["digest"] = hex
::encode(&digest
).into();
99 .filter(|sync_job
| check_sync_job_read_access(&user_info
, &auth_id
, sync_job
))
115 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",
116 permission
: &Permission
::Anybody
,
119 /// Create a new sync job.
120 pub fn create_sync_job(
121 config
: SyncJobConfig
,
122 rpcenv
: &mut dyn RpcEnvironment
,
123 ) -> Result
<(), Error
> {
124 let auth_id
: Authid
= rpcenv
.get_auth_id().unwrap().parse()?
;
125 let user_info
= CachedUserInfo
::new()?
;
127 let _lock
= sync
::lock_config()?
;
129 if !check_sync_job_modify_access(&user_info
, &auth_id
, &config
) {
130 bail
!("permission check failed");
133 let (mut section_config
, _digest
) = sync
::config()?
;
135 if section_config
.sections
.get(&config
.id
).is_some() {
136 param_bail
!("id", "job '{}' already exists.", config
.id
);
139 section_config
.set_data(&config
.id
, "sync", &config
)?
;
141 sync
::save_config(§ion_config
)?
;
143 crate::server
::jobstate
::create_state_file("syncjob", &config
.id
)?
;
152 schema
: JOB_ID_SCHEMA
,
156 returns
: { type: SyncJobConfig }
,
158 description
: "Limited to sync job entries where user has Datastore.Audit on target datastore, and Remote.Audit on source remote.",
159 permission
: &Permission
::Anybody
,
162 /// Read a sync job configuration.
163 pub fn read_sync_job(
165 mut rpcenv
: &mut dyn RpcEnvironment
,
166 ) -> Result
<SyncJobConfig
, Error
> {
167 let auth_id
: Authid
= rpcenv
.get_auth_id().unwrap().parse()?
;
168 let user_info
= CachedUserInfo
::new()?
;
170 let (config
, digest
) = sync
::config()?
;
172 let sync_job
= config
.lookup("sync", &id
)?
;
173 if !check_sync_job_read_access(&user_info
, &auth_id
, &sync_job
) {
174 bail
!("permission check failed");
177 rpcenv
["digest"] = hex
::encode(&digest
).into();
183 #[derive(Serialize, Deserialize)]
184 #[serde(rename_all="kebab-case")]
185 #[allow(non_camel_case_types)]
186 /// Deletable property name
187 pub enum DeletableProperty
{
188 /// Delete the owner property.
190 /// Delete the comment property.
192 /// Delete the job schedule.
194 /// Delete the remove-vanished flag.
196 /// Delete the group_filter property.
198 /// Delete the rate_in property.
200 /// Delete the burst_in property.
202 /// Delete the rate_out property.
204 /// Delete the burst_out property.
213 schema
: JOB_ID_SCHEMA
,
216 type: SyncJobConfigUpdater
,
220 description
: "List of properties to delete.",
224 type: DeletableProperty
,
229 schema
: PROXMOX_CONFIG_DIGEST_SCHEMA
,
234 permission
: &Permission
::Anybody
,
235 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",
238 /// Update sync job config.
239 #[allow(clippy::too_many_arguments)]
240 pub fn update_sync_job(
242 update
: SyncJobConfigUpdater
,
243 delete
: Option
<Vec
<DeletableProperty
>>,
244 digest
: Option
<String
>,
245 rpcenv
: &mut dyn RpcEnvironment
,
246 ) -> Result
<(), Error
> {
247 let auth_id
: Authid
= rpcenv
.get_auth_id().unwrap().parse()?
;
248 let user_info
= CachedUserInfo
::new()?
;
250 let _lock
= sync
::lock_config()?
;
252 let (mut config
, expected_digest
) = sync
::config()?
;
254 if let Some(ref digest
) = digest
{
255 let digest
= <[u8; 32]>::from_hex(digest
)?
;
256 crate::tools
::detect_modified_configuration_file(&digest
, &expected_digest
)?
;
259 let mut data
: SyncJobConfig
= config
.lookup("sync", &id
)?
;
261 if let Some(delete
) = delete
{
262 for delete_prop
in delete
{
264 DeletableProperty
::owner
=> { data.owner = None; }
,
265 DeletableProperty
::comment
=> { data.comment = None; }
,
266 DeletableProperty
::schedule
=> { data.schedule = None; }
,
267 DeletableProperty
::remove_vanished
=> { data.remove_vanished = None; }
,
268 DeletableProperty
::group_filter
=> { data.group_filter = None; }
,
269 DeletableProperty
::rate_in
=> { data.limit.rate_in = None; }
,
270 DeletableProperty
::rate_out
=> { data.limit.rate_out = None; }
,
271 DeletableProperty
::burst_in
=> { data.limit.burst_in = None; }
,
272 DeletableProperty
::burst_out
=> { data.limit.burst_out = None; }
,
277 if let Some(comment
) = update
.comment
{
278 let comment
= comment
.trim().to_string();
279 if comment
.is_empty() {
282 data
.comment
= Some(comment
);
286 if let Some(store
) = update
.store { data.store = store; }
287 if let Some(remote
) = update
.remote { data.remote = remote; }
288 if let Some(remote_store
) = update
.remote_store { data.remote_store = remote_store; }
289 if let Some(owner
) = update
.owner { data.owner = Some(owner); }
290 if let Some(group_filter
) = update
.group_filter { data.group_filter = Some(group_filter); }
292 if update
.limit
.rate_in
.is_some() {
293 data
.limit
.rate_in
= update
.limit
.rate_in
;
296 if update
.limit
.rate_out
.is_some() {
297 data
.limit
.rate_out
= update
.limit
.rate_out
;
300 if update
.limit
.burst_in
.is_some() {
301 data
.limit
.burst_in
= update
.limit
.burst_in
;
304 if update
.limit
.burst_out
.is_some() {
305 data
.limit
.burst_out
= update
.limit
.burst_out
;
308 let schedule_changed
= data
.schedule
!= update
.schedule
;
309 if update
.schedule
.is_some() { data.schedule = update.schedule; }
310 if update
.remove_vanished
.is_some() { data.remove_vanished = update.remove_vanished; }
312 if !check_sync_job_modify_access(&user_info
, &auth_id
, &data
) {
313 bail
!("permission check failed");
316 config
.set_data(&id
, "sync", &data
)?
;
318 sync
::save_config(&config
)?
;
320 if schedule_changed
{
321 crate::server
::jobstate
::update_job_last_run_time("syncjob", &id
)?
;
332 schema
: JOB_ID_SCHEMA
,
336 schema
: PROXMOX_CONFIG_DIGEST_SCHEMA
,
341 permission
: &Permission
::Anybody
,
342 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",
345 /// Remove a sync job configuration
346 pub fn delete_sync_job(
348 digest
: Option
<String
>,
349 rpcenv
: &mut dyn RpcEnvironment
,
350 ) -> Result
<(), Error
> {
351 let auth_id
: Authid
= rpcenv
.get_auth_id().unwrap().parse()?
;
352 let user_info
= CachedUserInfo
::new()?
;
354 let _lock
= sync
::lock_config()?
;
356 let (mut config
, expected_digest
) = sync
::config()?
;
358 if let Some(ref digest
) = digest
{
359 let digest
= <[u8; 32]>::from_hex(digest
)?
;
360 crate::tools
::detect_modified_configuration_file(&digest
, &expected_digest
)?
;
363 match config
.lookup("sync", &id
) {
365 if !check_sync_job_modify_access(&user_info
, &auth_id
, &job
) {
366 bail
!("permission check failed");
368 config
.sections
.remove(&id
);
370 Err(_
) => { http_bail!(NOT_FOUND, "job '{}' does not exist
.", id) },
373 sync::save_config(&config)?;
375 crate::server::jobstate::remove_state_file("syncjob
", &id)?;
380 const ITEM_ROUTER: Router = Router::new()
381 .get(&API_METHOD_READ_SYNC_JOB)
382 .put(&API_METHOD_UPDATE_SYNC_JOB)
383 .delete(&API_METHOD_DELETE_SYNC_JOB);
385 pub const ROUTER: Router = Router::new()
386 .get(&API_METHOD_LIST_SYNC_JOBS)
387 .post(&API_METHOD_CREATE_SYNC_JOB)
388 .match_all("id
", &ITEM_ROUTER);
392 fn sync_job_access_test() -> Result<(), Error> {
393 let (user_cfg, _) = pbs_config::user::test_cfg_from_str(r###"
400 "###).expect("test user
.cfg is not parsable
");
401 let acl_tree = pbs_config::acl::AclTree::from_raw(r###"
402 acl
:1:/datastore
/localstore1
:read@pbs
,write@pbs
:DatastoreAudit
403 acl
:1:/datastore
/localstore1
:write@pbs
:DatastoreBackup
404 acl
:1:/datastore
/localstore2
:write@pbs
:DatastorePowerUser
405 acl
:1:/datastore
/localstore3
:write@pbs
:DatastoreAdmin
406 acl
:1:/remote
/remote1
:read@pbs
,write@pbs
:RemoteAudit
407 acl
:1:/remote
/remote1
/remotestore1
:write@pbs
:RemoteSyncOperator
408 "###).expect("test acl
.cfg is not parsable
");
410 let user_info = CachedUserInfo::test_new(user_cfg, acl_tree);
412 let root_auth_id = Authid::root_auth_id();
414 let no_perm_auth_id: Authid = "noperm@pbs
".parse()?;
415 let read_auth_id: Authid = "read@pbs
".parse()?;
416 let write_auth_id: Authid = "write@pbs
".parse()?;
418 let mut job = SyncJobConfig {
419 id: "regular
".to_string(),
420 remote: "remote0
".to_string(),
421 remote_store: "remotestore1
".to_string(),
422 store: "localstore0
".to_string(),
423 owner: Some(write_auth_id.clone()),
425 remove_vanished: None,
428 limit: pbs_api_types::RateLimitConfig::default(), // no limit
431 // should work without ACLs
432 assert_eq!(check_sync_job_read_access(&user_info, root_auth_id, &job), true);
433 assert_eq!(check_sync_job_modify_access(&user_info, root_auth_id, &job), true);
435 // user without permissions must fail
436 assert_eq!(check_sync_job_read_access(&user_info, &no_perm_auth_id, &job), false);
437 assert_eq!(check_sync_job_modify_access(&user_info, &no_perm_auth_id, &job), false);
439 // reading without proper read permissions on either remote or local must fail
440 assert_eq!(check_sync_job_read_access(&user_info, &read_auth_id, &job), false);
442 // reading without proper read permissions on local end must fail
443 job.remote = "remote1
".to_string();
444 assert_eq!(check_sync_job_read_access(&user_info, &read_auth_id, &job), false);
446 // reading without proper read permissions on remote end must fail
447 job.remote = "remote0
".to_string();
448 job.store = "localstore1
".to_string();
449 assert_eq!(check_sync_job_read_access(&user_info, &read_auth_id, &job), false);
451 // writing without proper write permissions on either end must fail
452 job.store = "localstore0
".to_string();
453 assert_eq!(check_sync_job_modify_access(&user_info, &write_auth_id, &job), false);
455 // writing without proper write permissions on local end must fail
456 job.remote = "remote1
".to_string();
458 // writing without proper write permissions on remote end must fail
459 job.remote = "remote0
".to_string();
460 job.store = "localstore1
".to_string();
461 assert_eq!(check_sync_job_modify_access(&user_info, &write_auth_id, &job), false);
463 // reset remote to one where users have access
464 job.remote = "remote1
".to_string();
466 // user with read permission can only read, but not modify/run
467 assert_eq!(check_sync_job_read_access(&user_info, &read_auth_id, &job), true);
468 job.owner = Some(read_auth_id.clone());
469 assert_eq!(check_sync_job_modify_access(&user_info, &read_auth_id, &job), false);
471 assert_eq!(check_sync_job_modify_access(&user_info, &read_auth_id, &job), false);
472 job.owner = Some(write_auth_id.clone());
473 assert_eq!(check_sync_job_modify_access(&user_info, &read_auth_id, &job), false);
475 // user with simple write permission can modify/run
476 assert_eq!(check_sync_job_read_access(&user_info, &write_auth_id, &job), true);
477 assert_eq!(check_sync_job_modify_access(&user_info, &write_auth_id, &job), true);
479 // but can't modify/run with deletion
480 job.remove_vanished = Some(true);
481 assert_eq!(check_sync_job_modify_access(&user_info, &write_auth_id, &job), false);
483 // unless they have Datastore.Prune as well
484 job.store = "localstore2
".to_string();
485 assert_eq!(check_sync_job_modify_access(&user_info, &write_auth_id, &job), true);
487 // changing owner is not possible
488 job.owner = Some(read_auth_id.clone());
489 assert_eq!(check_sync_job_modify_access(&user_info, &write_auth_id, &job), false);
491 // also not to the default 'root@pam'
493 assert_eq!(check_sync_job_modify_access(&user_info, &write_auth_id, &job), false);
495 // unless they have Datastore.Modify as well
496 job.store = "localstore3
".to_string();
497 job.owner = Some(read_auth_id);
498 assert_eq!(check_sync_job_modify_access(&user_info, &write_auth_id, &job), true);
500 assert_eq!(check_sync_job_modify_access(&user_info, &write_auth_id, &job), true);