Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
57 changes: 57 additions & 0 deletions dsc/tests/dsc_adaptedResource.tests.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.

Describe 'Tests for adapted resources' {
BeforeAll {
$isAdmin = if ($IsWindows) {
$identity = [System.Security.Principal.WindowsIdentity]::GetCurrent()
[System.Security.Principal.WindowsPrincipal]::new($identity).IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator)
}
else {
[System.Environment]::UserName -eq 'root'
}
}

It 'Security context <securityContext> for operation <operation> works' -TestCases @(
@{ securityContext = 'Elevated'; operation = 'get' },
@{ securityContext = 'Elevated'; operation = 'test' },
@{ securityContext = 'Elevated'; operation = 'set' },
@{ securityContext = 'Elevated'; operation = 'delete' },
@{ securityContext = 'Elevated'; operation = 'export' },
@{ securityContext = 'Restricted'; operation = 'get' },
@{ securityContext = 'Restricted'; operation = 'test' },
@{ securityContext = 'Restricted'; operation = 'set' },
@{ securityContext = 'Restricted'; operation = 'delete' },
@{ securityContext = 'Restricted'; operation = 'export' },
@{ securityContext = 'Current'; operation = 'get' },
@{ securityContext = 'Current'; operation = 'test' },
@{ securityContext = 'Current'; operation = 'set' },
@{ securityContext = 'Current'; operation = 'delete' },
@{ securityContext = 'Current'; operation = 'export' }
) {
param($securityContext, $operation)

$resourceType = "Adapted/SecurityContext$securityContext"
$json = @{
one = $securityContext
} | ConvertTo-Json -Compress
$out = dsc -l trace resource $operation -r $resourceType 2>$TestDrive/error.txt -i $json
$errorTxt = Get-Content -Path $TestDrive/error.txt -Raw
if ($securityContext -eq 'Elevated' -and !$isAdmin) {
$LASTEXITCODE | Should -Be 2 -Because $errorTxt
$errorTxt | Should -BeLike "*Operation '$operation' for resource 'Adapted/SecurityContext$securityContext' requires security context 'elevated'*"
}
elseif ($securityContext -eq 'Restricted' -and $isAdmin) {
$LASTEXITCODE | Should -Be 2
$errorTxt | Should -BeLike "*Operation '$operation' for resource 'Adapted/SecurityContext$securityContext' requires security context 'restricted'*"
}
else {
$LASTEXITCODE | Should -Be 0
if ($operation -ne 'delete') {
$out | Should -Not -BeNullOrEmpty
} else {
$out | Should -BeNullOrEmpty
}
}
}
}
1 change: 1 addition & 0 deletions lib/dsc-lib/src/discovery/command_discovery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -902,6 +902,7 @@ pub fn load_adapted_resource_manifest(path: &Path, manifest: &AdaptedDscResource
resource.require_adapter = Some(manifest.require_adapter.clone());
resource.directory = directory.to_path_buf();
resource.manifest = None;
resource.adapted_manifest = Some(manifest.clone());
resource.schema = Some(manifest.schema.clone());

Ok(resource)
Expand Down
55 changes: 52 additions & 3 deletions lib/dsc-lib/src/dscresources/adapted_resource_manifest.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

use crate::{dscresources::{
dscresource::Capability, resource_manifest::{Kind, ResourceManifest}
}, types::ResourceVersion};
use crate::{
configure::config_doc::SecurityContextKind,
dscresources::{
dscresource::Capability, resource_manifest::{Kind, ResourceManifest}
},
types::ResourceVersion};
use crate::{
schemas::dsc_repo::DscRepoSchema,
types::FullyQualifiedTypeName,
Expand All @@ -21,6 +24,42 @@ pub enum AdaptedPathOrContent {
Content(Map<String, Value>),
}

#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema, DscRepoSchema)]
#[dsc_repo_schema(base_name = "manifest.get", folder_path = "resource/adapted")]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub struct GetOperation {
pub require_security_context: Option<SecurityContextKind>,
}

#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema, DscRepoSchema)]
#[dsc_repo_schema(base_name = "manifest.set", folder_path = "resource/adapted")]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub struct SetOperation {
pub require_security_context: Option<SecurityContextKind>,
}

