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
89 changes: 89 additions & 0 deletions dsc/tests/dsc_resource_securitycontext.tests.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.

Describe 'Tests for resource manifest security context' {
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 'Resource with <securityContext> security context for operation <operation>' -TestCases @(
# since `set` and `test` rely on `get` to retrieve the current state, we need to always allow that
# and have a separate resource to test the elevated and restricted contexts for get
@{ securityContext = 'Elevated'; operation = 'get'; property = 'actualState'; type = 'Test/SecurityContextElevatedGet' },
@{ securityContext = 'Elevated'; operation = 'set'; property = 'afterState' },
@{ securityContext = 'Elevated'; operation = 'delete' },
@{ securityContext = 'Elevated'; operation = 'test'; property = 'actualState' },
@{ securityContext = 'Elevated'; operation = 'export' },
@{ securityContext = 'Restricted'; operation = 'get'; property = 'actualState'; type = 'Test/SecurityContextRestrictedGet' },
@{ securityContext = 'Restricted'; operation = 'set'; property = 'afterState' },
@{ securityContext = 'Restricted'; operation = 'delete' },
@{ securityContext = 'Restricted'; operation = 'test'; property = 'actualState' },
@{ securityContext = 'Restricted'; operation = 'export' },
@{ securityContext = 'Current'; operation = 'get'; property = 'actualState' },
@{ securityContext = 'Current'; operation = 'set'; property = 'afterState' },
@{ securityContext = 'Current'; operation = 'delete' },
@{ securityContext = 'Current'; operation = 'test'; property = 'actualState' },
@{ securityContext = 'Current'; operation = 'export' }
) {
param($securityContext, $operation, $property, $type)

if ($null -eq $type) {
$type = "Test/SecurityContext$securityContext"
}
$inputObj = @{
hello = "world"
action = $operation
}
$out = dsc resource $operation -r $type --input ($inputObj | ConvertTo-Json -Compress) 2>$testdrive/error.log
switch ($securityContext) {
'Elevated' {
if ($isAdmin) {
$LASTEXITCODE | Should -Be 0
if ($property) {
$result = $out | ConvertFrom-Json
$result.$property.action | Should -Be $operation
} elseif ($operation -eq 'export') {
$result = $out | ConvertFrom-Json
$result.resources.properties.action | Should -Be 'export'
}
}
else {
$LASTEXITCODE | Should -Be 2
(Get-Content "$testdrive/error.log") | Should -BeLike "*ERROR*Operation '$operation' for resource '$type' requires security context '$securityContext'*"
}
}
'Restricted' {
if ($isAdmin) {
$LASTEXITCODE | Should -Be 2
(Get-Content "$testdrive/error.log") | Should -BeLike "*ERROR*Operation '$operation' for resource '$type' requires security context '$securityContext'*"
}
else {
$LASTEXITCODE | Should -Be 0
if ($property) {
$result = $out | ConvertFrom-Json
$result.$property.action | Should -Be $operation
} elseif ($operation -eq 'export') {
$result = $out | ConvertFrom-Json
$result.resources.properties.action | Should -Be 'export'
}
}
}
'Current' {
$LASTEXITCODE | Should -Be 0
if ($property) {
$result = $out | ConvertFrom-Json
$result.$property.action | Should -Be $operation
} elseif ($operation -eq 'export') {
$result = $out | ConvertFrom-Json
$result.resources.properties.action | Should -Be 'export'
}
}
}
}
}
1 change: 1 addition & 0 deletions lib/dsc-lib/locales/en-us.toml
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@ inDesiredStateNotBool = "'_inDesiredState' is not a boolean"
exportNotSupportedUsingGet = "Export is not supported by resource '%{resource}' using get operation"
runProcessError = "Failed to run process '%{executable}': %{error}"
whatIfWarning = "Resource '%{resource}' uses deprecated 'whatIf' operation. See https://github.com/PowerShell/DSC/issues/1361 for migration information."
securityContextRequired = "Operation '%{operation}' for resource '%{resource}' requires security context '%{context}'"

[dscresources.dscresource]
invokeGet = "Invoking get for '%{resource}'"
Expand Down
29 changes: 28 additions & 1 deletion lib/dsc-lib/src/dscresources/command_resource.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@
// Licensed under the MIT License.

