]> git.proxmox.com Git - proxmox-backup.git/blob - src/api2/config/sync.rs
complete_acl_path: add more paths
[proxmox-backup.git] / src / api2 / config / sync.rs
1 use anyhow::{bail, Error};
2 use serde_json::Value;
3 use ::serde::{Deserialize, Serialize};
4
5 use proxmox::api::{api, Permission, Router, RpcEnvironment};
6 use proxmox::tools::fs::open_file_locked;
7
8 use crate::api2::types::*;
9
10 use crate::config::acl::{
11 PRIV_DATASTORE_AUDIT,
12 PRIV_DATASTORE_BACKUP,
13 PRIV_DATASTORE_MODIFY,
14 PRIV_DATASTORE_PRUNE,
15 PRIV_REMOTE_AUDIT,
16 PRIV_REMOTE_READ,
17 };
18
19 use crate::config::cached_user_info::CachedUserInfo;
20 use crate::config::sync::{self, SyncJobConfig};
21
22 pub fn check_sync_job_read_access(
23 user_info: &CachedUserInfo,
24 auth_id: &Authid,
25 job: &SyncJobConfig,
26 ) -> bool {
27 let datastore_privs = user_info.lookup_privs(&auth_id, &["datastore", &job.store]);
28 if datastore_privs & PRIV_DATASTORE_AUDIT == 0 {
29 return false;
30 }
31
32 let remote_privs = user_info.lookup_privs(&auth_id, &["remote", &job.remote]);
33 remote_privs & PRIV_REMOTE_AUDIT != 0
34 }
35
36 // user can run the corresponding pull job
37 pub fn check_sync_job_modify_access(
38 user_info: &CachedUserInfo,
39 auth_id: &Authid,
40 job: &SyncJobConfig,
41 ) -> bool {
42 let datastore_privs = user_info.lookup_privs(&auth_id, &["datastore", &job.store]);
43 if datastore_privs & PRIV_DATASTORE_BACKUP == 0 {
44 return false;
45 }
46
47 if let Some(true) = job.remove_vanished {
48 if datastore_privs & PRIV_DATASTORE_PRUNE == 0 {
49 return false;
50 }
51 }
52
53 let correct_owner = match job.owner {
54 Some(ref owner) => {
55 owner == auth_id
56 || (owner.is_token()
57 && !auth_id.is_token()
58 && owner.user() == auth_id.user())
59 },
60 // default sync owner
61 None => auth_id == Authid::root_auth_id(),
62 };
63
64 // same permission as changing ownership after syncing
65 if !correct_owner && datastore_privs & PRIV_DATASTORE_MODIFY == 0 {
66 return false;
67 }
68
69 let remote_privs = user_info.lookup_privs(&auth_id, &["remote", &job.remote, &job.remote_store]);
70 remote_privs & PRIV_REMOTE_READ != 0
71 }
72
73 #[api(
74 input: {
75 properties: {},
76 },
77 returns: {
78 description: "List configured jobs.",
79 type: Array,
80 items: { type: sync::SyncJobConfig },
81 },
82 access: {
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,
85 },
86 )]
87 /// List all sync jobs
88 pub fn list_sync_jobs(
89 _param: Value,
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()?;
94
95 let (config, digest) = sync::config()?;
96
97 let list = config.convert_to_typed_array("sync")?;
98
99 rpcenv["digest"] = proxmox::tools::digest_to_hex(&digest).into();
100
101 let list = list
102 .into_iter()
103 .filter(|sync_job| check_sync_job_read_access(&user_info, &auth_id, &sync_job))
104 .collect();
105 Ok(list)
106 }
107
108 #[api(
109 protected: true,
110 input: {
111 properties: {
112 id: {
113 schema: JOB_ID_SCHEMA,
114 },
115 store: {
116 schema: DATASTORE_SCHEMA,
117 },
118 owner: {
119 type: Authid,
120 optional: true,
121 },
122 remote: {
123 schema: REMOTE_ID_SCHEMA,
124 },
125 "remote-store": {
126 schema: DATASTORE_SCHEMA,
127 },
128 "remove-vanished": {
129 schema: REMOVE_VANISHED_BACKUPS_SCHEMA,
130 optional: true,
131 },
132 comment: {
133 optional: true,
134 schema: SINGLE_LINE_COMMENT_SCHEMA,
135 },
136 schedule: {
137 optional: true,
138 schema: SYNC_SCHEDULE_SCHEMA,
139 },
140 },
141 },
142 access: {
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,
145 },
146 )]
147 /// Create a new sync job.
148 pub fn create_sync_job(
149 param: Value,
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()?;
154
155 let _lock = open_file_locked(sync::SYNC_CFG_LOCKFILE, std::time::Duration::new(10, 0), true)?;
156
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");
160 }
161
162 let (mut config, _digest) = sync::config()?;
163
164 if config.sections.get(&sync_job.id).is_some() {
165 bail!("job '{}' already exists.", sync_job.id);
166 }
167
168 config.set_data(&sync_job.id, "sync", &sync_job)?;
169
170 sync::save_config(&config)?;
171
172 crate::server::jobstate::create_state_file("syncjob", &sync_job.id)?;
173
174 Ok(())
175 }
176
177 #[api(
178 input: {
179 properties: {
180 id: {
181 schema: JOB_ID_SCHEMA,
182 },
183 },
184 },
185 returns: { type: sync::SyncJobConfig },
186 access: {
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,
189 },
190 )]
191 /// Read a sync job configuration.
192 pub fn read_sync_job(
193 id: String,
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()?;
198
199 let (config, digest) = sync::config()?;
200
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");
204 }
205
206 rpcenv["digest"] = proxmox::tools::digest_to_hex(&digest).into();
207
208 Ok(sync_job)
209 }
210
211 #[api()]
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.
218 owner,
219 /// Delete the comment property.
220 comment,
221 /// Delete the job schedule.
222 schedule,
223 /// Delete the remove-vanished flag.
224 remove_vanished,
225 }
226
227 #[api(
228 protected: true,
229 input: {
230 properties: {
231 id: {
232 schema: JOB_ID_SCHEMA,
233 },
234 store: {
235 schema: DATASTORE_SCHEMA,
236 optional: true,
237 },
238 owner: {
239 type: Authid,
240 optional: true,
241 },
242 remote: {
243 schema: REMOTE_ID_SCHEMA,
244 optional: true,
245 },
246 "remote-store": {
247 schema: DATASTORE_SCHEMA,
248 optional: true,
249 },
250 "remove-vanished": {
251 schema: REMOVE_VANISHED_BACKUPS_SCHEMA,
252 optional: true,
253 },
254 comment: {
255 optional: true,
256 schema: SINGLE_LINE_COMMENT_SCHEMA,
257 },
258 schedule: {
259 optional: true,
260 schema: SYNC_SCHEDULE_SCHEMA,
261 },
262 delete: {
263 description: "List of properties to delete.",
264 type: Array,
265 optional: true,
266 items: {
267 type: DeletableProperty,
268 }
269 },
270 digest: {
271 optional: true,
272 schema: PROXMOX_CONFIG_DIGEST_SCHEMA,
273 },
274 },
275 },
276 access: {
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",
279 },
280 )]
281 /// Update sync job config.
282 #[allow(clippy::too_many_arguments)]
283 pub fn update_sync_job(
284 id: String,
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()?;
298
299 let _lock = open_file_locked(sync::SYNC_CFG_LOCKFILE, std::time::Duration::new(10, 0), true)?;
300
301 // pass/compare digest
302 let (mut config, expected_digest) = sync::config()?;
303
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)?;
307 }
308
309 let mut data: sync::SyncJobConfig = config.lookup("sync", &id)?;
310
311 if let Some(delete) = delete {
312 for delete_prop in delete {
313 match delete_prop {
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; },
318 }
319 }
320 }
321
322 if let Some(comment) = comment {
323 let comment = comment.trim().to_string();
324 if comment.is_empty() {
325 data.comment = None;
326 } else {
327 data.comment = Some(comment);
328 }
329 }
330
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); }
335
336 if schedule.is_some() { data.schedule = schedule; }
337 if remove_vanished.is_some() { data.remove_vanished = remove_vanished; }
338
339 if !check_sync_job_modify_access(&user_info, &auth_id, &data) {
340 bail!("permission check failed");
341 }
342
343 config.set_data(&id, "sync", &data)?;
344
345 sync::save_config(&config)?;
346
347 Ok(())
348 }
349
350 #[api(
351 protected: true,
352 input: {
353 properties: {
354 id: {
355 schema: JOB_ID_SCHEMA,
356 },
357 digest: {
358 optional: true,
359 schema: PROXMOX_CONFIG_DIGEST_SCHEMA,
360 },
361 },
362 },
363 access: {
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",
366 },
367 )]
368 /// Remove a sync job configuration
369 pub fn delete_sync_job(
370 id: String,
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()?;
376
377 let _lock = open_file_locked(sync::SYNC_CFG_LOCKFILE, std::time::Duration::new(10, 0), true)?;
378
379 let (mut config, expected_digest) = sync::config()?;
380
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)?;
384 }
385
386 match config.lookup("sync", &id) {
387 Ok(job) => {
388 if !check_sync_job_modify_access(&user_info, &auth_id, &job) {
389 bail!("permission check failed");
390 }
391 config.sections.remove(&id);
392 },
393 Err(_) => { bail!("job '{}' does not exist.", id) },
394 };
395
396 sync::save_config(&config)?;
397
398 crate::server::jobstate::remove_state_file("syncjob", &id)?;
399
400 Ok(())
401 }
402
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);
407
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);
412
413
414 #[test]
415 fn sync_job_access_test() -> Result<(), Error> {
416 let (user_cfg, _) = crate::config::user::test_cfg_from_str(r###"
417 user: noperm@pbs
418
419 user: read@pbs
420
421 user: write@pbs
422
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");
432
433 let user_info = CachedUserInfo::test_new(user_cfg, acl_tree);
434
435 let root_auth_id = Authid::root_auth_id();
436
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()?;
440
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()),
447 comment: None,
448 remove_vanished: None,
449 schedule: None,
450 };
451
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);
455
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);
459
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);
462
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);
466
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);
471
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);
475
476 // writing without proper write permissions on local end must fail
477 job.remote = "remote1".to_string();
478
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);
483
484 // reset remote to one where users have access
485 job.remote = "remote1".to_string();
486
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);
491 job.owner = None;
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);
495
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);
499
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);
503
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);
507
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);
511
512 // also not to the default 'root@pam'
513 job.owner = None;
514 assert_eq!(check_sync_job_modify_access(&user_info, &write_auth_id, &job), false);
515
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);
520 job.owner = None;
521 assert_eq!(check_sync_job_modify_access(&user_info, &write_auth_id, &job), true);
522
523 Ok(())
524 }