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 #[allow(non_camel_case_types)]
193 /// Deletable property name
194 pub enum DeletableProperty
{
195 /// Delete the owner property.
197 /// Delete the comment property.
199 /// Delete the job schedule.
201 /// Delete the remove-vanished flag.
203 /// Delete the group_filter property.
205 /// Delete the rate_in property.
207 /// Delete the burst_in property.
209 /// Delete the rate_out property.
211 /// Delete the burst_out property.
213 /// Delete the ns property,
215 /// Delete the remote_ns property,
217 /// Delete the max_depth property,
226 schema
: JOB_ID_SCHEMA
,
229 type: SyncJobConfigUpdater
,
233 description
: "List of properties to delete.",
237 type: DeletableProperty
,
242 schema
: PROXMOX_CONFIG_DIGEST_SCHEMA
,
247 permission
: &Permission
::Anybody
,
248 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",
251 /// Update sync job config.
252 #[allow(clippy::too_many_arguments)]
253 pub fn update_sync_job(
255 update
: SyncJobConfigUpdater
,
256 delete
: Option
<Vec
<DeletableProperty
>>,
257 digest
: Option
<String
>,
258 rpcenv
: &mut dyn RpcEnvironment
,
259 ) -> Result
<(), Error
> {
260 let auth_id
: Authid
= rpcenv
.get_auth_id().unwrap().parse()?
;
261 let user_info
= CachedUserInfo
::new()?
;
263 let _lock
= sync
::lock_config()?
;
265 let (mut config
, expected_digest
) = sync
::config()?
;
267 if let Some(ref digest
) = digest
{
268 let digest
= <[u8; 32]>::from_hex(digest
)?
;
269 crate::tools
::detect_modified_configuration_file(&digest
, &expected_digest
)?
;
272 let mut data
: SyncJobConfig
= config
.lookup("sync", &id
)?
;
274 if let Some(delete
) = delete
{
275 for delete_prop
in delete
{
277 DeletableProperty
::owner
=> {
280 DeletableProperty
::comment
=> {
283 DeletableProperty
::schedule
=> {
284 data
.schedule
= None
;
286 DeletableProperty
::remove_vanished
=> {
287 data
.remove_vanished
= None
;
289 DeletableProperty
::group_filter
=> {
290 data
.group_filter
= None
;
292 DeletableProperty
::rate_in
=> {
293 data
.limit
.rate_in
= None
;
295 DeletableProperty
::rate_out
=> {
296 data
.limit
.rate_out
= None
;
298 DeletableProperty
::burst_in
=> {
299 data
.limit
.burst_in
= None
;
301 DeletableProperty
::burst_out
=> {
302 data
.limit
.burst_out
= None
;
304 DeletableProperty
::ns
=> {
307 DeletableProperty
::remote_ns
=> {
308 data
.remote_ns
= None
;
310 DeletableProperty
::max_depth
=> {
311 data
.max_depth
= None
;
317 if let Some(comment
) = update
.comment
{
318 let comment
= comment
.trim().to_string();
319 if comment
.is_empty() {
322 data
.comment
= Some(comment
);
326 if let Some(store
) = update
.store
{
329 if let Some(ns
) = update
.ns
{
332 if let Some(remote
) = update
.remote
{
333 data
.remote
= remote
;
335 if let Some(remote_store
) = update
.remote_store
{
336 data
.remote_store
= remote_store
;
338 if let Some(remote_ns
) = update
.remote_ns
{
339 data
.remote_ns
= Some(remote_ns
);
341 if let Some(owner
) = update
.owner
{
342 data
.owner
= Some(owner
);
344 if let Some(group_filter
) = update
.group_filter
{
345 data
.group_filter
= Some(group_filter
);
348 if update
.limit
.rate_in
.is_some() {
349 data
.limit
.rate_in
= update
.limit
.rate_in
;
352 if update
.limit
.rate_out
.is_some() {
353 data
.limit
.rate_out
= update
.limit
.rate_out
;
356 if update
.limit
.burst_in
.is_some() {
357 data
.limit
.burst_in
= update
.limit
.burst_in
;
360 if update
.limit
.burst_out
.is_some() {
361 data
.limit
.burst_out
= update
.limit
.burst_out
;
364 let schedule_changed
= data
.schedule
!= update
.schedule
;
365 if update
.schedule
.is_some() {
366 data
.schedule
= update
.schedule
;
368 if update
.remove_vanished
.is_some() {
369 data
.remove_vanished
= update
.remove_vanished
;
371 if let Some(max_depth
) = update
.max_depth
{
372 data
.max_depth
= Some(max_depth
);
375 if let Some(max_depth
) = data
.max_depth
{
376 if let Some(ref ns
) = data
.ns
{
377 ns
.check_max_depth(max_depth
)?
;
379 if let Some(ref ns
) = data
.remote_ns
{
380 ns
.check_max_depth(max_depth
)?
;
384 if !check_sync_job_modify_access(&user_info
, &auth_id
, &data
) {
385 bail
!("permission check failed");
388 config
.set_data(&id
, "sync", &data
)?
;
390 sync
::save_config(&config
)?
;
392 if schedule_changed
{
393 crate::server
::jobstate
::update_job_last_run_time("syncjob", &id
)?
;
404 schema
: JOB_ID_SCHEMA
,
408 schema
: PROXMOX_CONFIG_DIGEST_SCHEMA
,
413 permission
: &Permission
::Anybody
,
414 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",
417 /// Remove a sync job configuration
418 pub fn delete_sync_job(
420 digest
: Option
<String
>,
421 rpcenv
: &mut dyn RpcEnvironment
,
422 ) -> Result
<(), Error
> {
423 let auth_id
: Authid
= rpcenv
.get_auth_id().unwrap().parse()?
;
424 let user_info
= CachedUserInfo
::new()?
;
426 let _lock
= sync
::lock_config()?
;
428 let (mut config
, expected_digest
) = sync
::config()?
;
430 if let Some(ref digest
) = digest
{
431 let digest
= <[u8; 32]>::from_hex(digest
)?
;
432 crate::tools
::detect_modified_configuration_file(&digest
, &expected_digest
)?
;
435 match config
.lookup("sync", &id
) {
437 if !check_sync_job_modify_access(&user_info
, &auth_id
, &job
) {
438 bail
!("permission check failed");
440 config
.sections
.remove(&id
);
443 http_bail
!(NOT_FOUND
, "job '{}' does not exist.", id
)
447 sync
::save_config(&config
)?
;
449 crate::server
::jobstate
::remove_state_file("syncjob", &id
)?
;
454 const ITEM_ROUTER
: Router
= Router
::new()
455 .get(&API_METHOD_READ_SYNC_JOB
)
456 .put(&API_METHOD_UPDATE_SYNC_JOB
)
457 .delete(&API_METHOD_DELETE_SYNC_JOB
);
459 pub const ROUTER
: Router
= Router
::new()
460 .get(&API_METHOD_LIST_SYNC_JOBS
)
461 .post(&API_METHOD_CREATE_SYNC_JOB
)
462 .match_all("id", &ITEM_ROUTER
);
465 fn sync_job_access_test() -> Result
<(), Error
> {
466 let (user_cfg
, _
) = pbs_config
::user
::test_cfg_from_str(
476 .expect("test user.cfg is not parsable");
477 let acl_tree
= pbs_config
::acl
::AclTree
::from_raw(
479 acl:1:/datastore/localstore1:read@pbs,write@pbs:DatastoreAudit
480 acl:1:/datastore/localstore1:write@pbs:DatastoreBackup
481 acl:1:/datastore/localstore2:write@pbs:DatastorePowerUser
482 acl:1:/datastore/localstore3:write@pbs:DatastoreAdmin
483 acl:1:/remote/remote1:read@pbs,write@pbs:RemoteAudit
484 acl:1:/remote/remote1/remotestore1:write@pbs:RemoteSyncOperator
487 .expect("test acl.cfg is not parsable");
489 let user_info
= CachedUserInfo
::test_new(user_cfg
, acl_tree
);
491 let root_auth_id
= Authid
::root_auth_id();
493 let no_perm_auth_id
: Authid
= "noperm@pbs".parse()?
;
494 let read_auth_id
: Authid
= "read@pbs".parse()?
;
495 let write_auth_id
: Authid
= "write@pbs".parse()?
;
497 let mut job
= SyncJobConfig
{
498 id
: "regular".to_string(),
499 remote
: "remote0".to_string(),
500 remote_store
: "remotestore1".to_string(),
502 store
: "localstore0".to_string(),
504 owner
: Some(write_auth_id
.clone()),
506 remove_vanished
: None
,
510 limit
: pbs_api_types
::RateLimitConfig
::default(), // no limit
513 // should work without ACLs
514 assert
!(check_sync_job_read_access(&user_info
, root_auth_id
, &job
));
515 assert
!(check_sync_job_modify_access(&user_info
, root_auth_id
, &job
));
517 // user without permissions must fail
518 assert
!(!check_sync_job_read_access(
523 assert
!(!check_sync_job_modify_access(
529 // reading without proper read permissions on either remote or local must fail
530 assert
!(!check_sync_job_read_access(&user_info
, &read_auth_id
, &job
));
532 // reading without proper read permissions on local end must fail
533 job
.remote
= "remote1".to_string();
534 assert
!(!check_sync_job_read_access(&user_info
, &read_auth_id
, &job
));
536 // reading without proper read permissions on remote end must fail
537 job
.remote
= "remote0".to_string();
538 job
.store
= "localstore1".to_string();
539 assert
!(!check_sync_job_read_access(&user_info
, &read_auth_id
, &job
));
541 // writing without proper write permissions on either end must fail
542 job
.store
= "localstore0".to_string();
543 assert
!(!check_sync_job_modify_access(
549 // writing without proper write permissions on local end must fail
550 job
.remote
= "remote1".to_string();
552 // writing without proper write permissions on remote end must fail
553 job
.remote
= "remote0".to_string();
554 job
.store
= "localstore1".to_string();
555 assert
!(!check_sync_job_modify_access(
561 // reset remote to one where users have access
562 job
.remote
= "remote1".to_string();
564 // user with read permission can only read, but not modify/run
565 assert
!(check_sync_job_read_access(&user_info
, &read_auth_id
, &job
));
566 job
.owner
= Some(read_auth_id
.clone());
567 assert
!(!check_sync_job_modify_access(
573 assert
!(!check_sync_job_modify_access(
578 job
.owner
= Some(write_auth_id
.clone());
579 assert
!(!check_sync_job_modify_access(
585 // user with simple write permission can modify/run
586 assert
!(check_sync_job_read_access(&user_info
, &write_auth_id
, &job
));
587 assert
!(check_sync_job_modify_access(
593 // but can't modify/run with deletion
594 job
.remove_vanished
= Some(true);
595 assert
!(!check_sync_job_modify_access(
601 // unless they have Datastore.Prune as well
602 job
.store
= "localstore2".to_string();
603 assert
!(check_sync_job_modify_access(
609 // changing owner is not possible
610 job
.owner
= Some(read_auth_id
.clone());
611 assert
!(!check_sync_job_modify_access(
617 // also not to the default 'root@pam'
619 assert
!(!check_sync_job_modify_access(
625 // unless they have Datastore.Modify as well
626 job
.store
= "localstore3".to_string();
627 job
.owner
= Some(read_auth_id
);
628 assert
!(check_sync_job_modify_access(
634 assert
!(check_sync_job_modify_access(