use clap::ValueEnum;
use dsc_lib_security_context::{SecurityContext, get_security_context};
use jsonschema::Validator;
use rust_i18n::t;
use serde::Deserialize;
use serde_json::{Map, Value};
use std::{collections::HashMap, env, path::{Path, PathBuf}, process::Stdio};
use crate::{configure::{config_doc::ExecutionKind, config_result::{ResourceGetResult, ResourceTestResult}}, dscresources::resource_manifest::SchemaArgKind, types::{ExitCodesMap, FullyQualifiedTypeName}, util::canonicalize_which};
use crate::{configure::{config_doc::{ExecutionKind, SecurityContextKind}, config_result::{ResourceGetResult, ResourceTestResult}}, dscresources::resource_manifest::SchemaArgKind, types::{ExitCodesMap, FullyQualifiedTypeName}, util::canonicalize_which};
use crate::dscerror::DscError;
use super::{
dscresource::{get_diff, redact, DscResource},
Expand Down Expand Up @@ -53,6 +54,7 @@ pub fn invoke_get(resource: &DscResource, filter: &str, target_resource: Option<
Some(r) => r.type_name.clone(),
None => resource.type_name.clone(),
};
validate_security_context(&get.require_security_context, &resource_type, "get")?;
let path = if let Some(target_resource) = target_resource {
Some(target_resource.path.clone())
} else {
Expand Down Expand Up @@ -156,6 +158,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, &resource_type, "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 @@ -200,6 +203,7 @@ pub fn invoke_set(resource: &DscResource, desired: &str, skip_test: bool, execut
Some(r) => r.type_name.clone(),
None => resource.type_name.clone(),
};
validate_security_context(&get.require_security_context, &resource_type, "get")?;
let path = if let Some(target_resource) = target_resource {
Some(target_resource.path.clone())
} else {
Expand Down Expand Up @@ -357,6 +361,7 @@ pub fn invoke_test(resource: &DscResource, expected: &str, target_resource: Opti
Some(r) => r.type_name.clone(),
None => resource.type_name.clone(),
};
validate_security_context(&test.require_security_context, &resource_type, "test")?;
let path = if let Some(target_resource) = target_resource {
Some(target_resource.path.clone())
} else {
Expand Down Expand Up @@ -517,6 +522,7 @@ pub fn invoke_delete(resource: &DscResource, filter: &str, target_resource: Opti
Some(r) => r.type_name.clone(),
None => resource.type_name.clone(),
};
validate_security_context(&delete.require_security_context, &resource_type, "delete")?;
let path = if let Some(target_resource) = target_resource {
Some(target_resource.path.clone())
} else {
Expand Down Expand Up @@ -649,6 +655,7 @@ pub fn invoke_export(resource: &DscResource, input: Option<&str>, target_resourc
// see if get is supported and use that instead
if manifest.get.is_some() {
info!("{}", t!("dscresources.commandResource.exportNotSupportedUsingGet", resource = &resource.type_name));
validate_security_context(&manifest.get.as_ref().unwrap().require_security_context, &resource.type_name, "get")?;
let get_result = invoke_get(resource, input.unwrap_or(""), target_resource)?;
let mut instances: Vec<Value> = Vec::new();
match get_result {
Expand All @@ -675,6 +682,7 @@ pub fn invoke_export(resource: &DscResource, input: Option<&str>, target_resourc
Some(r) => r.type_name.clone(),
None => resource.type_name.clone(),
};
validate_security_context(&export.require_security_context, &resource_type, "export")?;
let path = if let Some(target_resource) = target_resource {
Some(target_resource.path.clone())
} else {
Expand Down Expand Up @@ -1245,6 +1253,25 @@ 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> {
match required_security_context {
Some(SecurityContextKind::Elevated) => {
if get_security_context() != SecurityContext::Admin {
return Err(DscError::SecurityContext(t!("dscresources.commandResource.securityContextRequired", operation = operation, resource = resource_type, context = "elevated").to_string()));
}
},
Some(SecurityContextKind::Restricted) => {
if get_security_context() != SecurityContext::User {
return Err(DscError::SecurityContext(t!("dscresources.commandResource.securityContextRequired", operation = operation, resource = resource_type, context = "restricted").to_string()));
}
},
None | Some(SecurityContextKind::Current) => {
// no specific context required, so allow any context
},
}
Ok(())
}

#[derive(Clone, Debug, PartialEq, Eq, Deserialize, ValueEnum)]
pub enum TraceLevel {
#[serde(rename = "ERROR")]
Expand Down
16 changes: 16 additions & 0 deletions lib/dsc-lib/src/dscresources/resource_manifest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};

use crate::{
configure::config_doc::SecurityContextKind,
schemas::{dsc_repo::DscRepoSchema, transforms::idiomaticize_string_enum},
types::{ExitCodesMap, FullyQualifiedTypeName, TagList},
};
Expand Down Expand Up @@ -221,6 +222,9 @@ pub struct GetMethod {
/// How to pass optional input for a Get.
#[serde(skip_serializing_if = "Option::is_none")]
pub input: Option<InputKind>,
/// The security context required to run the Get method. Default if not specified is `current`.
#[serde(rename = "requireSecurityContext", skip_serializing_if = "Option::is_none")]
pub require_security_context: Option<SecurityContextKind>,
}

#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, DscRepoSchema)]
Expand All @@ -241,6 +245,9 @@ pub struct SetMethod {
/// The type of return value expected from the Set method.
#[serde(rename = "return", skip_serializing_if = "Option::is_none")]
pub returns: Option<ReturnKind>,
/// The security context required to run the Set method. Default if not specified is `current`.
#[serde(rename = "requireSecurityContext", skip_serializing_if = "Option::is_none")]
pub require_security_context: Option<SecurityContextKind>,
}

#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, DscRepoSchema)]
Expand All @@ -255,6 +262,9 @@ pub struct TestMethod {
/// The type of return value expected from the Test method.
#[serde(rename = "return", skip_serializing_if = "Option::is_none")]
pub returns: Option<ReturnKind>,
/// The security context required to run the Test method. Default if not specified is `current`.
#[serde(rename = "requireSecurityContext", skip_serializing_if = "Option::is_none")]
pub require_security_context: Option<SecurityContextKind>,
}

#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, DscRepoSchema)]
Expand All @@ -266,6 +276,9 @@ pub struct DeleteMethod {
pub args: Option<Vec<SetDeleteArgKind>>,
/// How to pass required input for a Delete.
pub input: Option<InputKind>,
/// The security context required to run the Delete method. Default if not specified is `current`.
#[serde(rename = "requireSecurityContext", skip_serializing_if = "Option::is_none")]
pub require_security_context: Option<SecurityContextKind>,
}

#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, DscRepoSchema)]
Expand All @@ -288,6 +301,9 @@ pub struct ExportMethod {
pub args: Option<Vec<GetArgKind>>,
/// How to pass input for a Export.
pub input: Option<InputKind>,
/// The security context required to run the Export method. Default if not specified is `current`.
#[serde(rename = "requireSecurityContext", skip_serializing_if = "Option::is_none")]
pub require_security_context: Option<SecurityContextKind>,
}

#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, DscRepoSchema)]
Expand Down
Loading