]>
Commit | Line | Data |
---|---|---|
d308dc8a | 1 | use std::fs; |
d4b84c1d | 2 | use std::path::Path; |
d308dc8a TL |
3 | use std::sync::{Arc, Mutex}; |
4 | use std::time::SystemTime; | |
d4b84c1d WB |
5 | |
6 | use anyhow::{bail, format_err, Error}; | |
d308dc8a | 7 | use lazy_static::lazy_static; |
d4b84c1d WB |
8 | use serde::{Deserialize, Serialize}; |
9 | use serde_json::{json, Value}; | |
10 | ||
11 | use proxmox::api::router::SubdirMap; | |
12 | use proxmox::api::schema::Updatable; | |
13 | use proxmox::api::{api, Permission, Router, RpcEnvironment}; | |
14 | use proxmox::http_bail; | |
15 | use proxmox::list_subdirs_api_method; | |
16 | ||
17 | use proxmox_acme_rs::account::AccountData as AcmeAccountData; | |
18 | use proxmox_acme_rs::Account; | |
19 | ||
20 | use crate::acme::AcmeClient; | |
60643023 | 21 | use crate::api2::types::{AcmeAccountName, AcmeChallengeSchema, Authid, KnownAcmeDirectory}; |
d4b84c1d WB |
22 | use crate::config::acl::PRIV_SYS_MODIFY; |
23 | use crate::config::acme::plugin::{ | |
24 | DnsPlugin, DnsPluginCore, DnsPluginCoreUpdater, PLUGIN_ID_SCHEMA, | |
25 | }; | |
d4b84c1d WB |
26 | use crate::server::WorkerTask; |
27 | use crate::tools::ControlFlow; | |
28 | ||
29 | pub(crate) const ROUTER: Router = Router::new() | |
30 | .get(&list_subdirs_api_method!(SUBDIRS)) | |
31 | .subdirs(SUBDIRS); | |
32 | ||
33 | const SUBDIRS: SubdirMap = &[ | |
34 | ( | |
35 | "account", | |
36 | &Router::new() | |
37 | .get(&API_METHOD_LIST_ACCOUNTS) | |
38 | .post(&API_METHOD_REGISTER_ACCOUNT) | |
39 | .match_all("name", &ACCOUNT_ITEM_ROUTER), | |
40 | ), | |
41 | ( | |
42 | "challenge-schema", | |
43 | &Router::new().get(&API_METHOD_GET_CHALLENGE_SCHEMA), | |
44 | ), | |
45 | ( | |
46 | "directories", | |
47 | &Router::new().get(&API_METHOD_GET_DIRECTORIES), | |
48 | ), | |
49 | ( | |
50 | "plugins", | |
51 | &Router::new() | |
52 | .get(&API_METHOD_LIST_PLUGINS) | |
53 | .post(&API_METHOD_ADD_PLUGIN) | |
54 | .match_all("id", &PLUGIN_ITEM_ROUTER), | |
55 | ), | |
56 | ("tos", &Router::new().get(&API_METHOD_GET_TOS)), | |
57 | ]; | |
58 | ||
59 | const ACCOUNT_ITEM_ROUTER: Router = Router::new() | |
60 | .get(&API_METHOD_GET_ACCOUNT) | |
61 | .put(&API_METHOD_UPDATE_ACCOUNT) | |
62 | .delete(&API_METHOD_DEACTIVATE_ACCOUNT); | |
63 | ||
64 | const PLUGIN_ITEM_ROUTER: Router = Router::new() | |
65 | .get(&API_METHOD_GET_PLUGIN) | |
66 | .put(&API_METHOD_UPDATE_PLUGIN) | |
67 | .delete(&API_METHOD_DELETE_PLUGIN); | |
68 | ||
69 | #[api( | |
70 | properties: { | |
39c5db7f | 71 | name: { type: AcmeAccountName }, |
d4b84c1d WB |
72 | }, |
73 | )] | |
74 | /// An ACME Account entry. | |
75 | /// | |
76 | /// Currently only contains a 'name' property. | |
77 | #[derive(Serialize)] | |
78 | pub struct AccountEntry { | |
39c5db7f | 79 | name: AcmeAccountName, |
d4b84c1d WB |
80 | } |
81 | ||
82 | #[api( | |
83 | access: { | |
84 | permission: &Permission::Privilege(&["system", "certificates"], PRIV_SYS_MODIFY, false), | |
85 | }, | |
86 | returns: { | |
87 | type: Array, | |
88 | items: { type: AccountEntry }, | |
89 | description: "List of ACME accounts.", | |
90 | }, | |
91 | protected: true, | |
92 | )] | |
93 | /// List ACME accounts. | |
94 | pub fn list_accounts() -> Result<Vec<AccountEntry>, Error> { | |
95 | let mut entries = Vec::new(); | |
96 | crate::config::acme::foreach_acme_account(|name| { | |
97 | entries.push(AccountEntry { name }); | |
98 | ControlFlow::Continue(()) | |
99 | })?; | |
100 | Ok(entries) | |
101 | } | |
102 | ||
103 | #[api( | |
104 | properties: { | |
105 | account: { type: Object, properties: {}, additional_properties: true }, | |
106 | tos: { | |
107 | type: String, | |
108 | optional: true, | |
109 | }, | |
110 | }, | |
111 | )] | |
112 | /// ACME Account information. | |
113 | /// | |
114 | /// This is what we return via the API. | |
115 | #[derive(Serialize)] | |
116 | pub struct AccountInfo { | |
117 | /// Raw account data. | |
118 | account: AcmeAccountData, | |
119 | ||
120 | /// The ACME directory URL the account was created at. | |
121 | directory: String, | |
122 | ||
123 | /// The account's own URL within the ACME directory. | |
124 | location: String, | |
125 | ||
126 | /// The ToS URL, if the user agreed to one. | |
127 | #[serde(skip_serializing_if = "Option::is_none")] | |
128 | tos: Option<String>, | |
129 | } | |
130 | ||
131 | #[api( | |
132 | input: { | |
133 | properties: { | |
39c5db7f | 134 | name: { type: AcmeAccountName }, |
d4b84c1d WB |
135 | }, |
136 | }, | |
137 | access: { | |
138 | permission: &Permission::Privilege(&["system", "certificates"], PRIV_SYS_MODIFY, false), | |
139 | }, | |
140 | returns: { type: AccountInfo }, | |
141 | protected: true, | |
142 | )] | |
143 | /// Return existing ACME account information. | |
39c5db7f | 144 | pub async fn get_account(name: AcmeAccountName) -> Result<AccountInfo, Error> { |
d4b84c1d WB |
145 | let client = AcmeClient::load(&name).await?; |
146 | let account = client.account()?; | |
147 | Ok(AccountInfo { | |
148 | location: account.location.clone(), | |
149 | tos: client.tos().map(str::to_owned), | |
150 | directory: client.directory_url().to_owned(), | |
151 | account: AcmeAccountData { | |
152 | only_return_existing: false, // don't actually write this out in case it's set | |
153 | ..account.data.clone() | |
154 | }, | |
155 | }) | |
156 | } | |
157 | ||
158 | fn account_contact_from_string(s: &str) -> Vec<String> { | |
159 | s.split(&[' ', ';', ',', '\0'][..]) | |
160 | .map(|s| format!("mailto:{}", s)) | |
161 | .collect() | |
162 | } | |
163 | ||
164 | #[api( | |
165 | input: { | |
166 | properties: { | |
167 | name: { | |
39c5db7f | 168 | type: AcmeAccountName, |
d4b84c1d WB |
169 | optional: true, |
170 | }, | |
171 | contact: { | |
172 | description: "List of email addresses.", | |
173 | }, | |
174 | tos_url: { | |
175 | description: "URL of CA TermsOfService - setting this indicates agreement.", | |
176 | optional: true, | |
177 | }, | |
178 | directory: { | |
179 | type: String, | |
180 | description: "The ACME Directory.", | |
181 | optional: true, | |
182 | }, | |
183 | }, | |
184 | }, | |
185 | access: { | |
186 | permission: &Permission::Privilege(&["system", "certificates"], PRIV_SYS_MODIFY, false), | |
187 | }, | |
188 | protected: true, | |
189 | )] | |
190 | /// Register an ACME account. | |
191 | fn register_account( | |
39c5db7f | 192 | name: Option<AcmeAccountName>, |
d4b84c1d WB |
193 | // Todo: email & email-list schema |
194 | contact: String, | |
195 | tos_url: Option<String>, | |
196 | directory: Option<String>, | |
197 | rpcenv: &mut dyn RpcEnvironment, | |
198 | ) -> Result<String, Error> { | |
199 | let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?; | |
200 | ||
875d53ef TL |
201 | let name = name.unwrap_or_else(|| unsafe { |
202 | AcmeAccountName::from_string_unchecked("default".to_string()) | |
203 | }); | |
d4b84c1d WB |
204 | |
205 | if Path::new(&crate::config::acme::account_path(&name)).exists() { | |
ee0c5c8e | 206 | http_bail!(BAD_REQUEST, "account {} already exists", name); |
d4b84c1d WB |
207 | } |
208 | ||
209 | let directory = directory.unwrap_or_else(|| { | |
210 | crate::config::acme::DEFAULT_ACME_DIRECTORY_ENTRY | |
211 | .url | |
212 | .to_owned() | |
213 | }); | |
214 | ||
215 | WorkerTask::spawn( | |
216 | "acme-register", | |
217 | None, | |
218 | auth_id, | |
219 | true, | |
220 | move |worker| async move { | |
221 | let mut client = AcmeClient::new(directory); | |
222 | ||
223 | worker.log("Registering ACME account..."); | |
224 | ||
225 | let account = | |
226 | do_register_account(&mut client, &name, tos_url.is_some(), contact, None).await?; | |
227 | ||
228 | worker.log(format!( | |
229 | "Registration successful, account URL: {}", | |
230 | account.location | |
231 | )); | |
232 | ||
233 | Ok(()) | |
234 | }, | |
235 | ) | |
236 | } | |
237 | ||
238 | pub async fn do_register_account<'a>( | |
239 | client: &'a mut AcmeClient, | |
39c5db7f | 240 | name: &AcmeAccountName, |
d4b84c1d WB |
241 | agree_to_tos: bool, |
242 | contact: String, | |
243 | rsa_bits: Option<u32>, | |
244 | ) -> Result<&'a Account, Error> { | |
245 | let contact = account_contact_from_string(&contact); | |
246 | Ok(client | |
247 | .new_account(name, agree_to_tos, contact, rsa_bits) | |
248 | .await?) | |
249 | } | |
250 | ||
251 | #[api( | |
252 | input: { | |
253 | properties: { | |
39c5db7f | 254 | name: { type: AcmeAccountName }, |
d4b84c1d WB |
255 | contact: { |
256 | description: "List of email addresses.", | |
257 | optional: true, | |
258 | }, | |
259 | }, | |
260 | }, | |
261 | access: { | |
262 | permission: &Permission::Privilege(&["system", "certificates"], PRIV_SYS_MODIFY, false), | |
263 | }, | |
264 | protected: true, | |
265 | )] | |
266 | /// Update an ACME account. | |
267 | pub fn update_account( | |
39c5db7f | 268 | name: AcmeAccountName, |
d4b84c1d WB |
269 | // Todo: email & email-list schema |
270 | contact: Option<String>, | |
271 | rpcenv: &mut dyn RpcEnvironment, | |
272 | ) -> Result<String, Error> { | |
273 | let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?; | |
274 | ||
275 | WorkerTask::spawn( | |
276 | "acme-update", | |
277 | None, | |
278 | auth_id, | |
279 | true, | |
280 | move |_worker| async move { | |
281 | let data = match contact { | |
282 | Some(data) => json!({ | |
283 | "contact": account_contact_from_string(&data), | |
284 | }), | |
285 | None => json!({}), | |
286 | }; | |
287 | ||
288 | AcmeClient::load(&name).await?.update_account(&data).await?; | |
289 | ||
290 | Ok(()) | |
291 | }, | |
292 | ) | |
293 | } | |
294 | ||
295 | #[api( | |
296 | input: { | |
297 | properties: { | |
39c5db7f | 298 | name: { type: AcmeAccountName }, |
d4b84c1d WB |
299 | force: { |
300 | description: | |
301 | "Delete account data even if the server refuses to deactivate the account.", | |
302 | optional: true, | |
303 | default: false, | |
304 | }, | |
305 | }, | |
306 | }, | |
307 | access: { | |
308 | permission: &Permission::Privilege(&["system", "certificates"], PRIV_SYS_MODIFY, false), | |
309 | }, | |
310 | protected: true, | |
311 | )] | |
312 | /// Deactivate an ACME account. | |
313 | pub fn deactivate_account( | |
39c5db7f | 314 | name: AcmeAccountName, |
d4b84c1d WB |
315 | force: bool, |
316 | rpcenv: &mut dyn RpcEnvironment, | |
317 | ) -> Result<String, Error> { | |
318 | let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?; | |
319 | ||
320 | WorkerTask::spawn( | |
321 | "acme-deactivate", | |
322 | None, | |
323 | auth_id, | |
324 | true, | |
325 | move |worker| async move { | |
326 | match AcmeClient::load(&name) | |
327 | .await? | |
328 | .update_account(&json!({"status": "deactivated"})) | |
329 | .await | |
330 | { | |
331 | Ok(_account) => (), | |
332 | Err(err) if !force => return Err(err), | |
333 | Err(err) => { | |
334 | worker.warn(format!( | |
ee0c5c8e | 335 | "error deactivating account {}, proceedeing anyway - {}", |
d4b84c1d WB |
336 | name, err, |
337 | )); | |
338 | } | |
339 | } | |
340 | crate::config::acme::mark_account_deactivated(&name)?; | |
341 | Ok(()) | |
342 | }, | |
343 | ) | |
344 | } | |
345 | ||
346 | #[api( | |
347 | input: { | |
348 | properties: { | |
349 | directory: { | |
350 | type: String, | |
351 | description: "The ACME Directory.", | |
352 | optional: true, | |
353 | }, | |
354 | }, | |
355 | }, | |
356 | access: { | |
357 | permission: &Permission::Anybody, | |
358 | }, | |
359 | returns: { | |
360 | type: String, | |
361 | optional: true, | |
362 | description: "The ACME Directory's ToS URL, if any.", | |
363 | }, | |
364 | )] | |
365 | /// Get the Terms of Service URL for an ACME directory. | |
366 | async fn get_tos(directory: Option<String>) -> Result<Option<String>, Error> { | |
367 | let directory = directory.unwrap_or_else(|| { | |
368 | crate::config::acme::DEFAULT_ACME_DIRECTORY_ENTRY | |
369 | .url | |
370 | .to_owned() | |
371 | }); | |
372 | Ok(AcmeClient::new(directory) | |
373 | .terms_of_service_url() | |
374 | .await? | |
375 | .map(str::to_owned)) | |
376 | } | |
377 | ||
378 | #[api( | |
379 | access: { | |
380 | permission: &Permission::Anybody, | |
381 | }, | |
382 | returns: { | |
383 | description: "List of known ACME directories.", | |
384 | type: Array, | |
385 | items: { type: KnownAcmeDirectory }, | |
386 | }, | |
387 | )] | |
388 | /// Get named known ACME directory endpoints. | |
389 | fn get_directories() -> Result<&'static [KnownAcmeDirectory], Error> { | |
390 | Ok(crate::config::acme::KNOWN_ACME_DIRECTORIES) | |
391 | } | |
392 | ||
d308dc8a TL |
393 | /// Wrapper for efficient Arc use when returning the ACME challenge-plugin schema for serializing |
394 | struct ChallengeSchemaWrapper { | |
395 | inner: Arc<Vec<AcmeChallengeSchema>>, | |
396 | } | |
397 | ||
398 | impl Serialize for ChallengeSchemaWrapper { | |
399 | fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> | |
400 | where | |
401 | S: serde::Serializer, | |
402 | { | |
403 | self.inner.serialize(serializer) | |
404 | } | |
405 | } | |
406 | ||
407 | fn get_cached_challenge_schemas() -> Result<ChallengeSchemaWrapper, Error> { | |
408 | lazy_static! { | |
409 | static ref CACHE: Mutex<Option<(Arc<Vec<AcmeChallengeSchema>>, SystemTime)>> = | |
410 | Mutex::new(None); | |
411 | } | |
412 | ||
413 | // the actual loading code | |
414 | let mut last = CACHE.lock().unwrap(); | |
415 | ||
416 | let actual_mtime = fs::metadata(crate::config::acme::ACME_DNS_SCHEMA_FN)?.modified()?; | |
417 | ||
418 | let schema = match &*last { | |
419 | Some((schema, cached_mtime)) if *cached_mtime >= actual_mtime => schema.clone(), | |
420 | _ => { | |
421 | let new_schema = Arc::new(crate::config::acme::load_dns_challenge_schema()?); | |
422 | *last = Some((Arc::clone(&new_schema), actual_mtime)); | |
423 | new_schema | |
424 | } | |
425 | }; | |
426 | ||
427 | Ok(ChallengeSchemaWrapper { inner: schema }) | |
428 | } | |
429 | ||
d4b84c1d WB |
430 | #[api( |
431 | access: { | |
432 | permission: &Permission::Anybody, | |
433 | }, | |
434 | returns: { | |
435 | description: "ACME Challenge Plugin Shema.", | |
436 | type: Array, | |
60643023 | 437 | items: { type: AcmeChallengeSchema }, |
d4b84c1d WB |
438 | }, |
439 | )] | |
440 | /// Get named known ACME directory endpoints. | |
d308dc8a TL |
441 | fn get_challenge_schema() -> Result<ChallengeSchemaWrapper, Error> { |
442 | get_cached_challenge_schemas() | |
d4b84c1d WB |
443 | } |
444 | ||
445 | #[api] | |
446 | #[derive(Default, Deserialize, Serialize)] | |
447 | #[serde(rename_all = "kebab-case")] | |
448 | /// The API's format is inherited from PVE/PMG: | |
449 | pub struct PluginConfig { | |
450 | /// Plugin ID. | |
451 | plugin: String, | |
452 | ||
453 | /// Plugin type. | |
454 | #[serde(rename = "type")] | |
455 | ty: String, | |
456 | ||
457 | /// DNS Api name. | |
458 | api: Option<String>, | |
459 | ||
460 | /// Plugin configuration data. | |
461 | data: Option<String>, | |
462 | ||
463 | /// Extra delay in seconds to wait before requesting validation. | |
464 | /// | |
465 | /// Allows to cope with long TTL of DNS records. | |
466 | #[serde(skip_serializing_if = "Option::is_none", default)] | |
467 | validation_delay: Option<u32>, | |
468 | ||
469 | /// Flag to disable the config. | |
470 | #[serde(skip_serializing_if = "Option::is_none", default)] | |
471 | disable: Option<bool>, | |
472 | } | |
473 | ||
474 | // See PMG/PVE's $modify_cfg_for_api sub | |
475 | fn modify_cfg_for_api(id: &str, ty: &str, data: &Value) -> PluginConfig { | |
476 | let mut entry = data.clone(); | |
477 | ||
478 | let obj = entry.as_object_mut().unwrap(); | |
479 | obj.remove("id"); | |
480 | obj.insert("plugin".to_string(), Value::String(id.to_owned())); | |
481 | obj.insert("type".to_string(), Value::String(ty.to_owned())); | |
482 | ||
483 | // FIXME: This needs to go once the `Updater` is fixed. | |
484 | // None of these should be able to fail unless the user changed the files by hand, in which | |
485 | // case we leave the unmodified string in the Value for now. This will be handled with an error | |
486 | // later. | |
487 | if let Some(Value::String(ref mut data)) = obj.get_mut("data") { | |
488 | if let Ok(new) = base64::decode_config(&data, base64::URL_SAFE_NO_PAD) { | |
489 | if let Ok(utf8) = String::from_utf8(new) { | |
490 | *data = utf8; | |
491 | } | |
492 | } | |
493 | } | |
494 | ||
495 | // PVE/PMG do this explicitly for ACME plugins... | |
496 | // obj.insert("digest".to_string(), Value::String(digest.clone())); | |
497 | ||
498 | serde_json::from_value(entry).unwrap_or_else(|_| PluginConfig { | |
499 | plugin: "*Error*".to_string(), | |
500 | ty: "*Error*".to_string(), | |
501 | ..Default::default() | |
502 | }) | |
503 | } | |
504 | ||
505 | #[api( | |
506 | access: { | |
507 | permission: &Permission::Privilege(&["system", "certificates"], PRIV_SYS_MODIFY, false), | |
508 | }, | |
509 | protected: true, | |
510 | returns: { | |
511 | type: Array, | |
512 | description: "List of ACME plugin configurations.", | |
513 | items: { type: PluginConfig }, | |
514 | }, | |
515 | )] | |
516 | /// List ACME challenge plugins. | |
517 | pub fn list_plugins(mut rpcenv: &mut dyn RpcEnvironment) -> Result<Vec<PluginConfig>, Error> { | |
518 | use crate::config::acme::plugin; | |
519 | ||
520 | let (plugins, digest) = plugin::config()?; | |
521 | rpcenv["digest"] = proxmox::tools::digest_to_hex(&digest).into(); | |
522 | Ok(plugins | |
523 | .iter() | |
524 | .map(|(id, (ty, data))| modify_cfg_for_api(&id, &ty, data)) | |
525 | .collect()) | |
526 | } | |
527 | ||
528 | #[api( | |
529 | input: { | |
530 | properties: { | |
531 | id: { schema: PLUGIN_ID_SCHEMA }, | |
532 | }, | |
533 | }, | |
534 | access: { | |
535 | permission: &Permission::Privilege(&["system", "certificates"], PRIV_SYS_MODIFY, false), | |
536 | }, | |
537 | protected: true, | |
538 | returns: { type: PluginConfig }, | |
539 | )] | |
540 | /// List ACME challenge plugins. | |
541 | pub fn get_plugin(id: String, mut rpcenv: &mut dyn RpcEnvironment) -> Result<PluginConfig, Error> { | |
542 | use crate::config::acme::plugin; | |
543 | ||
544 | let (plugins, digest) = plugin::config()?; | |
545 | rpcenv["digest"] = proxmox::tools::digest_to_hex(&digest).into(); | |
546 | ||
547 | match plugins.get(&id) { | |
548 | Some((ty, data)) => Ok(modify_cfg_for_api(&id, &ty, &data)), | |
549 | None => http_bail!(NOT_FOUND, "no such plugin"), | |
550 | } | |
551 | } | |
552 | ||
553 | // Currently we only have "the" standalone plugin and DNS plugins so we can just flatten a | |
554 | // DnsPluginUpdater: | |
555 | // | |
556 | // FIXME: The 'id' parameter should not be "optional" in the schema. | |
557 | #[api( | |
558 | input: { | |
559 | properties: { | |
560 | type: { | |
561 | type: String, | |
562 | description: "The ACME challenge plugin type.", | |
563 | }, | |
564 | core: { | |
565 | type: DnsPluginCoreUpdater, | |
566 | flatten: true, | |
567 | }, | |
568 | data: { | |
569 | type: String, | |
570 | // This is different in the API! | |
571 | description: "DNS plugin data (base64 encoded with padding).", | |
572 | }, | |
573 | }, | |
574 | }, | |
575 | access: { | |
576 | permission: &Permission::Privilege(&["system", "certificates"], PRIV_SYS_MODIFY, false), | |
577 | }, | |
578 | protected: true, | |
579 | )] | |
580 | /// Add ACME plugin configuration. | |
581 | pub fn add_plugin(r#type: String, core: DnsPluginCoreUpdater, data: String) -> Result<(), Error> { | |
582 | use crate::config::acme::plugin; | |
583 | ||
584 | // Currently we only support DNS plugins and the standalone plugin is "fixed": | |
585 | if r#type != "dns" { | |
586 | bail!("invalid ACME plugin type: {:?}", r#type); | |
587 | } | |
588 | ||
589 | let data = String::from_utf8(base64::decode(&data)?) | |
590 | .map_err(|_| format_err!("data must be valid UTF-8"))?; | |
591 | //core.api_fixup()?; | |
592 | ||
593 | // FIXME: Solve the Updater with non-optional fields thing... | |
594 | let id = core | |
595 | .id | |
596 | .clone() | |
597 | .ok_or_else(|| format_err!("missing required 'id' parameter"))?; | |
598 | ||
599 | let _lock = plugin::lock()?; | |
600 | ||
601 | let (mut plugins, _digest) = plugin::config()?; | |
602 | if plugins.contains_key(&id) { | |
603 | bail!("ACME plugin ID {:?} already exists", id); | |
604 | } | |
605 | ||
606 | let plugin = serde_json::to_value(DnsPlugin { | |
607 | core: DnsPluginCore::try_build_from(core)?, | |
608 | data, | |
609 | })?; | |
610 | ||
611 | plugins.insert(id, r#type, plugin); | |
612 | ||
613 | plugin::save_config(&plugins)?; | |
614 | ||
615 | Ok(()) | |
616 | } | |
617 | ||
618 | #[api( | |
619 | input: { | |
620 | properties: { | |
621 | id: { schema: PLUGIN_ID_SCHEMA }, | |
622 | }, | |
623 | }, | |
624 | access: { | |
625 | permission: &Permission::Privilege(&["system", "certificates"], PRIV_SYS_MODIFY, false), | |
626 | }, | |
627 | protected: true, | |
628 | )] | |
629 | /// Delete an ACME plugin configuration. | |
630 | pub fn delete_plugin(id: String) -> Result<(), Error> { | |
631 | use crate::config::acme::plugin; | |
632 | ||
633 | let _lock = plugin::lock()?; | |
634 | ||
635 | let (mut plugins, _digest) = plugin::config()?; | |
636 | if plugins.remove(&id).is_none() { | |
637 | http_bail!(NOT_FOUND, "no such plugin"); | |
638 | } | |
639 | plugin::save_config(&plugins)?; | |
640 | ||
641 | Ok(()) | |
642 | } | |
643 | ||
644 | #[api( | |
645 | input: { | |
646 | properties: { | |
647 | core_update: { | |
648 | type: DnsPluginCoreUpdater, | |
649 | flatten: true, | |
650 | }, | |
651 | data: { | |
652 | type: String, | |
653 | optional: true, | |
654 | // This is different in the API! | |
655 | description: "DNS plugin data (base64 encoded with padding).", | |
656 | }, | |
657 | digest: { | |
658 | description: "Digest to protect against concurrent updates", | |
659 | optional: true, | |
660 | }, | |
661 | delete: { | |
662 | description: "Options to remove from the configuration", | |
663 | optional: true, | |
664 | }, | |
665 | }, | |
666 | }, | |
667 | access: { | |
668 | permission: &Permission::Privilege(&["system", "certificates"], PRIV_SYS_MODIFY, false), | |
669 | }, | |
670 | protected: true, | |
671 | )] | |
672 | /// Update an ACME plugin configuration. | |
673 | pub fn update_plugin( | |
674 | core_update: DnsPluginCoreUpdater, | |
675 | data: Option<String>, | |
676 | delete: Option<String>, | |
677 | digest: Option<String>, | |
678 | ) -> Result<(), Error> { | |
679 | use crate::config::acme::plugin; | |
680 | ||
681 | let data = data | |
682 | .as_deref() | |
683 | .map(base64::decode) | |
684 | .transpose()? | |
685 | .map(String::from_utf8) | |
686 | .transpose() | |
687 | .map_err(|_| format_err!("data must be valid UTF-8"))?; | |
688 | //core_update.api_fixup()?; | |
689 | ||
690 | // unwrap: the id is matched by this method's API path | |
691 | let id = core_update.id.clone().unwrap(); | |
692 | ||
693 | let delete: Vec<&str> = delete | |
694 | .as_deref() | |
695 | .unwrap_or("") | |
696 | .split(&[' ', ',', ';', '\0'][..]) | |
697 | .collect(); | |
698 | ||
699 | let _lock = plugin::lock()?; | |
700 | ||
701 | let (mut plugins, expected_digest) = plugin::config()?; | |
702 | ||
703 | if let Some(digest) = digest { | |
704 | let digest = proxmox::tools::hex_to_digest(&digest)?; | |
705 | crate::tools::detect_modified_configuration_file(&digest, &expected_digest)?; | |
706 | } | |
707 | ||
708 | match plugins.get_mut(&id) { | |
709 | Some((ty, ref mut entry)) => { | |
710 | if ty != "dns" { | |
711 | bail!("cannot update plugin of type {:?}", ty); | |
712 | } | |
713 | ||
714 | let mut plugin: DnsPlugin = serde_json::from_value(entry.clone())?; | |
715 | plugin.core.update_from(core_update, &delete)?; | |
716 | if let Some(data) = data { | |
717 | plugin.data = data; | |
718 | } | |
719 | *entry = serde_json::to_value(plugin)?; | |
720 | } | |
721 | None => http_bail!(NOT_FOUND, "no such plugin"), | |
722 | } | |
723 | ||
724 | plugin::save_config(&plugins)?; | |
725 | ||
726 | Ok(()) | |
727 | } |