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