Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion dsc/examples/osinfo_parameters.dsc.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
15 changes: 15 additions & 0 deletions dsc/examples/secure_parameters.dsc.yaml
Original file line number Diff line number Diff line change
@@ -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]"
4 changes: 4 additions & 0 deletions dsc/examples/secure_parameters.parameters.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
parameters:
myString: mySecret
myObject:
myProperty: mySecretProperty
35 changes: 35 additions & 0 deletions dsc/tests/dsc_parameters.tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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 <type>' -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
}
}
4 changes: 2 additions & 2 deletions dsc_lib/src/configure/config_doc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,15 +74,15 @@ pub struct Parameter {
pub enum DataType {
#[serde(rename = "string")]
String,
#[serde(rename = "securestring")]
#[serde(rename = "secureString")]
SecureString,
#[serde(rename = "int")]
Int,
#[serde(rename = "bool")]
Bool,
#[serde(rename = "object")]
Object,
#[serde(rename = "secureobject")]
#[serde(rename = "secureObject")]
SecureObject,
#[serde(rename = "array")]
Array,
Expand Down
4 changes: 3 additions & 1 deletion dsc_lib/src/configure/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@
use serde_json::Value;
use std::collections::HashMap;

use super::config_doc::DataType;

pub struct Context {
pub parameters: HashMap<String, Value>,
pub parameters: HashMap<String, (Value, DataType)>,
pub _variables: HashMap<String, Value>,
pub outputs: HashMap<String, Value>, // this is used by the `reference()` function to retrieve output
}
Expand Down
84 changes: 52 additions & 32 deletions dsc_lib/src/configure/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -390,16 +390,26 @@ impl Configurator {
let config = serde_json::from_str::<Configuration>(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()));
};

for (name, parameter) in parameters {
if let Some(default_value) = &parameter.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, &parameter.parameter_type)?;
self.context.parameters.insert(name.clone(), (value, parameter.parameter_type.clone()));
}
}

Expand All @@ -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")));
Expand All @@ -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<Configuration, DscError> {
let config: Configuration = serde_json::from_str(self.config.as_str())?;
check_security_context(&config.metadata)?;
Expand Down
9 changes: 9 additions & 0 deletions dsc_lib/src/configure/parameters.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,12 @@ use std::collections::HashMap;
pub struct Input {
pub parameters: HashMap<String, Value>,
}

#[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),
}
52 changes: 0 additions & 52 deletions dsc_lib/src/dscresources/dscresource.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand Down Expand Up @@ -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::<Value>(&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::<Value>(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 {
Expand Down Expand Up @@ -201,7 +152,6 @@ pub trait Invoke {

impl Invoke for DscResource {
fn get(&self, filter: &str) -> Result<GetResult, DscError> {
self.validate_input(filter)?;
match &self.implemented_as {
ImplementedAs::Custom(_custom) => {
Err(DscError::NotImplemented("get custom resources".to_string()))
Expand All @@ -217,7 +167,6 @@ impl Invoke for DscResource {
}

fn set(&self, desired: &str, skip_test: bool) -> Result<SetResult, DscError> {
self.validate_input(desired)?;
match &self.implemented_as {
ImplementedAs::Custom(_custom) => {
Err(DscError::NotImplemented("set custom resources".to_string()))
Expand All @@ -233,7 +182,6 @@ impl Invoke for DscResource {
}

fn test(&self, expected: &str) -> Result<TestResult, DscError> {
self.validate_input(expected)?;
match &self.implemented_as {
ImplementedAs::Custom(_custom) => {
Err(DscError::NotImplemented("test custom resources".to_string()))
Expand Down
22 changes: 21 additions & 1 deletion dsc_lib/src/functions/parameters.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand Down Expand Up @@ -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")))
Expand Down
4 changes: 2 additions & 2 deletions tools/dsctest/src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},

Expand All @@ -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,
},
}
Loading