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/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/tests/dsc_parameters.tests.ps1 b/dsc/tests/dsc_parameters.tests.ps1 index f08b5faea..7841fcb06 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 'parameter 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/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/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 89c1c8ef2..44847f57e 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())); @@ -397,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())); } } @@ -419,35 +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}'"); } - - 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"))); @@ -456,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 efa1de740..62f95158c 100644 --- a/dsc_lib/src/configure/parameters.rs +++ b/dsc_lib/src/configure/parameters.rs @@ -10,3 +10,12 @@ use std::collections::HashMap; pub struct Input { pub parameters: HashMap, } + +#[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/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), }