]> git.proxmox.com Git - proxmox-backup.git/blob - src/api2/config/sync.rs
get rid of backup@pam
[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.clone())?;
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 let Some(_) = config.sections.get(&sync_job.id) {
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: {
186 description: "The sync job configuration.",
187 type: sync::SyncJobConfig,
188 },
189 access: {
190 description: "Limited to sync job entries where user has Datastore.Audit on target datastore, and Remote.Audit on source remote.",
191 permission: &Permission::Anybody,
192 },
193 )]
194 /// Read a sync job configuration.
195 pub fn read_sync_job(
196 id: String,
197 mut rpcenv: &mut dyn RpcEnvironment,
198 ) -> Result<SyncJobConfig, Error> {
199 let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
200 let user_info = CachedUserInfo::new()?;
201
202 let (config, digest) = sync::config()?;
203
204 let sync_job = config.lookup("sync", &id)?;
205 if !check_sync_job_read_access(&user_info, &auth_id, &sync_job) {
206 bail!("permission check failed");
207 }
208
209 rpcenv["digest"] = proxmox::tools::digest_to_hex(&digest).into();
210
211 Ok(sync_job)
212 }
213
214 #[api()]
215 #[derive(Serialize, Deserialize)]
216 #[serde(rename_all="kebab-case")]
217 #[allow(non_camel_case_types)]
218 /// Deletable property name
219 pub enum DeletableProperty {
220 /// Delete the owner property.
221 owner,
222 /// Delete the comment property.
223 comment,
224 /// Delete the job schedule.
225 schedule,
226 /// Delete the remove-vanished flag.
227 remove_vanished,
228 }
229
230 #[api(
231 protected: true,
232 input: {
233 properties: {
234 id: {
235 schema: JOB_ID_SCHEMA,
236 },
237 store: {
238 schema: DATASTORE_SCHEMA,
239 optional: true,
240 },
241 owner: {
242 type: Authid,
243 optional: true,
244 },
245 remote: {
246 schema: REMOTE_ID_SCHEMA,
247 optional: true,
248 },
249 "remote-store": {
250 schema: DATASTORE_SCHEMA,
251 optional: true,
252 },
253 "remove-vanished": {
254 schema: REMOVE_VANISHED_BACKUPS_SCHEMA,
255 optional: true,
256 },
257 comment: {
258 optional: true,
259 schema: SINGLE_LINE_COMMENT_SCHEMA,
260 },
261 schedule: {
262 optional: true,
263 schema: SYNC_SCHEDULE_SCHEMA,
264 },
265 delete: {
266 description: "List of properties to delete.",
267 type: Array,
268 optional: true,
269 items: {
270 type: DeletableProperty,
271 }
272 },
273 digest: {
274 optional: true,
275 schema: PROXMOX_CONFIG_DIGEST_SCHEMA,
276 },
277 },
278 },
279 access: {
280 permission: &Permission::Anybody,
281 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",
282 },
283 )]
284 /// Update sync job config.
285 pub fn update_sync_job(
286 id: String,
287 store: Option<String>,
288 owner: Option<Authid>,
289 remote: Option<String>,
290 remote_store: Option<String>,
291 remove_vanished: Option<bool>,
292 comment: Option<String>,
293 schedule: Option<String>,
294 delete: Option<Vec<DeletableProperty>>,
295 digest: Option<String>,
296 rpcenv: &mut dyn RpcEnvironment,
297 ) -> Result<(), Error> {
298 let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
299 let user_info = CachedUserInfo::new()?;
300
301 let _lock = open_file_locked(sync::SYNC_CFG_LOCKFILE, std::time::Duration::new(10, 0), true)?;
302
303 // pass/compare digest
304 let (mut config, expected_digest) = sync::config()?;
305
306 if let Some(ref digest) = digest {
307 let digest = proxmox::tools::hex_to_digest(digest)?;
308 crate::tools::detect_modified_configuration_file(&digest, &expected_digest)?;
309 }
310
311 let mut data: sync::SyncJobConfig = config.lookup("sync", &id)?;
312
313 if let Some(delete) = delete {
314 for delete_prop in delete {
315 match delete_prop {
316 DeletableProperty::owner => { data.owner = None; },
317 DeletableProperty::comment => { data.comment = None; },
318 DeletableProperty::schedule => { data.schedule = None; },
319 DeletableProperty::remove_vanished => { data.remove_vanished = None; },
320 }
321 }
322 }
323
324 if let Some(comment) = comment {
325 let comment = comment.trim().to_string();
326 if comment.is_empty() {
327 data.comment = None;
328 } else {
329 data.comment = Some(comment);
330 }
331 }
332
333 if let Some(store) = store { data.store = store; }
334 if let Some(remote) = remote { data.remote = remote; }
335 if let Some(remote_store) = remote_store { data.remote_store = remote_store; }
336 if let Some(owner) = owner { data.owner = Some(owner); }
337
338 if schedule.is_some() { data.schedule = schedule; }
339 if remove_vanished.is_some() { data.remove_vanished = remove_vanished; }
340
341 if !check_sync_job_modify_access(&user_info, &auth_id, &data) {
342 bail!("permission check failed");
343 }
344
345 config.set_data(&id, "sync", &data)?;
346
347 sync::save_config(&config)?;
348
349 Ok(())
350 }
351
352 #[api(
353 protected: true,
354 input: {
355 properties: {
356 id: {
357 schema: JOB_ID_SCHEMA,
358 },
359 digest: {
360 optional: true,
361 schema: PROXMOX_CONFIG_DIGEST_SCHEMA,
362 },
363 },
364 },
365 access: {
366 permission: &Permission::Anybody,
367 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 },
369 )]
370 /// Remove a sync job configuration
371 pub fn delete_sync_job(
372 id: String,
373 digest: Option<String>,
374 rpcenv: &mut dyn RpcEnvironment,
375 ) -> Result<(), Error> {
376 let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
377 let user_info = CachedUserInfo::new()?;
378
379 let _lock = open_file_locked(sync::SYNC_CFG_LOCKFILE, std::time::Duration::new(10, 0), true)?;
380
381 let (mut config, expected_digest) = sync::config()?;
382
383 if let Some(ref digest) = digest {
384 let digest = proxmox::tools::hex_to_digest(digest)?;
385 crate::tools::detect_modified_configuration_file(&digest, &expected_digest)?;
386 }
387
388 match config.lookup("sync", &id) {
389 Ok(job) => {
390 if !check_sync_job_modify_access(&user_info, &auth_id, &job) {
391 bail!("permission check failed");
392 }
393 config.sections.remove(&id);
394 },
395 Err(_) => { bail!("job '{}' does not exist.", id) },
396 };
397
398 sync::save_config(&config)?;
399
400 crate::server::jobstate::remove_state_file("syncjob", &id)?;
401
402 Ok(())
403 }
404
405 const ITEM_ROUTER: Router = Router::new()
406 .get(&API_METHOD_READ_SYNC_JOB)
407 .put(&API_METHOD_UPDATE_SYNC_JOB)
408 .delete(&API_METHOD_DELETE_SYNC_JOB);
409
410 pub const ROUTER: Router = Router::new()
411 .get(&API_METHOD_LIST_SYNC_JOBS)
412 .post(&API_METHOD_CREATE_SYNC_JOB)
413 .match_all("id", &ITEM_ROUTER);
414
415
416 #[test]
417 fn sync_job_access_test() -> Result<(), Error> {
418 let (user_cfg, _) = crate::config::user::test_cfg_from_str(r###"
419 user: noperm@pbs
420
421 user: read@pbs
422
423 user: write@pbs
424
425 "###).expect("test user.cfg is not parsable");
426 let acl_tree = crate::config::acl::AclTree::from_raw(r###"
427 acl:1:/datastore/localstore1:read@pbs,write@pbs:DatastoreAudit
428 acl:1:/datastore/localstore1:write@pbs:DatastoreBackup
429 acl:1:/datastore/localstore2:write@pbs:DatastorePowerUser
430 acl:1:/datastore/localstore3:write@pbs:DatastoreAdmin
431 acl:1:/remote/remote1:read@pbs,write@pbs:RemoteAudit
432 acl:1:/remote/remote1/remotestore1:write@pbs:RemoteSyncOperator
433 "###).expect("test acl.cfg is not parsable");
434
435 let user_info = CachedUserInfo::test_new(user_cfg, acl_tree);
436
437 let root_auth_id = Authid::root_auth_id();
438
439 let no_perm_auth_id: Authid = "noperm@pbs".parse()?;
440 let read_auth_id: Authid = "read@pbs".parse()?;
441 let write_auth_id: Authid = "write@pbs".parse()?;
442
443 let mut job = SyncJobConfig {
444 id: "regular".to_string(),
445 remote: "remote0".to_string(),
446 remote_store: "remotestore1".to_string(),
447 store: "localstore0".to_string(),
448 owner: Some(write_auth_id.clone()),
449 comment: None,
450 remove_vanished: None,
451 schedule: None,
452 };
453
454 // should work without ACLs
455 assert_eq!(check_sync_job_read_access(&user_info, &root_auth_id, &job), true);
456 assert_eq!(check_sync_job_modify_access(&user_info, &root_auth_id, &job), true);
457
458 // user without permissions must fail
459 assert_eq!(check_sync_job_read_access(&user_info, &no_perm_auth_id, &job), false);
460 assert_eq!(check_sync_job_modify_access(&user_info, &no_perm_auth_id, &job), false);
461
462 // reading without proper read permissions on either remote or local must fail
463 assert_eq!(check_sync_job_read_access(&user_info, &read_auth_id, &job), false);
464
465 // reading without proper read permissions on local end must fail
466 job.remote = "remote1".to_string();
467 assert_eq!(check_sync_job_read_access(&user_info, &read_auth_id, &job), false);
468
469 // reading without proper read permissions on remote end must fail
470 job.remote = "remote0".to_string();
471 job.store = "localstore1".to_string();
472 assert_eq!(check_sync_job_read_access(&user_info, &read_auth_id, &job), false);
473
474 // writing without proper write permissions on either end must fail
475 job.store = "localstore0".to_string();
476 assert_eq!(check_sync_job_modify_access(&user_info, &write_auth_id, &job), false);
477
478 // writing without proper write permissions on local end must fail
479 job.remote = "remote1".to_string();
480
481 // writing without proper write permissions on remote end must fail
482 job.remote = "remote0".to_string();
483 job.store = "localstore1".to_string();
484 assert_eq!(check_sync_job_modify_access(&user_info, &write_auth_id, &job), false);
485
486 // reset remote to one where users have access
487 job.remote = "remote1".to_string();
488
489 // user with read permission can only read, but not modify/run
490 assert_eq!(check_sync_job_read_access(&user_info, &read_auth_id, &job), true);
491 job.owner = Some(read_auth_id.clone());
492 assert_eq!(check_sync_job_modify_access(&user_info, &read_auth_id, &job), false);
493 job.owner = None;
494 assert_eq!(check_sync_job_modify_access(&user_info, &read_auth_id, &job), false);
495 job.owner = Some(write_auth_id.clone());
496 assert_eq!(check_sync_job_modify_access(&user_info, &read_auth_id, &job), false);
497
498 // user with simple write permission can modify/run
499 assert_eq!(check_sync_job_read_access(&user_info, &write_auth_id, &job), true);
500 assert_eq!(check_sync_job_modify_access(&user_info, &write_auth_id, &job), true);
501
502 // but can't modify/run with deletion
503 job.remove_vanished = Some(true);
504 assert_eq!(check_sync_job_modify_access(&user_info, &write_auth_id, &job), false);
505
506 // unless they have Datastore.Prune as well
507 job.store = "localstore2".to_string();
508 assert_eq!(check_sync_job_modify_access(&user_info, &write_auth_id, &job), true);
509
510 // changing owner is not possible
511 job.owner = Some(read_auth_id.clone());
512 assert_eq!(check_sync_job_modify_access(&user_info, &write_auth_id, &job), false);
513
514 // also not to the default 'root@pam'
515 job.owner = None;
516 assert_eq!(check_sync_job_modify_access(&user_info, &write_auth_id, &job), false);
517
518 // unless they have Datastore.Modify as well
519 job.store = "localstore3".to_string();
520 job.owner = Some(read_auth_id.clone());
521 assert_eq!(check_sync_job_modify_access(&user_info, &write_auth_id, &job), true);
522 job.owner = None;
523 assert_eq!(check_sync_job_modify_access(&user_info, &write_auth_id, &job), true);
524
525 Ok(())
526 }