1 use ::serde
::{Deserialize, Serialize}
;
2 use anyhow
::{bail, Error}
;
6 use proxmox_router
::{http_bail, Permission, Router, RpcEnvironment}
;
7 use proxmox_schema
::{api, param_bail}
;
10 Authid
, SyncJobConfig
, SyncJobConfigUpdater
, JOB_ID_SCHEMA
, PRIV_DATASTORE_AUDIT
,
11 PRIV_DATASTORE_BACKUP
, PRIV_DATASTORE_MODIFY
, PRIV_DATASTORE_PRUNE
, PRIV_REMOTE_AUDIT
,
12 PRIV_REMOTE_READ
, PROXMOX_CONFIG_DIGEST_SCHEMA
,
16 use pbs_config
::CachedUserInfo
;
18 pub fn check_sync_job_read_access(
19 user_info
: &CachedUserInfo
,
23 let ns_anchor_privs
= user_info
.lookup_privs(auth_id
, &job
.acl_path());
24 if ns_anchor_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 /// checks whether user can run the corresponding pull job
34 /// namespace creation/deletion ACL and backup group ownership checks happen in the pull code directly.
35 /// remote side checks/filters remote datastore/namespace/group access.
36 pub fn check_sync_job_modify_access(
37 user_info
: &CachedUserInfo
,
41 let ns_anchor_privs
= user_info
.lookup_privs(auth_id
, &job
.acl_path());
42 if ns_anchor_privs
& PRIV_DATASTORE_BACKUP
== 0 {
46 if let Some(true) = job
.remove_vanished
{
47 if ns_anchor_privs
& PRIV_DATASTORE_PRUNE
== 0 {
52 let correct_owner
= match job
.owner
{
55 || (owner
.is_token() && !auth_id
.is_token() && owner
.user() == auth_id
.user())
58 None
=> auth_id
== Authid
::root_auth_id(),
61 // same permission as changing ownership after syncing
62 if !correct_owner
&& ns_anchor_privs
& PRIV_DATASTORE_MODIFY
== 0 {
66 let remote_privs
= user_info
.lookup_privs(auth_id
, &["remote", &job
.remote
, &job
.remote_store
]);
67 remote_privs
& PRIV_REMOTE_READ
!= 0
75 description
: "List configured jobs.",
77 items
: { type: SyncJobConfig }
,
80 description
: "Limited to sync job entries where user has Datastore.Audit on target datastore, and Remote.Audit on source remote.",
81 permission
: &Permission
::Anybody
,
84 /// List all sync jobs
85 pub fn list_sync_jobs(
87 rpcenv
: &mut dyn RpcEnvironment
,
88 ) -> Result
<Vec
<SyncJobConfig
>, Error
> {
89 let auth_id
: Authid
= rpcenv
.get_auth_id().unwrap().parse()?
;
90 let user_info
= CachedUserInfo
::new()?
;
92 let (config
, digest
) = sync
::config()?
;
94 let list
= config
.convert_to_typed_array("sync")?
;
96 rpcenv
["digest"] = hex
::encode(digest
).into();
100 .filter(|sync_job
| check_sync_job_read_access(&user_info
, &auth_id
, sync_job
))
116 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",
117 permission
: &Permission
::Anybody
,
120 /// Create a new sync job.
121 pub fn create_sync_job(
122 config
: SyncJobConfig
,
123 rpcenv
: &mut dyn RpcEnvironment
,
124 ) -> Result
<(), Error
> {
125 let auth_id
: Authid
= rpcenv
.get_auth_id().unwrap().parse()?
;
126 let user_info
= CachedUserInfo
::new()?
;
128 let _lock
= sync
::lock_config()?
;
130 if !check_sync_job_modify_access(&user_info
, &auth_id
, &config
) {
131 bail
!("permission check failed");
134 if let Some(max_depth
) = config
.max_depth
{
135 if let Some(ref ns
) = config
.ns
{
136 ns
.check_max_depth(max_depth
)?
;
138 if let Some(ref ns
) = config
.remote_ns
{
139 ns
.check_max_depth(max_depth
)?
;
143 let (mut section_config
, _digest
) = sync
::config()?
;
145 if section_config
.sections
.get(&config
.id
).is_some() {
146 param_bail
!("id", "job '{}' already exists.", config
.id
);
149 section_config
.set_data(&config
.id
, "sync", &config
)?
;
151 sync
::save_config(§ion_config
)?
;
153 crate::server
::jobstate
::create_state_file("syncjob", &config
.id
)?
;
162 schema
: JOB_ID_SCHEMA
,
166 returns
: { type: SyncJobConfig }
,
168 description
: "Limited to sync job entries where user has Datastore.Audit on target datastore, and Remote.Audit on source remote.",
169 permission
: &Permission
::Anybody
,
172 /// Read a sync job configuration.
173 pub fn read_sync_job(id
: String
, rpcenv
: &mut dyn RpcEnvironment
) -> Result
<SyncJobConfig
, Error
> {
174 let auth_id
: Authid
= rpcenv
.get_auth_id().unwrap().parse()?
;
175 let user_info
= CachedUserInfo
::new()?
;
177 let (config
, digest
) = sync
::config()?
;
179 let sync_job
= config
.lookup("sync", &id
)?
;
180 if !check_sync_job_read_access(&user_info
, &auth_id
, &sync_job
) {
181 bail
!("permission check failed");
184 rpcenv
["digest"] = hex
::encode(digest
).into();
190 #[derive(Serialize, Deserialize)]
191 #[serde(rename_all = "kebab-case")]
192 /// Deletable property name
193 pub enum DeletableProperty
{
194 /// Delete the owner property.
196 /// Delete the comment property.
198 /// Delete the job schedule.
200 /// Delete the remove-vanished flag.
202 /// Delete the group_filter property.
204 /// Delete the rate_in property.
206 /// Delete the burst_in property.
208 /// Delete the rate_out property.
210 /// Delete the burst_out property.
212 /// Delete the ns property,
214 /// Delete the remote_ns property,
216 /// Delete the max_depth property,
225 schema
: JOB_ID_SCHEMA
,
228 type: SyncJobConfigUpdater
,
232 description
: "List of properties to delete.",
236 type: DeletableProperty
,
241 schema
: PROXMOX_CONFIG_DIGEST_SCHEMA
,
246 permission
: &Permission
::Anybody
,
247 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",
250 /// Update sync job config.
251 #[allow(clippy::too_many_arguments)]
252 pub fn update_sync_job(
254 update
: SyncJobConfigUpdater
,
255 delete
: Option
<Vec
<DeletableProperty
>>,
256 digest
: Option
<String
>,
257 rpcenv
: &mut dyn RpcEnvironment
,
258 ) -> Result
<(), Error
> {
259 let auth_id
: Authid
= rpcenv
.get_auth_id().unwrap().parse()?
;
260 let user_info
= CachedUserInfo
::new()?
;
262 let _lock
= sync
::lock_config()?
;
264 let (mut config
, expected_digest
) = sync
::config()?
;
266 if let Some(ref digest
) = digest
{
267 let digest
= <[u8; 32]>::from_hex(digest
)?
;
268 crate::tools
::detect_modified_configuration_file(&digest
, &expected_digest
)?
;
271 let mut data
: SyncJobConfig
= config
.lookup("sync", &id
)?
;
273 if let Some(delete
) = delete
{
274 for delete_prop
in delete
{
276 DeletableProperty
::Owner
=> {
279 DeletableProperty
::Comment
=> {
282 DeletableProperty
::Schedule
=> {
283 data
.schedule
= None
;
285 DeletableProperty
::RemoveVanished
=> {
286 data
.remove_vanished
= None
;
288 DeletableProperty
::GroupFilter
=> {
289 data
.group_filter
= None
;
291 DeletableProperty
::RateIn
=> {
292 data
.limit
.rate_in
= None
;
294 DeletableProperty
::RateOut
=> {
295 data
.limit
.rate_out
= None
;
297 DeletableProperty
::BurstIn
=> {
298 data
.limit
.burst_in
= None
;
300 DeletableProperty
::BurstOut
=> {
301 data
.limit
.burst_out
= None
;
303 DeletableProperty
::Ns
=> {
306 DeletableProperty
::RemoteNs
=> {
307 data
.remote_ns
= None
;
309 DeletableProperty
::MaxDepth
=> {
310 data
.max_depth
= None
;
316 if let Some(comment
) = update
.comment
{
317 let comment
= comment
.trim().to_string();
318 if comment
.is_empty() {
321 data
.comment
= Some(comment
);
325 if let Some(store
) = update
.store
{
328 if let Some(ns
) = update
.ns
{
331 if let Some(remote
) = update
.remote
{
332 data
.remote
= remote
;
334 if let Some(remote_store
) = update
.remote_store
{
335 data
.remote_store
= remote_store
;
337 if let Some(remote_ns
) = update
.remote_ns
{
338 data
.remote_ns
= Some(remote_ns
);
340 if let Some(owner
) = update
.owner
{
341 data
.owner
= Some(owner
);
343 if let Some(group_filter
) = update
.group_filter
{
344 data
.group_filter
= Some(group_filter
);
347 if update
.limit
.rate_in
.is_some() {
348 data
.limit
.rate_in
= update
.limit
.rate_in
;
351 if update
.limit
.rate_out
.is_some() {
352 data
.limit
.rate_out
= update
.limit
.rate_out
;
355 if update
.limit
.burst_in
.is_some() {
356 data
.limit
.burst_in
= update
.limit
.burst_in
;
359 if update
.limit
.burst_out
.is_some() {
360 data
.limit
.burst_out
= update
.limit
.burst_out
;
363 let schedule_changed
= data
.schedule
!= update
.schedule
;
364 if update
.schedule
.is_some() {
365 data
.schedule
= update
.schedule
;
367 if update
.remove_vanished
.is_some() {
368 data
.remove_vanished
= update
.remove_vanished
;
370 if let Some(max_depth
) = update
.max_depth
{
371 data
.max_depth
= Some(max_depth
);
374 if let Some(max_depth
) = data
.max_depth
{
375 if let Some(ref ns
) = data
.ns
{
376 ns
.check_max_depth(max_depth
)?
;
378 if let Some(ref ns
) = data
.remote_ns
{
379 ns
.check_max_depth(max_depth
)?
;
383 if !check_sync_job_modify_access(&user_info
, &auth_id
, &data
) {
384 bail
!("permission check failed");
387 config
.set_data(&id
, "sync", &data
)?
;
389 sync
::save_config(&config
)?
;
391 if schedule_changed
{
392 crate::server
::jobstate
::update_job_last_run_time("syncjob", &id
)?
;
403 schema
: JOB_ID_SCHEMA
,
407 schema
: PROXMOX_CONFIG_DIGEST_SCHEMA
,
412 permission
: &Permission
::Anybody
,
413 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",
416 /// Remove a sync job configuration
417 pub fn delete_sync_job(
419 digest
: Option
<String
>,
420 rpcenv
: &mut dyn RpcEnvironment
,
421 ) -> Result
<(), Error
> {
422 let auth_id
: Authid
= rpcenv
.get_auth_id().unwrap().parse()?
;
423 let user_info
= CachedUserInfo
::new()?
;
425 let _lock
= sync
::lock_config()?
;
427 let (mut config
, expected_digest
) = sync
::config()?
;
429 if let Some(ref digest
) = digest
{
430 let digest
= <[u8; 32]>::from_hex(digest
)?
;
431 crate::tools
::detect_modified_configuration_file(&digest
, &expected_digest
)?
;
434 match config
.lookup("sync", &id
) {
436 if !check_sync_job_modify_access(&user_info
, &auth_id
, &job
) {
437 bail
!("permission check failed");
439 config
.sections
.remove(&id
);
442 http_bail
!(NOT_FOUND
, "job '{}' does not exist.", id
)
446 sync
::save_config(&config
)?
;
448 crate::server
::jobstate
::remove_state_file("syncjob", &id
)?
;
453 const ITEM_ROUTER
: Router
= Router
::new()
454 .get(&API_METHOD_READ_SYNC_JOB
)
455 .put(&API_METHOD_UPDATE_SYNC_JOB
)
456 .delete(&API_METHOD_DELETE_SYNC_JOB
);
458 pub const ROUTER
: Router
= Router
::new()
459 .get(&API_METHOD_LIST_SYNC_JOBS
)
460 .post(&API_METHOD_CREATE_SYNC_JOB
)
461 .match_all("id", &ITEM_ROUTER
);
464 fn sync_job_access_test() -> Result
<(), Error
> {
465 let (user_cfg
, _
) = pbs_config
::user
::test_cfg_from_str(
475 .expect("test user.cfg is not parsable");
476 let acl_tree
= pbs_config
::acl
::AclTree
::from_raw(
478 acl:1:/datastore/localstore1:read@pbs,write@pbs:DatastoreAudit
479 acl:1:/datastore/localstore1:write@pbs:DatastoreBackup
480 acl:1:/datastore/localstore2:write@pbs:DatastorePowerUser
481 acl:1:/datastore/localstore3:write@pbs:DatastoreAdmin
482 acl:1:/remote/remote1:read@pbs,write@pbs:RemoteAudit
483 acl:1:/remote/remote1/remotestore1:write@pbs:RemoteSyncOperator
486 .expect("test acl.cfg is not parsable");
488 let user_info
= CachedUserInfo
::test_new(user_cfg
, acl_tree
);
490 let root_auth_id
= Authid
::root_auth_id();
492 let no_perm_auth_id
: Authid
= "noperm@pbs".parse()?
;
493 let read_auth_id
: Authid
= "read@pbs".parse()?
;
494 let write_auth_id
: Authid
= "write@pbs".parse()?
;
496 let mut job
= SyncJobConfig
{
497 id
: "regular".to_string(),
498 remote
: "remote0".to_string(),
499 remote_store
: "remotestore1".to_string(),
501 store
: "localstore0".to_string(),
503 owner
: Some(write_auth_id
.clone()),
505 remove_vanished
: None
,
509 limit
: pbs_api_types
::RateLimitConfig
::default(), // no limit
512 // should work without ACLs
513 assert
!(check_sync_job_read_access(&user_info
, root_auth_id
, &job
));
514 assert
!(check_sync_job_modify_access(&user_info
, root_auth_id
, &job
));
516 // user without permissions must fail
517 assert
!(!check_sync_job_read_access(
522 assert
!(!check_sync_job_modify_access(
528 // reading without proper read permissions on either remote or local must fail
529 assert
!(!check_sync_job_read_access(&user_info
, &read_auth_id
, &job
));
531 // reading without proper read permissions on local end must fail
532 job
.remote
= "remote1".to_string();
533 assert
!(!check_sync_job_read_access(&user_info
, &read_auth_id
, &job
));
535 // reading without proper read permissions on remote end must fail
536 job
.remote
= "remote0".to_string();
537 job
.store
= "localstore1".to_string();
538 assert
!(!check_sync_job_read_access(&user_info
, &read_auth_id
, &job
));
540 // writing without proper write permissions on either end must fail
541 job
.store
= "localstore0".to_string();
542 assert
!(!check_sync_job_modify_access(
548 // writing without proper write permissions on local end must fail
549 job
.remote
= "remote1".to_string();
551 // writing without proper write permissions on remote end must fail
552 job
.remote
= "remote0".to_string();
553 job
.store
= "localstore1".to_string();
554 assert
!(!check_sync_job_modify_access(
560 // reset remote to one where users have access
561 job
.remote
= "remote1".to_string();
563 // user with read permission can only read, but not modify/run
564 assert
!(check_sync_job_read_access(&user_info
, &read_auth_id
, &job
));
565 job
.owner
= Some(read_auth_id
.clone());
566 assert
!(!check_sync_job_modify_access(
572 assert
!(!check_sync_job_modify_access(
577 job
.owner
= Some(write_auth_id
.clone());
578 assert
!(!check_sync_job_modify_access(
584 // user with simple write permission can modify/run
585 assert
!(check_sync_job_read_access(&user_info
, &write_auth_id
, &job
));
586 assert
!(check_sync_job_modify_access(
592 // but can't modify/run with deletion
593 job
.remove_vanished
= Some(true);
594 assert
!(!check_sync_job_modify_access(
600 // unless they have Datastore.Prune as well
601 job
.store
= "localstore2".to_string();
602 assert
!(check_sync_job_modify_access(
608 // changing owner is not possible
609 job
.owner
= Some(read_auth_id
.clone());
610 assert
!(!check_sync_job_modify_access(
616 // also not to the default 'root@pam'
618 assert
!(!check_sync_job_modify_access(
624 // unless they have Datastore.Modify as well
625 job
.store
= "localstore3".to_string();
626 job
.owner
= Some(read_auth_id
);
627 assert
!(check_sync_job_modify_access(
633 assert
!(check_sync_job_modify_access(