From 8652590fda921badeda7990384662fff0c057e2b Mon Sep 17 00:00:00 2001 From: Steve Lee Date: Thu, 7 Mar 2024 12:50:02 -0800 Subject: [PATCH 1/6] add secure structs for types --- dsc_lib/src/configure/config_doc.rs | 4 ++-- dsc_lib/src/configure/parameters.rs | 34 ++++++++++++++++++++++++++++- 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/dsc_lib/src/configure/config_doc.rs b/dsc_lib/src/configure/config_doc.rs index 08edbedb2..a3d96f2e3 100644 --- a/dsc_lib/src/configure/config_doc.rs +++ b/dsc_lib/src/configure/config_doc.rs @@ -74,7 +74,7 @@ pub struct Parameter { pub enum DataType { #[serde(rename = "string")] String, - #[serde(rename = "securestring")] + #[serde(rename = "secureString")] SecureString, #[serde(rename = "int")] Int, @@ -82,7 +82,7 @@ pub enum DataType { Bool, #[serde(rename = "object")] Object, - #[serde(rename = "secureobject")] + #[serde(rename = "secureObject")] SecureObject, #[serde(rename = "array")] Array, diff --git a/dsc_lib/src/configure/parameters.rs b/dsc_lib/src/configure/parameters.rs index efa1de740..cf45b1d85 100644 --- a/dsc_lib/src/configure/parameters.rs +++ b/dsc_lib/src/configure/parameters.rs @@ -4,9 +4,41 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use serde_json::Value; -use std::collections::HashMap; +use std::{collections::HashMap, fmt::{self, Display, Formatter}}; #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)] pub struct Input { pub parameters: HashMap, } + +pub struct SecureString { + pub value: String, +} + +impl Display for SecureString { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "SecureString") + } +} + +impl Drop for SecureString { + fn drop(&mut self) { + self.value.clear(); + } +} + +pub struct SecureObject { + pub value: Value, +} + +impl Display for SecureObject { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "SecureObject") + } +} + +impl Drop for SecureObject { + fn drop(&mut self) { + self.value = Value::Null; + } +} From 2f39c0236f52151cf2925963cd39a8267d3af854 Mon Sep 17 00:00:00 2001 From: Steve Lee Date: Fri, 8 Mar 2024 17:59:54 -0800 Subject: [PATCH 2/6] add examples --- dsc/examples/secure_parameters.dsc.yaml | 15 +++++++++++++++ dsc/examples/secure_parameters.parameters.yaml | 4 ++++ dsc_lib/src/configure/parameters.rs | 4 ++-- 3 files changed, 21 insertions(+), 2 deletions(-) create mode 100644 dsc/examples/secure_parameters.dsc.yaml create mode 100644 dsc/examples/secure_parameters.parameters.yaml diff --git a/dsc/examples/secure_parameters.dsc.yaml b/dsc/examples/secure_parameters.dsc.yaml new file mode 100644 index 000000000..061238837 --- /dev/null +++ b/dsc/examples/secure_parameters.dsc.yaml @@ -0,0 +1,15 @@ +$schema: https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2023/08/config/document.json +parameters: + myString: + type: secureString + myObject: + type: secureObject +resources: + - name: Echo 1 + type: Test/Echo + properties: + output: "[parameters('myString')]" + - name: Echo 2 + type: Test/Echo + properties: + output: "[parameters('myObject').myProperty]" diff --git a/dsc/examples/secure_parameters.parameters.yaml b/dsc/examples/secure_parameters.parameters.yaml new file mode 100644 index 000000000..2352b4e92 --- /dev/null +++ b/dsc/examples/secure_parameters.parameters.yaml @@ -0,0 +1,4 @@ +parameters: + myString: mySecret + myObject: + myProperty: mySecretProperty diff --git a/dsc_lib/src/configure/parameters.rs b/dsc_lib/src/configure/parameters.rs index cf45b1d85..13d76b741 100644 --- a/dsc_lib/src/configure/parameters.rs +++ b/dsc_lib/src/configure/parameters.rs @@ -17,7 +17,7 @@ pub struct SecureString { impl Display for SecureString { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - write!(f, "SecureString") + write!(f, "") } } @@ -33,7 +33,7 @@ pub struct SecureObject { impl Display for SecureObject { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - write!(f, "SecureObject") + write!(f, "") } } From 33daeef242c75d078039a621911a2d8e995f612e Mon Sep 17 00:00:00 2001 From: Steve Lee Date: Sun, 10 Mar 2024 16:57:42 -0700 Subject: [PATCH 3/6] add tracing --- dsc_lib/src/configure/mod.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/dsc_lib/src/configure/mod.rs b/dsc_lib/src/configure/mod.rs index 89c1c8ef2..9748e8383 100644 --- a/dsc_lib/src/configure/mod.rs +++ b/dsc_lib/src/configure/mod.rs @@ -17,7 +17,7 @@ use indicatif::ProgressStyle; use security_context_lib::{SecurityContext, get_security_context}; use serde_json::{Map, Value}; use std::{collections::HashMap, mem}; -use tracing::{debug, trace, warn_span, Span}; +use tracing::{debug, info, trace, warn_span, Span}; use tracing_indicatif::span_ext::IndicatifSpanExt; pub mod context; @@ -390,6 +390,7 @@ impl Configurator { let config = serde_json::from_str::(self.config.as_str())?; let Some(parameters) = &config.parameters else { if parameters_input.is_none() { + debug!("No parameters defined in configuration and no parameters input"); return Ok(()); } return Err(DscError::Validation("No parameters defined in configuration".to_string())); @@ -447,6 +448,7 @@ impl Configurator { }, } + info!("Set parameter '{name}' to '{value}'"); self.context.parameters.insert(name.clone(), value.clone()); } else { From 7a13298a2c4a2022a0969e071a92eccf039c7b5f Mon Sep 17 00:00:00 2001 From: Steve Lee Date: Wed, 13 Mar 2024 18:07:28 -0700 Subject: [PATCH 4/6] fix expression invocation for default parameter values --- dsc/examples/osinfo_parameters.dsc.yaml | 2 +- dsc/tests/dsc_parameters.tests.ps1 | 35 +++++++++++ dsc_lib/src/configure/context.rs | 4 +- dsc_lib/src/configure/mod.rs | 82 +++++++++++++++---------- dsc_lib/src/configure/parameters.rs | 39 +++--------- dsc_lib/src/dscresources/dscresource.rs | 52 ---------------- dsc_lib/src/functions/parameters.rs | 22 ++++++- tools/dsctest/src/args.rs | 4 +- tools/dsctest/src/echo.rs | 18 ++++++ 9 files changed, 138 insertions(+), 120 deletions(-) diff --git a/dsc/examples/osinfo_parameters.dsc.yaml b/dsc/examples/osinfo_parameters.dsc.yaml index 219e1524c..bde35970d 100644 --- a/dsc/examples/osinfo_parameters.dsc.yaml +++ b/dsc/examples/osinfo_parameters.dsc.yaml @@ -2,7 +2,7 @@ $schema: https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2023/08/c parameters: osFamily: type: string - defaultValue: Windows + defaultValue: "[concat('Win','dows')]" allowedValues: - Windows - Linux diff --git a/dsc/tests/dsc_parameters.tests.ps1 b/dsc/tests/dsc_parameters.tests.ps1 index f08b5faea..eaa1050c3 100644 --- a/dsc/tests/dsc_parameters.tests.ps1 +++ b/dsc/tests/dsc_parameters.tests.ps1 @@ -268,4 +268,39 @@ Describe 'Parameters tests' { $out.results[0].result.actualState.family | Should -BeExactly $os $out.results[0].result.inDesiredState | Should -BeTrue } + + It 'secure types can be passed as objects to resources' { + $out = dsc config -f $PSScriptRoot/../examples/secure_parameters.parameters.yaml get -p $PSScriptRoot/../examples/secure_parameters.dsc.yaml | ConvertFrom-Json + $LASTEXITCODE | Should -Be 0 + $out.results[0].result.actualState.output | Should -BeExactly 'mySecret' + $out.results[1].result.actualState.output | Should -BeExactly 'mySecretProperty' + } + + It 'paramter types are validated for ' -TestCases @( + @{ type = 'array'; value = 'hello'} + @{ type = 'bool'; value = 'hello'} + @{ type = 'int'; value = @(1,2)} + @{ type = 'object'; value = 1} + @{ type = 'secureString'; value = 1} + @{ type = 'secureObject'; value = 'hello'} + @{ type = 'string'; value = 42 } + ){ + param($type, $value) + + $config_yaml = @" + `$schema: https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2023/10/config/document.json + parameters: + param: + type: $type + resources: + - name: Echo + type: Test/Echo + properties: + output: '[parameters(''param'')]' +"@ + + $params_json = @{ parameters = @{ param = $value }} | ConvertTo-Json + $null = $config_yaml | dsc config -p $params_json get + $LASTEXITCODE | Should -Be 4 + } } diff --git a/dsc_lib/src/configure/context.rs b/dsc_lib/src/configure/context.rs index bbafde83d..7e3547eae 100644 --- a/dsc_lib/src/configure/context.rs +++ b/dsc_lib/src/configure/context.rs @@ -4,8 +4,10 @@ use serde_json::Value; use std::collections::HashMap; +use super::config_doc::DataType; + pub struct Context { - pub parameters: HashMap, + pub parameters: HashMap, pub _variables: HashMap, pub outputs: HashMap, // this is used by the `reference()` function to retrieve output } diff --git a/dsc_lib/src/configure/mod.rs b/dsc_lib/src/configure/mod.rs index 9748e8383..967923a88 100644 --- a/dsc_lib/src/configure/mod.rs +++ b/dsc_lib/src/configure/mod.rs @@ -398,9 +398,18 @@ impl Configurator { for (name, parameter) in parameters { if let Some(default_value) = ¶meter.default_value { - // TODO: default values can be expressions - // TODO: validate default value matches the type - self.context.parameters.insert(name.clone(), default_value.clone()); + // default values can be expressions + let value = if default_value.is_string() { + if let Some(value) = default_value.as_str() { + self.statement_parser.parse_and_execute(&value, &self.context)? + } else { + return Err(DscError::Parser("Default value as string is not defined".to_string())); + } + } else { + default_value.clone() + }; + Configurator::validate_parameter_type(&name, &value, ¶meter.parameter_type)?; + self.context.parameters.insert(name.clone(), (value, parameter.parameter_type.clone())); } } @@ -420,36 +429,13 @@ impl Configurator { // TODO: additional array constraints // TODO: object constraints - match constraint.parameter_type { - DataType::String | DataType::SecureString => { - if !value.is_string() { - return Err(DscError::Validation(format!("Parameter '{name}' is not a string"))); - } - }, - DataType::Int => { - if !value.is_i64() { - return Err(DscError::Validation(format!("Parameter '{name}' is not an integer"))); - } - }, - DataType::Bool => { - if !value.is_boolean() { - return Err(DscError::Validation(format!("Parameter '{name}' is not a boolean"))); - } - }, - DataType::Array => { - if !value.is_array() { - return Err(DscError::Validation(format!("Parameter '{name}' is not an array"))); - } - }, - DataType::Object | DataType::SecureObject => { - if !value.is_object() { - return Err(DscError::Validation(format!("Parameter '{name}' is not an object"))); - } - }, + Configurator::validate_parameter_type(&name, &value, &constraint.parameter_type)?; + if constraint.parameter_type == DataType::SecureString || constraint.parameter_type == DataType::SecureObject { + info!("Set secure parameter '{name}'"); + } else { + info!("Set parameter '{name}' to '{value}'"); } - - info!("Set parameter '{name}' to '{value}'"); - self.context.parameters.insert(name.clone(), value.clone()); + self.context.parameters.insert(name.clone(), (value.clone(), constraint.parameter_type.clone())); } else { return Err(DscError::Validation(format!("Parameter '{name}' not defined in configuration"))); @@ -458,6 +444,38 @@ impl Configurator { Ok(()) } + fn validate_parameter_type(name: &str, value: &Value, parameter_type: &DataType) -> Result<(), DscError> { + match parameter_type { + DataType::String | DataType::SecureString => { + if !value.is_string() { + return Err(DscError::Validation(format!("Parameter '{name}' is not a string"))); + } + }, + DataType::Int => { + if !value.is_i64() { + return Err(DscError::Validation(format!("Parameter '{name}' is not an integer"))); + } + }, + DataType::Bool => { + if !value.is_boolean() { + return Err(DscError::Validation(format!("Parameter '{name}' is not a boolean"))); + } + }, + DataType::Array => { + if !value.is_array() { + return Err(DscError::Validation(format!("Parameter '{name}' is not an array"))); + } + }, + DataType::Object | DataType::SecureObject => { + if !value.is_object() { + return Err(DscError::Validation(format!("Parameter '{name}' is not an object"))); + } + }, + } + + Ok(()) + } + fn validate_config(&mut self) -> Result { let config: Configuration = serde_json::from_str(self.config.as_str())?; check_security_context(&config.metadata)?; diff --git a/dsc_lib/src/configure/parameters.rs b/dsc_lib/src/configure/parameters.rs index 13d76b741..62f95158c 100644 --- a/dsc_lib/src/configure/parameters.rs +++ b/dsc_lib/src/configure/parameters.rs @@ -4,41 +4,18 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use serde_json::Value; -use std::{collections::HashMap, fmt::{self, Display, Formatter}}; +use std::collections::HashMap; #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)] pub struct Input { pub parameters: HashMap, } -pub struct SecureString { - pub value: String, -} - -impl Display for SecureString { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - write!(f, "") - } -} - -impl Drop for SecureString { - fn drop(&mut self) { - self.value.clear(); - } -} - -pub struct SecureObject { - pub value: Value, -} - -impl Display for SecureObject { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - write!(f, "") - } -} - -impl Drop for SecureObject { - fn drop(&mut self) { - self.value = Value::Null; - } +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)] +#[serde(deny_unknown_fields, untagged)] +pub enum SecureKind { + #[serde(rename = "secureString")] + SecureString(String), + #[serde(rename = "secureObject")] + SecureObject(Value), } diff --git a/dsc_lib/src/dscresources/dscresource.rs b/dsc_lib/src/dscresources/dscresource.rs index 2ca9f990c..1a588d760 100644 --- a/dsc_lib/src/dscresources/dscresource.rs +++ b/dsc_lib/src/dscresources/dscresource.rs @@ -7,7 +7,6 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use serde_json::Value; use std::collections::HashMap; -use tracing::{debug, trace}; use super::{command_resource, dscerror, invoke_result::{ExportResult, GetResult, ResourceTestResponse, SetResult, TestResult, ValidateResult}, resource_manifest::import_manifest}; @@ -77,54 +76,6 @@ impl DscResource { manifest: None, } } - - fn validate_input(&self, input: &str) -> Result<(), DscError> { - debug!("Validating input for resource: {}", &self.type_name); - if input.is_empty() { - return Ok(()); - } - let Some(manifest) = &self.manifest else { - return Err(DscError::MissingManifest(self.type_name.clone())); - }; - let resource_manifest = import_manifest(manifest.clone())?; - - if resource_manifest.validate.is_some() { - trace!("Using custom validation"); - let validation_result = match self.validate(input) { - Ok(validation_result) => validation_result, - Err(err) => { - return Err(DscError::Validation(format!("Validation failed: {err}"))); - }, - }; - trace!("Validation result is valid: {}", validation_result.valid); - if !validation_result.valid { - return Err(DscError::Validation("Validation failed".to_string())); - } - } - else { - trace!("Using JSON schema validation"); - let Ok(schema) = self.schema() else { - return Err(DscError::Validation("Schema not available".to_string())); - }; - - let schema = serde_json::from_str::(&schema)?; - - let Ok(compiled_schema) = jsonschema::JSONSchema::compile(&schema) else { - return Err(DscError::Validation("Schema compilation failed".to_string())); - }; - - let input = serde_json::from_str::(input)?; - if let Err(err) = compiled_schema.validate(&input) { - let mut error = format!("Resource '{}' failed validation: ", self.type_name); - for e in err { - error.push_str(&format!("\n{e} ")); - } - return Err(DscError::Validation(error)); - }; - } - - Ok(()) - } } impl Default for DscResource { @@ -201,7 +152,6 @@ pub trait Invoke { impl Invoke for DscResource { fn get(&self, filter: &str) -> Result { - self.validate_input(filter)?; match &self.implemented_as { ImplementedAs::Custom(_custom) => { Err(DscError::NotImplemented("get custom resources".to_string())) @@ -217,7 +167,6 @@ impl Invoke for DscResource { } fn set(&self, desired: &str, skip_test: bool) -> Result { - self.validate_input(desired)?; match &self.implemented_as { ImplementedAs::Custom(_custom) => { Err(DscError::NotImplemented("set custom resources".to_string())) @@ -233,7 +182,6 @@ impl Invoke for DscResource { } fn test(&self, expected: &str) -> Result { - self.validate_input(expected)?; match &self.implemented_as { ImplementedAs::Custom(_custom) => { Err(DscError::NotImplemented("test custom resources".to_string())) diff --git a/dsc_lib/src/functions/parameters.rs b/dsc_lib/src/functions/parameters.rs index de01ffe08..51e4533af 100644 --- a/dsc_lib/src/functions/parameters.rs +++ b/dsc_lib/src/functions/parameters.rs @@ -1,6 +1,8 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +use crate::configure::config_doc::DataType; +use crate::configure::parameters::SecureKind; use crate::DscError; use crate::configure::context::Context; use crate::functions::{AcceptedArgKind, Function}; @@ -28,7 +30,25 @@ impl Function for Parameters { if let Some(key) = args[0].as_str() { trace!("parameters key: {key}"); if context.parameters.contains_key(key) { - Ok(context.parameters[key].clone()) + let (value, data_type) = &context.parameters[key]; + + // if secureString or secureObject types, we keep it as JSON object + match data_type { + DataType::SecureString => { + let Some(value) = value.as_str() else { + return Err(DscError::Parser(format!("Parameter '{key}' is not a string"))); + }; + let secure_string = SecureKind::SecureString(value.to_string()); + Ok(serde_json::to_value(secure_string)?) + }, + DataType::SecureObject => { + let secure_object = SecureKind::SecureObject(value.clone()); + Ok(serde_json::to_value(secure_object)?) + }, + _ => { + Ok(value.clone()) + } + } } else { Err(DscError::Parser(format!("Parameter '{key}' not found in context"))) diff --git a/tools/dsctest/src/args.rs b/tools/dsctest/src/args.rs index 7acb03e94..6bf2ab0f9 100644 --- a/tools/dsctest/src/args.rs +++ b/tools/dsctest/src/args.rs @@ -21,7 +21,7 @@ pub struct Args { pub enum SubCommand { #[clap(name = "echo", about = "Return the input")] Echo { - #[clap(name = "input", short, long, help = "The input to the echo command")] + #[clap(name = "input", short, long, help = "The input to the echo command as JSON")] input: String, }, @@ -33,7 +33,7 @@ pub enum SubCommand { #[clap(name = "sleep", about = "Sleep for a specified number of seconds")] Sleep { - #[clap(name = "input", short, long, help = "The input to the sleep command")] + #[clap(name = "input", short, long, help = "The input to the sleep command as JSON")] input: String, }, } diff --git a/tools/dsctest/src/echo.rs b/tools/dsctest/src/echo.rs index 64d762598..10cf9612f 100644 --- a/tools/dsctest/src/echo.rs +++ b/tools/dsctest/src/echo.rs @@ -5,6 +5,18 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use serde_json::Value; +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)] +pub struct SecureString { + #[serde(rename = "secureString")] + pub value: String, +} + +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)] +pub struct SecureObject { + #[serde(rename = "secureObject")] + pub value: Value, +} + #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)] #[serde(untagged)] pub enum Output { @@ -14,6 +26,12 @@ pub enum Output { Bool(bool), #[serde(rename = "number")] Number(i64), + #[serde(rename = "object")] + Object(Value), + #[serde(rename = "secureObject")] + SecureObject(Value), + #[serde(rename = "secureString")] + SecureString(String), #[serde(rename = "string")] String(String), } From e2e00c1dc9f12a8b95258d98addb40fe5672186e Mon Sep 17 00:00:00 2001 From: Steve Lee Date: Wed, 13 Mar 2024 18:20:50 -0700 Subject: [PATCH 5/6] fix clippy --- dsc_lib/src/configure/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dsc_lib/src/configure/mod.rs b/dsc_lib/src/configure/mod.rs index 967923a88..44847f57e 100644 --- a/dsc_lib/src/configure/mod.rs +++ b/dsc_lib/src/configure/mod.rs @@ -401,14 +401,14 @@ impl Configurator { // default values can be expressions let value = if default_value.is_string() { if let Some(value) = default_value.as_str() { - self.statement_parser.parse_and_execute(&value, &self.context)? + self.statement_parser.parse_and_execute(value, &self.context)? } else { return Err(DscError::Parser("Default value as string is not defined".to_string())); } } else { default_value.clone() }; - Configurator::validate_parameter_type(&name, &value, ¶meter.parameter_type)?; + Configurator::validate_parameter_type(name, &value, ¶meter.parameter_type)?; self.context.parameters.insert(name.clone(), (value, parameter.parameter_type.clone())); } } From aed741ba871b2575f1c52fa6eac1197b450b5394 Mon Sep 17 00:00:00 2001 From: Steve Lee Date: Thu, 14 Mar 2024 09:57:18 -0700 Subject: [PATCH 6/6] Update dsc/tests/dsc_parameters.tests.ps1 Co-authored-by: Tess Gauthier --- dsc/tests/dsc_parameters.tests.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dsc/tests/dsc_parameters.tests.ps1 b/dsc/tests/dsc_parameters.tests.ps1 index eaa1050c3..7841fcb06 100644 --- a/dsc/tests/dsc_parameters.tests.ps1 +++ b/dsc/tests/dsc_parameters.tests.ps1 @@ -276,7 +276,7 @@ Describe 'Parameters tests' { $out.results[1].result.actualState.output | Should -BeExactly 'mySecretProperty' } - It 'paramter types are validated for ' -TestCases @( + It 'parameter types are validated for ' -TestCases @( @{ type = 'array'; value = 'hello'} @{ type = 'bool'; value = 'hello'} @{ type = 'int'; value = @(1,2)}