]> git.proxmox.com Git - proxmox-backup.git/blob - src/api2/config/sync.rs
sync: allow sync for non-superusers
[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 // user can run the corresponding pull job
36 pub fn check_sync_job_modify_access(
37 user_info: &CachedUserInfo,
38 auth_id: &Authid,
39 job: &SyncJobConfig,
40 ) -> bool {
41 let datastore_privs = user_info.lookup_privs(&auth_id, &["datastore", &job.store]);
42 if datastore_privs & PRIV_DATASTORE_BACKUP == 0 {
43 return false;
44 }
45
46 if let Some(true) = job.remove_vanished {
47 if datastore_privs & PRIV_DATASTORE_PRUNE == 0 {
48 return false;
49 }
50 }
51
52 let correct_owner = match job.owner {
53 Some(ref owner) => {
54 owner == auth_id
55 || (owner.is_token()
56 && !auth_id.is_token()
57 && owner.user() == auth_id.user())
58 },
59 // default sync owner
60 None => auth_id == Authid::backup_auth_id(),
61 };
62
63 // same permission as changing ownership after syncing
64 if !correct_owner && datastore_privs & PRIV_DATASTORE_MODIFY == 0 {
65 return false;
66 }
67
68 let remote_privs = user_info.lookup_privs(&auth_id, &["remote", &job.remote, &job.remote_store]);
69 remote_privs & PRIV_REMOTE_READ != 0
70 }
71
72 #[api(
73 input: {
74 properties: {},
75 },
76 returns: {
77 description: "List configured jobs.",
78 type: Array,
79 items: { type: sync::SyncJobConfig },
80 },
81 access: {
82 description: "Limited to sync job entries where user has Datastore.Audit on target datastore, and Remote.Audit on source remote.",
83 permission: &Permission::Anybody,
84 },
85 )]
86 /// List all sync jobs
87 pub fn list_sync_jobs(
88 _param: Value,
89 mut rpcenv: &mut dyn RpcEnvironment,
90 ) -> Result<Vec<SyncJobConfig>, Error> {
91 let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
92 let user_info = CachedUserInfo::new()?;
93
94 let (config, digest) = sync::config()?;
95
96 let list = config.convert_to_typed_array("sync")?;
97
98 rpcenv["digest"] = proxmox::tools::digest_to_hex(&digest).into();
99
100 let list = list
101 .into_iter()
102 .filter(|sync_job| check_sync_job_read_access(&user_info, &auth_id, &sync_job))
103 .collect();
104 Ok(list)
105 }
106
107 #[api(
108 protected: true,
109 input: {
110 properties: {
111 id: {
112 schema: JOB_ID_SCHEMA,
113 },
114 store: {
115 schema: DATASTORE_SCHEMA,
116 },
117 owner: {
118 type: Authid,
119 optional: true,
120 },
121 remote: {
122 schema: REMOTE_ID_SCHEMA,
123 },
124 "remote-store": {
125 schema: DATASTORE_SCHEMA,
126 },
127 "remove-vanished": {
128 schema: REMOVE_VANISHED_BACKUPS_SCHEMA,
129 optional: true,
130 },
131 comment: {
132 optional: true,
133 schema: SINGLE_LINE_COMMENT_SCHEMA,
134 },
135 schedule: {
136 optional: true,
137 schema: SYNC_SCHEDULE_SCHEMA,
138 },
139 },
140 },
141 access: {
142 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",
143 permission: &Permission::Anybody,
144 },
145 )]
146 /// Create a new sync job.
147 pub fn create_sync_job(
148 param: Value,
149 rpcenv: &mut dyn RpcEnvironment,
150 ) -> Result<(), Error> {
151 let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
152 let user_info = CachedUserInfo::new()?;
153
154 let _lock = open_file_locked(sync::SYNC_CFG_LOCKFILE, std::time::Duration::new(10, 0), true)?;
155
156 let sync_job: sync::SyncJobConfig = serde_json::from_value(param.clone())?;
157 if !check_sync_job_modify_access(&user_info, &auth_id, &sync_job) {
158 bail!("permission check failed");
159 }
160
161 let (mut config, _digest) = sync::config()?;
162
163 if let Some(_) = config.sections.get(&sync_job.id) {
164 bail!("job '{}' already exists.", sync_job.id);
165 }
166
167 config.set_data(&sync_job.id, "sync", &sync_job)?;
168
169 sync::save_config(&config)?;
170
171 crate::server::jobstate::create_state_file("syncjob", &sync_job.id)?;
172
173 Ok(())
174 }
175
176 #[api(
177 input: {
178 properties: {
179 id: {
180 schema: JOB_ID_SCHEMA,
181 },
182 },
183 },
184 returns: {
185 description: "The sync job configuration.",
186 type: sync::SyncJobConfig,
187 },
188 access: {
189 description: "Limited to sync job entries where user has Datastore.Audit on target datastore, and Remote.Audit on source remote.",
190 permission: &Permission::Anybody,
191 },
192 )]
193 /// Read a sync job configuration.
194 pub fn read_sync_job(
195 id: String,
196 mut rpcenv: &mut dyn RpcEnvironment,
197 ) -> Result<SyncJobConfig, Error> {
198 let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
199 let user_info = CachedUserInfo::new()?;
200
201 let (config, digest) = sync::config()?;
202
203 let sync_job = config.lookup("sync", &id)?;
204 if !check_sync_job_read_access(&user_info, &auth_id, &sync_job) {
205 bail!("permission check failed");
206 }
207
208 rpcenv["digest"] = proxmox::tools::digest_to_hex(&digest).into();
209
210 Ok(sync_job)
211 }
212
213 #[api()]
214 #[derive(Serialize, Deserialize)]
215 #[serde(rename_all="kebab-case")]
216 #[allow(non_camel_case_types)]
217 /// Deletable property name
218 pub enum DeletableProperty {
219 /// Delete the owner property.
220 owner,
221 /// Delete the comment property.
222 comment,
223 /// Delete the job schedule.
224 schedule,
225 /// Delete the remove-vanished flag.
226 remove_vanished,
227 }
228
229 #[api(
230 protected: true,
231 input: {
232 properties: {
233 id: {
234 schema: JOB_ID_SCHEMA,
235 },
236 store: {
237 schema: DATASTORE_SCHEMA,
238 optional: true,
239 },
240 owner: {
241 type: Authid,
242 optional: true,
243 },
244 remote: {
245 schema: REMOTE_ID_SCHEMA,
246 optional: true,
247 },
248 "remote-store": {
249 schema: DATASTORE_SCHEMA,
250 optional: true,
251 },
252 "remove-vanished": {
253 schema: REMOVE_VANISHED_BACKUPS_SCHEMA,
254 optional: true,
255 },
256 comment: {
257 optional: true,
258 schema: SINGLE_LINE_COMMENT_SCHEMA,
259 },
260 schedule: {
261 optional: true,
262 schema: SYNC_SCHEDULE_SCHEMA,
263 },
264 delete: {
265 description: "List of properties to delete.",
266 type: Array,
267 optional: true,
268 items: {
269 type: DeletableProperty,
270 }
271 },
272 digest: {
273 optional: true,
274 schema: PROXMOX_CONFIG_DIGEST_SCHEMA,
275 },
276 },
277 },
278 access: {
279 permission: &Permission::Anybody,
280 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",
281 },
282 )]
283 /// Update sync job config.
284 pub fn update_sync_job(
285 id: String,
286 store: Option<String>,
287 owner: Option<Authid>,
288 remote: Option<String>,
289 remote_store: Option<String>,
290 remove_vanished: Option<bool>,
291 comment: Option<String>,
292 schedule: Option<String>,
293 delete: Option<Vec<DeletableProperty>>,
294 digest: Option<String>,
295 rpcenv: &mut dyn RpcEnvironment,
296 ) -> Result<(), Error> {
297 let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
298 let user_info = CachedUserInfo::new()?;
299
300 let _lock = open_file_locked(sync::SYNC_CFG_LOCKFILE, std::time::Duration::new(10, 0), true)?;
301
302 // pass/compare digest
303 let (mut config, expected_digest) = sync::config()?;
304
305 if let Some(ref digest) = digest {
306 let digest = proxmox::tools::hex_to_digest(digest)?;
307 crate::tools::detect_modified_configuration_file(&digest, &expected_digest)?;
308 }
309
310 let mut data: sync::SyncJobConfig = config.lookup("sync", &id)?;
311
312 if let Some(delete) = delete {
313 for delete_prop in delete {
314 match delete_prop {
315 DeletableProperty::owner => { data.owner = None; },
316 DeletableProperty::comment => { data.comment = None; },
317 DeletableProperty::schedule => { data.schedule = None; },
318 DeletableProperty::remove_vanished => { data.remove_vanished = None; },
319 }
320 }
321 }
322
323 if let Some(comment) = comment {
324 let comment = comment.trim().to_string();
325 if comment.is_empty() {
326 data.comment = None;
327 } else {
328 data.comment = Some(comment);
329 }
330 }
331
332 if let Some(store) = store { data.store = store; }
333 if let Some(remote) = remote { data.remote = remote; }
334 if let Some(remote_store) = remote_store { data.remote_store = remote_store; }
335 if let Some(owner) = owner { data.owner = Some(owner); }
336
337 if schedule.is_some() { data.schedule = schedule; }
338 if remove_vanished.is_some() { data.remove_vanished = remove_vanished; }
339
340 if !check_sync_job_modify_access(&user_info, &auth_id, &data) {
341 bail!("permission check failed");
342 }
343
344 config.set_data(&id, "sync", &data)?;
345
346 sync::save_config(&config)?;
347
348 Ok(())
349 }
350
351 #[api(
352 protected: true,
353 input: {
354 properties: {
355 id: {
356 schema: JOB_ID_SCHEMA,
357 },
358 digest: {
359 optional: true,
360 schema: PROXMOX_CONFIG_DIGEST_SCHEMA,
361 },
362 },
363 },
364 access: {
365 permission: &Permission::Anybody,
366 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",
367 },
368 )]
369 /// Remove a sync job configuration
370 pub fn delete_sync_job(
371 id: String,
372 digest: Option<String>,
373 rpcenv: &mut dyn RpcEnvironment,
374 ) -> Result<(), Error> {
375 let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
376 let user_info = CachedUserInfo::new()?;
377
378 let _lock = open_file_locked(sync::SYNC_CFG_LOCKFILE, std::time::Duration::new(10, 0), true)?;
379
380 let (mut config, expected_digest) = sync::config()?;
381
382 if let Some(ref digest) = digest {
383 let digest = proxmox::tools::hex_to_digest(digest)?;
384 crate::tools::detect_modified_configuration_file(&digest, &expected_digest)?;
385 }
386
387 match config.lookup("sync", &id) {
388 Ok(job) => {
389 if !check_sync_job_modify_access(&user_info, &auth_id, &job) {
390 bail!("permission check failed");
391 }
392 config.sections.remove(&id);
393 },
394 Err(_) => { bail!("job '{}' does not exist.", id) },
395 };
396
397 sync::save_config(&config)?;
398
399 crate::server::jobstate::remove_state_file("syncjob", &id)?;
400
401 Ok(())
402 }
403
404 const ITEM_ROUTER: Router = Router::new()
405 .get(&API_METHOD_READ_SYNC_JOB)
406 .put(&API_METHOD_UPDATE_SYNC_JOB)
407 .delete(&API_METHOD_DELETE_SYNC_JOB);
408
409 pub const ROUTER: Router = Router::new()
410 .get(&API_METHOD_LIST_SYNC_JOBS)
411 .post(&API_METHOD_CREATE_SYNC_JOB)
412 .match_all("id", &ITEM_ROUTER);