diff --git a/man/en/acmed.toml.5 b/man/en/acmed.toml.5 index 2b449f4..7754f4d 100644 --- a/man/en/acmed.toml.5 +++ b/man/en/acmed.toml.5 @@ -321,11 +321,13 @@ section. Default is 30d. Array containing the path to root certificates that should be added to the trust store. .El .It Ic group -Table of array allowing to group several hooks as one. A group is considered as new hook. The array contains the names of the hooks that are grouped. The hooks are guaranteed to be called sequentially in the declaration order. +Table of array allowing to group several hooks as one. A group is considered as new hook. The array contains the names of the hooks that are grouped. The hooks are guaranteed to be called sequentially in the declaration order. The group name must not start with +.Dq internal: . .It Ic hook Table where each element defines a command that will be launched at a defined point. See section .Sx WRITING A HOOK -for more details. +for more details. The hook name must not start with +.Dq internal: . .Bl -tag .It Cm allow_failure Ar boolean Defines if an error return value for this hook is allowed or not. If not allowed, a failure in this hook will fail the whole certificate request process. Default is false. diff --git a/src/config.rs b/src/config.rs index 587a895..24c6994 100644 --- a/src/config.rs +++ b/src/config.rs @@ -16,6 +16,7 @@ pub use rate_limit::*; use anyhow::{Context, Result}; use config::{Config, File}; +use serde::{de, Deserialize, Deserializer}; use serde_derive::Deserialize; use std::collections::HashMap; use std::path::{Path, PathBuf}; @@ -24,6 +25,7 @@ use walkdir::WalkDir; const ALLOWED_FILE_EXT: &[&str] = &["toml"]; #[derive(Debug, Deserialize)] +#[serde(remote = "Self")] #[serde(deny_unknown_fields)] pub struct AcmedConfig { pub(in crate::config) global: Option, @@ -41,6 +43,26 @@ pub struct AcmedConfig { pub(in crate::config) certificate: Vec, } +impl<'de> Deserialize<'de> for AcmedConfig { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let unchecked = AcmedConfig::deserialize(deserializer)?; + for key in unchecked.hook.keys() { + if key.starts_with(crate::INTERNAL_HOOK_PREFIX) { + return Err(de::Error::custom(format!("{key}: invalid hook name"))); + } + } + for key in unchecked.group.keys() { + if key.starts_with(crate::INTERNAL_HOOK_PREFIX) { + return Err(de::Error::custom(format!("{key}: invalid group name"))); + } + } + Ok(unchecked) + } +} + pub fn load>(config_dir: P) -> Result { let config_dir = config_dir.as_ref(); tracing::debug!("loading config directory: {config_dir:?}"); @@ -195,4 +217,41 @@ mod tests { assert!(account.signature_algorithm.is_none()); assert!(cfg.certificate.is_empty()); } + + #[test] + fn invalid_hook_name() { + let cfg = r#" +[hook."internal:hook"] +cmd = "cat" +type = ["file-pre-edit"] +"#; + let res = load_str::(cfg); + assert!(res.is_err()); + } + + #[test] + fn invalid_group_name() { + let cfg = r#" +[hook."test"] +cmd = "cat" +type = ["file-pre-edit"] +[group] +internal:grp = ["test"] +"#; + let res = load_str::(cfg); + assert!(res.is_err()); + } + + #[test] + fn valid_group_name() { + let cfg = r#" +[hook."internaltest"] +cmd = "cat" +type = ["file-pre-edit"] +[group] +internal-grp = ["internaltest"] +"#; + let res = load_str::(cfg); + assert!(res.is_ok()); + } } diff --git a/src/main.rs b/src/main.rs index 082d33a..98d6376 100644 --- a/src/main.rs +++ b/src/main.rs @@ -14,6 +14,7 @@ use std::process; pub const APP_IDENTITY: &[u8] = b"acmed\0"; pub const APP_THREAD_NAME: &str = "acmed-runtime"; pub const DEFAULT_LOG_LEVEL: log::Level = log::Level::Warn; +pub const INTERNAL_HOOK_PREFIX: &str = "internal:"; fn main() { // CLI