#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema, DscRepoSchema)]
#[dsc_repo_schema(base_name = "manifest.delete", folder_path = "resource/adapted")]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub struct DeleteOperation {
pub require_security_context: Option<SecurityContextKind>,
}

#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema, DscRepoSchema)]
#[dsc_repo_schema(base_name = "manifest.test", folder_path = "resource/adapted")]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub struct TestOperation {
pub require_security_context: Option<SecurityContextKind>,
}

#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema, DscRepoSchema)]
#[dsc_repo_schema(base_name = "manifest.export", folder_path = "resource/adapted")]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub struct ExportOperation {
pub require_security_context: Option<SecurityContextKind>,
}


#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema, DscRepoSchema)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
#[dsc_repo_schema(
Expand All @@ -47,6 +86,16 @@ pub struct AdaptedDscResourceManifest {
pub version: ResourceVersion,
/// The capabilities of the resource.
pub capabilities: Vec<Capability>,
/// Properties of the Get operation for the resource.
pub get: Option<GetOperation>,
/// Properties of the Set operation for the resource.
pub set: Option<SetOperation>,
/// Properties of the Delete operation for the resource.
pub delete: Option<DeleteOperation>,
/// Properties of the Test operation for the resource.
pub test: Option<TestOperation>,
/// Properties of the Export operation for the resource.
pub export: Option<ExportOperation>,
/// An optional condition for the resource to be active.
pub condition: Option<String>,
/// An optional message indicating the resource is deprecated. If provided, the message will be shown when the resource is used.
Expand Down
77 changes: 66 additions & 11 deletions lib/dsc-lib/src/dscresources/command_resource.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use rust_i18n::t;
use serde::Deserialize;
use serde_json::{Map, Value};
use std::{collections::HashMap, env, path::Path, process::Stdio};
use crate::{configure::{config_doc::{ExecutionKind, SecurityContextKind}, config_result::{ResourceGetResult, ResourceTestResult}, schema_cache::{get_resource_schema, RESOURCE_SCHEMAS}}, dscresources::resource_manifest::{ExportSchemaKind, ExportSchemaOrFiltering, SchemaArgKind}, types::ExitCodesMap, util::canonicalize_which};
use crate::{configure::{config_doc::{ExecutionKind, SecurityContextKind}, config_result::{ResourceGetResult, ResourceTestResult}, schema_cache::{RESOURCE_SCHEMAS, get_resource_schema}}, dscresources::{dscresource::Operation, resource_manifest::{ExportSchemaKind, ExportSchemaOrFiltering, SchemaArgKind}}, types::ExitCodesMap, util::canonicalize_which};
use crate::dscerror::DscError;
use crate::locked_insert;
use super::{
Expand All @@ -31,8 +31,9 @@ pub const EXIT_PROCESS_TERMINATED: i32 = 0x102;
///
/// # Arguments
///
/// * `resource` - The resource manifest
/// * `resource` - The resource
/// * `filter` - The filter to apply to the resource in JSON
/// * `target_resource` - The target resource, if applicable
///
/// # Errors
///
Expand All @@ -50,7 +51,7 @@ pub fn invoke_get(resource: &DscResource, filter: &str, target_resource: Option<
Some(target) => target,
None => resource
};
validate_security_context(&get.require_security_context, &command_resource.type_name, "get")?;
validate_security_context(target_resource, &get.require_security_context, &command_resource.type_name, &Operation::Get)?;
let args = process_get_args(get.args.as_ref(), filter, command_resource);
if !filter.is_empty() {
verify_json_from_manifest(resource, filter, target_resource)?;
Expand Down Expand Up @@ -86,9 +87,11 @@ pub fn invoke_get(resource: &DscResource, filter: &str, target_resource: Option<
///
/// # Arguments
///
/// * `resource` - The resource manifest
/// * `resource` - The resource
/// * `desired` - The desired state of the resource in JSON
/// * `skip_test` - If true, skip the test and directly invoke the set operation
/// * `execution_type` - Whether this is an actual set or what-if
/// * `target_resource` - The target resource, if applicable
///
/// # Errors
///
Expand Down Expand Up @@ -137,7 +140,7 @@ pub fn invoke_set(resource: &DscResource, desired: &str, skip_test: bool, execut
let Some(set) = set_method.as_ref() else {
return Err(DscError::NotImplemented("set".to_string()));
};
validate_security_context(&set.require_security_context, &command_resource.type_name, "set")?;
validate_security_context(target_resource, &set.require_security_context, &command_resource.type_name, &Operation::Set)?;
verify_json_from_manifest(resource, desired, target_resource)?;

// if resource doesn't implement a pre-test, we execute test first to see if a set is needed
Expand Down Expand Up @@ -182,7 +185,7 @@ pub fn invoke_set(resource: &DscResource, desired: &str, skip_test: bool, execut
Some(r) => r,
None => resource,
};
validate_security_context(&get.require_security_context, &command_resource.type_name, "get")?;
validate_security_context(target_resource, &get.require_security_context, &command_resource.type_name, &Operation::Get)?;
let args = process_get_args(get.args.as_ref(), desired, command_resource);
let command_input = get_command_input(get.input.as_ref(), desired)?;

Expand Down Expand Up @@ -317,6 +320,7 @@ pub fn invoke_set(resource: &DscResource, desired: &str, skip_test: bool, execut
///
/// * `resource` - The resource manifest for the command resource.
/// * `expected` - The expected state of the resource in JSON.
/// * `target_resource` - The target resource, if applicable.
///
/// # Errors
///
Expand All @@ -337,7 +341,7 @@ pub fn invoke_test(resource: &DscResource, expected: &str, target_resource: Opti
Some(r) => r,
None => resource,
};
validate_security_context(&test.require_security_context, &command_resource.type_name, "test")?;
validate_security_context(target_resource, &test.require_security_context, &command_resource.type_name, &Operation::Test)?;
let args = process_get_args(test.args.as_ref(), expected, command_resource);
let command_input = get_command_input(test.input.as_ref(), expected)?;

Expand Down Expand Up @@ -487,6 +491,7 @@ fn invoke_synthetic_test(resource: &DscResource, expected: &str, target_resource
/// * `resource` - The resource manifest for the command resource.
/// * `cwd` - The current working directory.
/// * `filter` - The filter to apply to the resource in JSON.
/// * `target_resource` - The target resource, if applicable.
/// * `execution_type` - Whether this is an actual delete or what-if.
///
/// # Errors
Expand All @@ -506,7 +511,7 @@ pub fn invoke_delete(resource: &DscResource, filter: &str, target_resource: Opti
Some(r) => r,
None => resource,
};
validate_security_context(&delete.require_security_context, &command_resource.type_name, "delete")?;
validate_security_context(target_resource, &delete.require_security_context, &command_resource.type_name, &Operation::Delete)?;
let (args, supports_whatif) = process_set_delete_args(delete.args.as_ref(), filter, command_resource, execution_type);
if execution_type == &ExecutionKind::WhatIf && !supports_whatif {
// perform a synthetic what-if by calling test and wrapping the TestResult in DeleteResultKind::SyntheticWhatIf
Expand All @@ -533,6 +538,7 @@ pub fn invoke_delete(resource: &DscResource, filter: &str, target_resource: Opti
/// * `resource` - The resource manifest for the command resource.
/// * `cwd` - The current working directory.
/// * `config` - The configuration to validate in JSON.
/// * `target_resource` - The target resource, if applicable.
///
/// # Returns
///
Expand Down Expand Up @@ -569,6 +575,7 @@ pub fn invoke_validate(resource: &DscResource, config: &str, target_resource: Op
/// # Arguments
///
/// * `resource` - The resource manifest
/// * `target_resource` - The target resource, if applicable
///
/// # Errors
///
Expand Down Expand Up @@ -679,7 +686,8 @@ fn verify_with_export_schema(input: &str, resource: &DscResource, target_resourc
/// * `resource` - The resource manifest
/// * `cwd` - The current working directory
/// * `input` - Input to the command
///
/// * `target_resource` - The target resource, if applicable
///
/// # Returns
///
/// * `ExportResult` - The result of the export operation
Expand Down Expand Up @@ -721,7 +729,7 @@ pub fn invoke_export(resource: &DscResource, input: Option<&str>, target_resourc
Some(r) => r,
None => resource,
};
validate_security_context(&export.require_security_context, &command_resource.type_name, "export")?;
validate_security_context(target_resource, &export.require_security_context, &command_resource.type_name, &Operation::Export)?;

if let Some(input) = input {
if !input.is_empty() {
Expand Down Expand Up @@ -1326,7 +1334,54 @@ pub fn log_stderr_line<'a>(process_id: &u32, trace_line: &'a str) -> &'a str
""
}

fn validate_security_context(required_security_context: &Option<SecurityContextKind>, resource_type: &str, operation: &str) -> Result<(), DscError> {
fn validate_security_context(target_resource: Option<&DscResource>, required_security_context: &Option<SecurityContextKind>, resource_type: &str, operation: &Operation) -> Result<(), DscError> {
if let Some(resource) = target_resource && let Some(adapted_manifest) = &resource.adapted_manifest {
let require_security_context = match operation {
Operation::Get => {
if let Some(get) = &adapted_manifest.get {
&get.require_security_context
} else {
// if adapted manifest does not have get, fall back to original manifest
&None
}
},
Operation::Set => {
if let Some(set) = &adapted_manifest.set {
&set.require_security_context
} else {
// if adapted manifest does not have get, fall back to original manifest
&None
}
},
Operation::Delete => {
if let Some(delete) = &adapted_manifest.delete {
&delete.require_security_context
} else {
// if adapted manifest does not have get, fall back to original manifest
&None
}
},
Operation::Test => {
if let Some(test) = &adapted_manifest.test {
&test.require_security_context
} else {
// if adapted manifest does not have get, fall back to original manifest
&None
}
},
Operation::Export => {
if let Some(export) = &adapted_manifest.export {
&export.require_security_context
} else {
// if adapted manifest does not have get, fall back to original manifest
&None
}
},
};
if require_security_context.is_some() {
return validate_security_context(None, require_security_context, &resource.type_name, operation);
}
}
match required_security_context {
Some(SecurityContextKind::Elevated) => {
if get_security_context() != SecurityContext::Admin {
Expand Down
26 changes: 25 additions & 1 deletion lib/dsc-lib/src/dscresources/dscresource.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

use crate::{configure::{Configurator, config_doc::{Configuration, ExecutionKind, Resource}, context::ProcessMode, parameters::{SECURE_VALUE_REDACTED, is_secure_value}, schema_cache::get_resource_schema}, dscresources::resource_manifest::{AdapterInputKind, Kind}, types::{FullyQualifiedTypeName, ResourceVersion}};
use crate::{configure::{Configurator, config_doc::{Configuration, ExecutionKind, Resource}, context::ProcessMode, parameters::{SECURE_VALUE_REDACTED, is_secure_value}, schema_cache::get_resource_schema}, dscresources::{adapted_resource_manifest::AdaptedDscResourceManifest, resource_manifest::{AdapterInputKind, Kind}}, types::{FullyQualifiedTypeName, ResourceVersion}};
use crate::discovery::discovery_trait::DiscoveryFilter;
use crate::dscresources::invoke_result::{ResourceGetResponse, ResourceSetResponse};
use crate::schemas::transforms::idiomaticize_string_enum;
Expand All @@ -12,6 +12,7 @@ use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};
use std::collections::{HashMap, HashSet};
use std::fmt::Display;
use std::path::PathBuf;
use tracing::{debug, info, trace, warn};

Expand Down Expand Up @@ -61,10 +62,32 @@ pub struct DscResource {
pub target_resource: Option<Box<DscResource>>,
/// The manifest of the resource.
pub manifest: Option<ResourceManifest>,
/// The adapted manifest of the resource, if available.
pub adapted_manifest: Option<AdaptedDscResourceManifest>,
/// The content of the adapted resource, if available.
pub adapted_content: Option<Map<String, Value>>,
}

pub(crate) enum Operation {
Get,
Set,
Test,
Delete,
Export,
}

impl Display for Operation {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Operation::Get => write!(f, "get"),
Operation::Set => write!(f, "set"),
Operation::Test => write!(f, "test"),
Operation::Delete => write!(f, "delete"),
Operation::Export => write!(f, "export"),
}
}
}

#[derive(Clone, Debug, Eq, Hash, PartialEq, Deserialize, Serialize, JsonSchema, DscRepoSchema, Ord, PartialOrd)]
#[serde(rename_all = "camelCase")]
#[schemars(transform = idiomaticize_string_enum)]
Expand Down Expand Up @@ -118,6 +141,7 @@ impl DscResource {
schema: None,
target_resource: None,
manifest: None,
adapted_manifest: None,
adapted_content: None,
}
}
Expand Down
Loading
Loading