diff --git a/sources/api/schnauzer/src/helpers/mod.rs b/sources/api/schnauzer/src/helpers/mod.rs index 145f3c1a6..4b47b37be 100644 --- a/sources/api/schnauzer/src/helpers/mod.rs +++ b/sources/api/schnauzer/src/helpers/mod.rs @@ -22,7 +22,7 @@ use url::Url; pub mod stdlib; pub use stdlib::{ - any_enabled, base64_decode, default, goarch, join_array, join_map, negate_or_else, + any_enabled, base64_decode, default, goarch, join_array, join_map, negate_or_else, toml_encode, IfNotNullHelper, IsArray, IsBool, IsNull, IsNumber, IsObject, IsString, }; @@ -282,6 +282,18 @@ mod error { rps: handlebars::JsonValue, burst: handlebars::JsonValue, }, + + #[snafu(display( + "Unable to encode input '{}' from template '{}' as toml: {}", + value, + source, + template + ))] + TomlEncode { + value: serde_json::Value, + source: serde_json::Error, + template: String, + }, } // Handlebars helpers are required to return a RenderError. diff --git a/sources/api/schnauzer/src/helpers/stdlib/mod.rs b/sources/api/schnauzer/src/helpers/stdlib/mod.rs index dc0edefd6..c2f1d5030 100644 --- a/sources/api/schnauzer/src/helpers/stdlib/mod.rs +++ b/sources/api/schnauzer/src/helpers/stdlib/mod.rs @@ -1130,3 +1130,130 @@ mod test_negate_or_else { }); } } + +/// `toml_encode` accepts arbitrary input and encodes it as a toml string +/// +/// # Example +/// +/// Consider an array of values: `[ "a", "b", "c" ]` stored in a setting such as +/// `settings.somewhere.foo-list`. In our template we can write: +/// `{{ toml_encode settings.somewhere.foo-list }}` +/// +/// This will render `["a", "b", "c"]`. +/// +/// Similarly, for a string: `"string"`, the template {{ toml-encode "string" }} +/// will render `"string"`. +pub fn toml_encode( + helper: &Helper<'_, '_>, + _: &Handlebars, + _: &Context, + renderctx: &mut RenderContext<'_, '_>, + out: &mut dyn Output, +) -> Result<(), RenderError> { + trace!("Starting toml_encode helper"); + let template_name = template_name(renderctx); + check_param_count(helper, template_name, 1)?; + + // get the string + let encode_param = get_param(helper, 0)?; + let toml_value: toml::Value = + serde_json::from_value(encode_param.to_owned()).with_context(|_| { + error::TomlEncodeSnafu { + value: encode_param.to_owned(), + template: template_name, + } + })?; + + let result = toml_value.to_string(); + + // write it to the template + out.write(&result) + .with_context(|_| error::TemplateWriteSnafu { + template: template_name.to_owned(), + })?; + + Ok(()) +} + +#[cfg(test)] +mod test_toml_encode { + use crate::helpers::toml_encode; + use handlebars::{Handlebars, RenderError}; + use serde::Serialize; + use serde_json::json; + + // A thin wrapper around the handlebars render_template method that includes + // setup and registration of helpers + fn setup_and_render_template(tmpl: &str, data: &T) -> Result + where + T: Serialize, + { + let mut registry = Handlebars::new(); + registry.register_helper("toml_encode", Box::new(toml_encode)); + + registry.render_template(tmpl, data) + } + + const TEMPLATE: &str = r#"{{ toml_encode settings.foo-string }}"#; + + #[test] + fn toml_encode_map() { + let result = setup_and_render_template( + TEMPLATE, + &json!({"settings": {"foo-string": {"hello": "world"}}}), + ) + .unwrap(); + let expected = r#"{ hello = "world" }"#; + assert_eq!(result, expected); + } + + #[test] + fn toml_encode_empty() { + let result = + setup_and_render_template(TEMPLATE, &json!({"settings": {"foo-string": []}})).unwrap(); + let expected = r#"[]"#; + assert_eq!(result, expected); + } + + #[test] + fn toml_encode_empty_string() { + let result = + setup_and_render_template(TEMPLATE, &json!({"settings": {"foo-string": [""]}})) + .unwrap(); + let expected = r#"[""]"#; + assert_eq!(result, expected); + } + + #[test] + fn toml_encode_toml_injection_1() { + let result = setup_and_render_template( + TEMPLATE, + &json!({"settings": {"foo-string": [ "apiclient set motd=hello', 'echo pwned\""]}}), + ) + .unwrap(); + let expected = "['''apiclient set motd=hello', 'echo pwned\"''']"; + assert_eq!(result, expected); + } + + #[test] + fn toml_encode_toml_injection_2() { + let result = setup_and_render_template( + TEMPLATE, + &json!({"settings": {"foo-string": [ "apiclient set motd=hello\", \"echo pwned\""]}}), + ) + .unwrap(); + let expected = "['apiclient set motd=hello\", \"echo pwned\"']"; + assert_eq!(result, expected); + } + + #[test] + fn toml_encode_toml_injection_3() { + let result = setup_and_render_template( + TEMPLATE, + &json!({"settings": {"foo-string": [ "apiclient set motd=hello\", \"echo pwned\", 'echo pwned2'"]}}), + ) + .unwrap(); + let expected = "[\"apiclient set motd=hello\\\", \\\"echo pwned\\\", 'echo pwned2'\"]"; + assert_eq!(result, expected); + } +} diff --git a/sources/api/schnauzer/src/v1.rs b/sources/api/schnauzer/src/v1.rs index 1351cf3e6..49a33948e 100644 --- a/sources/api/schnauzer/src/v1.rs +++ b/sources/api/schnauzer/src/v1.rs @@ -125,6 +125,7 @@ pub fn build_template_registry() -> Result> { template_registry.register_helper("host", Box::new(helpers::host)); template_registry.register_helper("goarch", Box::new(helpers::goarch)); template_registry.register_helper("join_array", Box::new(helpers::join_array)); + template_registry.register_helper("toml_encode", Box::new(helpers::toml_encode)); template_registry.register_helper("kube_reserve_cpu", Box::new(helpers::kube_reserve_cpu)); template_registry.register_helper( "kube_reserve_memory", diff --git a/sources/api/schnauzer/src/v2/import/helpers.rs b/sources/api/schnauzer/src/v2/import/helpers.rs index 621489e30..9fd750696 100644 --- a/sources/api/schnauzer/src/v2/import/helpers.rs +++ b/sources/api/schnauzer/src/v2/import/helpers.rs @@ -68,6 +68,7 @@ fn all_helpers() -> HashMap helper!(handlebars_helpers::any_enabled), "base64_decode" => helper!(handlebars_helpers::base64_decode), "default" => helper!(handlebars_helpers::default), + "toml_encode" => helper!(handlebars_helpers::toml_encode), "join_array" => helper!(handlebars_helpers::join_array), "join_map" => helper!(handlebars_helpers::join_map), "if_not_null" => Box::new(handlebars_helpers::IfNotNullHelper),