From 93f569df02a1ccaff04931e47cd8cef6551f8c3d Mon Sep 17 00:00:00 2001 From: "Steve Lee (POWERSHELL HE/HIM) (from Dev Box)" Date: Tue, 13 Oct 2026 10:29:04 -0700 Subject: [PATCH 1/5] Add filesystem ACL resource Add Microsoft.Filesystem/ACL to the shared file_dsc executable with native Windows SDDL get, set, and test support. Rename the existing filecontent binary and add Allow/Deny ACE coverage for files and folders. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- Cargo.lock | 3 +- Cargo.toml | 4 +- data.build.json | 14 +- resources/filecontent/.project.data.json | 7 +- resources/filecontent/Cargo.toml | 7 +- .../filecontent/filecontent.dsc.resource.json | 6 +- .../filesystem_acl.dsc.resource.json | 87 ++++++++ resources/filecontent/locales/en-us.toml | 15 +- resources/filecontent/src/acl.rs | 210 ++++++++++++++++++ resources/filecontent/src/main.rs | 54 ++++- resources/filecontent/src/types.rs | 10 + .../tests/filecontent_get.tests.ps1 | 4 +- .../tests/filesystem_acl.tests.ps1 | 83 +++++++ 13 files changed, 485 insertions(+), 19 deletions(-) create mode 100644 resources/filecontent/filesystem_acl.dsc.resource.json create mode 100644 resources/filecontent/src/acl.rs create mode 100644 resources/filecontent/tests/filesystem_acl.tests.ps1 diff --git a/Cargo.lock b/Cargo.lock index 463a32b57..388a21ca3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1185,7 +1185,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" [[package]] -name = "filecontent" +name = "file_dsc" version = "0.1.0" dependencies = [ "base16ct", @@ -1193,6 +1193,7 @@ dependencies = [ "serde", "serde_json", "sha2 0.11.0", + "windows 0.62.2", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 5504aa256..30b6c8558 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -160,7 +160,7 @@ strip = "symbols" # See split-debuginfo - allows us to drop the size b [workspace.dependencies] #external dependencies -# filecontent +# file_dsc base16ct = { version = "1.0.0", features = ["alloc"] } # dsc-lib base32 = { version = "0.5" } @@ -238,7 +238,7 @@ semver = { version = "1.0.28" } serde = { version = "1.0.229", features = ["derive"] } # dsc, dsc-lib, dscecho, dsc-lib-osinfo, osinfo, process, registry, dsc-lib-registry, runcommandonset, sshdconfig, dsctest, test_group_resource, y2j serde_json = { version = "1.0.151", features = ["preserve_order"] } -# filecontent +# file_dsc sha2 = { version = "0.11.0" } # dsc, dsc-lib, y2j serde_yaml = { version = "0.9" } diff --git a/data.build.json b/data.build.json index 370d36e3e..6e2de827c 100644 --- a/data.build.json +++ b/data.build.json @@ -11,7 +11,7 @@ "dsc-bicep-ext", "dscecho", "echo.dsc.resource.json", - "filecontent", + "file_dsc", "filecontent.dsc.resource.json", "assertion.dsc.resource.json", "apt.dsc.resource.json", @@ -43,7 +43,7 @@ "dsc-bicep-ext", "dscecho", "echo.dsc.resource.json", - "filecontent", + "file_dsc", "filecontent.dsc.resource.json", "assertion.dsc.resource.json", "brew.dsc.resource.json", @@ -78,8 +78,9 @@ "dsc-bicep-ext.exe", "dscecho.exe", "echo.dsc.resource.json", - "filecontent.exe", + "file_dsc.exe", "filecontent.dsc.resource.json", + "filesystem_acl.dsc.resource.json", "environment_variable.exe", "environment_variable.dsc.manifests.json", "assertion.dsc.resource.json", @@ -344,16 +345,19 @@ } }, { - "Name": "filecontent", + "Name": "file_dsc", "Kind": "Resource", "RelativePath": "resources/filecontent", "IsRust": true, "Binaries": [ - "filecontent" + "file_dsc" ], "CopyFiles": { "All": [ "filecontent.dsc.resource.json" + ], + "Windows": [ + "filesystem_acl.dsc.resource.json" ] } }, diff --git a/resources/filecontent/.project.data.json b/resources/filecontent/.project.data.json index 15b7f760d..2af882645 100644 --- a/resources/filecontent/.project.data.json +++ b/resources/filecontent/.project.data.json @@ -1,13 +1,16 @@ { - "Name": "filecontent", + "Name": "file_dsc", "Kind": "Resource", "IsRust": true, "Binaries": [ - "filecontent" + "file_dsc" ], "CopyFiles": { "All": [ "filecontent.dsc.resource.json" + ], + "Windows": [ + "filesystem_acl.dsc.resource.json" ] } } diff --git a/resources/filecontent/Cargo.toml b/resources/filecontent/Cargo.toml index 266c15df4..48c04998d 100644 --- a/resources/filecontent/Cargo.toml +++ b/resources/filecontent/Cargo.toml @@ -1,10 +1,10 @@ [package] -name = "filecontent" +name = "file_dsc" version = "0.1.0" edition = "2024" [[bin]] -name = "filecontent" +name = "file_dsc" path = "src/main.rs" [package.metadata.i18n] @@ -18,3 +18,6 @@ rust-i18n = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } sha2 = { workspace = true } + +[target.'cfg(windows)'.dependencies] +windows = { workspace = true } diff --git a/resources/filecontent/filecontent.dsc.resource.json b/resources/filecontent/filecontent.dsc.resource.json index 514a256fe..49f1e32d6 100644 --- a/resources/filecontent/filecontent.dsc.resource.json +++ b/resources/filecontent/filecontent.dsc.resource.json @@ -10,7 +10,7 @@ ], "version": "0.1.0", "get": { - "executable": "filecontent", + "executable": "file_dsc", "args": [ "get", { @@ -20,7 +20,7 @@ ] }, "set": { - "executable": "filecontent", + "executable": "file_dsc", "args": [ "set", { @@ -32,7 +32,7 @@ "return": "state" }, "test": { - "executable": "filecontent", + "executable": "file_dsc", "args": [ "test", { diff --git a/resources/filecontent/filesystem_acl.dsc.resource.json b/resources/filecontent/filesystem_acl.dsc.resource.json new file mode 100644 index 000000000..670c9585f --- /dev/null +++ b/resources/filecontent/filesystem_acl.dsc.resource.json @@ -0,0 +1,87 @@ +{ + "$schema": "https://aka.ms/dsc/schemas/v3/bundled/resource/manifest.json", + "type": "Microsoft.Filesystem/ACL", + "description": "Manage the discretionary access control list of a local file or folder.", + "tags": [ + "file", + "folder", + "windows" + ], + "version": "0.1.0", + "get": { + "executable": "file_dsc", + "args": [ + "acl", + "get", + { + "jsonInputArg": "--input", + "mandatory": true + } + ] + }, + "set": { + "executable": "file_dsc", + "args": [ + "acl", + "set", + { + "jsonInputArg": "--input", + "mandatory": true + } + ], + "implementsPretest": false, + "return": "state" + }, + "test": { + "executable": "file_dsc", + "args": [ + "acl", + "test", + { + "jsonInputArg": "--input", + "mandatory": true + } + ], + "return": "state" + }, + "exitCodes": { + "0": "Success", + "1": "Invalid arguments", + "2": "Invalid input", + "3": "Filesystem ACL operation error" + }, + "schema": { + "embedded": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Filesystem ACL", + "description": "Manage the discretionary access control list of a local file or folder.", + "type": "object", + "required": [ + "path", + "SDDL" + ], + "properties": { + "path": { + "type": "string", + "title": "Path", + "description": "The path to the local file or folder.", + "minLength": 1, + "x-dsc-key": true + }, + "SDDL": { + "type": "string", + "title": "Security descriptor definition language", + "description": "The discretionary access control list in SDDL form.", + "minLength": 1 + }, + "_inDesiredState": { + "type": "boolean", + "title": "In desired state", + "description": "Indicates whether the ACL is in the desired state. Only returned by test.", + "readOnly": true + } + }, + "additionalProperties": false + } + } +} diff --git a/resources/filecontent/locales/en-us.toml b/resources/filecontent/locales/en-us.toml index ce267e0ca..e83ead5f2 100644 --- a/resources/filecontent/locales/en-us.toml +++ b/resources/filecontent/locales/en-us.toml @@ -1,7 +1,7 @@ _version = 1 [main] -missingOperation = "Missing operation. Usage: filecontent --input " +missingOperation = "Missing operation. Usage: file_dsc --input | file_dsc acl --input " unknownOperation = "Unknown operation: '%{operation}'. Expected: get, set, or test" missingInput = "Missing --input argument" missingInputValue = "Missing value for --input argument" @@ -23,3 +23,16 @@ writeError = "Failed to write file '%{path}': %{error}" removeError = "Failed to remove file '%{path}': %{error}" sha256Mismatch = "The sha256 value does not match the content property" sha512Mismatch = "The sha512 value does not match the content property" + +[acl] +missingOperation = "Missing ACL operation. Usage: file_dsc acl --input " +unknownOperation = "Unknown ACL operation: '%{operation}'. Expected: get, set, or test" +windowsOnly = "The filesystem ACL resource is only supported on Windows" +emptySddl = "The SDDL property must not be empty" +unsupportedSddlComponent = "The SDDL property must contain only a discretionary ACL (D:) component" +pathNotFound = "The path '%{path}' does not exist" +invalidSddl = "The SDDL property is invalid: %{error}" +getError = "Failed to read the ACL for '%{path}': %{error}" +setError = "Failed to set the ACL for '%{path}': %{error}" +convertError = "Failed to convert the security descriptor to SDDL: %{error}" +emptyDescriptor = "The security descriptor was empty" diff --git a/resources/filecontent/src/acl.rs b/resources/filecontent/src/acl.rs new file mode 100644 index 000000000..1c97be511 --- /dev/null +++ b/resources/filecontent/src/acl.rs @@ -0,0 +1,210 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use crate::types::FilesystemAcl; +use rust_i18n::t; +use std::ptr; +use windows::Win32::Foundation::{HLOCAL, LocalFree}; +use windows::Win32::Security::Authorization::{ + ConvertSecurityDescriptorToStringSecurityDescriptorW, + ConvertStringSecurityDescriptorToSecurityDescriptorW, SDDL_REVISION_1, +}; +use windows::Win32::Security::{ + DACL_SECURITY_INFORMATION, GetFileSecurityW, PSECURITY_DESCRIPTOR, SetFileSecurityW, +}; +use windows::core::{PCWSTR, PWSTR}; + +pub fn get(input: &FilesystemAcl) -> Result { + validate_input(input)?; + Ok(FilesystemAcl { + path: input.path.clone(), + sddl: read_sddl(&input.path)?, + in_desired_state: None, + }) +} + +pub fn set(input: &FilesystemAcl) -> Result { + validate_input(input)?; + let security_descriptor = SecurityDescriptor::from_sddl(&input.sddl)?; + let path = to_wide(&input.path); + + unsafe { + SetFileSecurityW( + PCWSTR(path.as_ptr()), + DACL_SECURITY_INFORMATION, + security_descriptor.as_ptr(), + ) + } + .ok() + .map_err(|error| { + t!( + "acl.setError", + path = input.path.as_str(), + error = error.to_string() + ) + .to_string() + })?; + + get(input) +} + +pub fn test(input: &FilesystemAcl) -> Result { + validate_input(input)?; + let desired = SecurityDescriptor::from_sddl(&input.sddl)?.to_sddl()?; + let mut actual = get(input)?; + actual.in_desired_state = Some(actual.sddl.eq_ignore_ascii_case(&desired)); + Ok(actual) +} + +fn validate_input(input: &FilesystemAcl) -> Result<(), String> { + if input.path.is_empty() { + return Err(t!("input.emptyPath").to_string()); + } + if input.sddl.is_empty() { + return Err(t!("acl.emptySddl").to_string()); + } + if !has_top_level_section(&input.sddl, b"D:") + || [b"O:", b"G:", b"S:"] + .iter() + .any(|section| has_top_level_section(&input.sddl, section)) + { + return Err(t!("acl.unsupportedSddlComponent").to_string()); + } + if !std::path::Path::new(&input.path).exists() { + return Err(t!("acl.pathNotFound", path = input.path.as_str()).to_string()); + } + Ok(()) +} + +fn has_top_level_section(sddl: &str, section: &[u8; 2]) -> bool { + let bytes = sddl.as_bytes(); + let mut depth = 0_u32; + for index in 0..bytes.len().saturating_sub(1) { + match bytes[index] { + b'(' => depth += 1, + b')' => depth = depth.saturating_sub(1), + _ if depth == 0 + && bytes[index].eq_ignore_ascii_case(§ion[0]) + && bytes[index + 1] == section[1] => + { + return true; + } + _ => {} + } + } + false +} + +fn read_sddl(path: &str) -> Result { + let path_wide = to_wide(path); + let mut required_length = 0; + let first_result = unsafe { + GetFileSecurityW( + PCWSTR(path_wide.as_ptr()), + DACL_SECURITY_INFORMATION.0, + None, + 0, + &mut required_length, + ) + }; + if required_length == 0 { + return Err(t!( + "acl.getError", + path = path, + error = first_result + .ok() + .err() + .map_or_else(|| t!("acl.emptyDescriptor").to_string(), |error| error.to_string()) + ) + .to_string()); + } + + let mut buffer = vec![0_u8; required_length as usize]; + unsafe { + GetFileSecurityW( + PCWSTR(path_wide.as_ptr()), + DACL_SECURITY_INFORMATION.0, + Some(PSECURITY_DESCRIPTOR(buffer.as_mut_ptr().cast())), + required_length, + &mut required_length, + ) + } + .ok() + .map_err(|error| { + t!("acl.getError", path = path, error = error.to_string()).to_string() + })?; + + SecurityDescriptor::borrowed(buffer.as_mut_ptr().cast()).to_sddl() +} + +fn to_wide(value: &str) -> Vec { + value.encode_utf16().chain(std::iter::once(0)).collect() +} + +struct SecurityDescriptor { + pointer: PSECURITY_DESCRIPTOR, + owned: bool, +} + +impl SecurityDescriptor { + fn from_sddl(sddl: &str) -> Result { + let sddl_wide = to_wide(sddl); + let mut pointer = PSECURITY_DESCRIPTOR(ptr::null_mut()); + unsafe { + ConvertStringSecurityDescriptorToSecurityDescriptorW( + PCWSTR(sddl_wide.as_ptr()), + SDDL_REVISION_1, + &mut pointer, + None, + ) + } + .map_err(|error| t!("acl.invalidSddl", error = error.to_string()).to_string())?; + + Ok(Self { + pointer, + owned: true, + }) + } + + fn borrowed(pointer: *mut core::ffi::c_void) -> Self { + Self { + pointer: PSECURITY_DESCRIPTOR(pointer), + owned: false, + } + } + + fn as_ptr(&self) -> PSECURITY_DESCRIPTOR { + self.pointer + } + + fn to_sddl(&self) -> Result { + let mut output = PWSTR::null(); + unsafe { + ConvertSecurityDescriptorToStringSecurityDescriptorW( + self.pointer, + SDDL_REVISION_1, + DACL_SECURITY_INFORMATION, + &mut output, + None, + ) + } + .map_err(|error| t!("acl.convertError", error = error.to_string()).to_string())?; + + let result = unsafe { output.to_string() } + .map_err(|error| t!("acl.convertError", error = error.to_string()).to_string()); + unsafe { + let _ = LocalFree(Some(HLOCAL(output.0.cast()))); + } + result + } +} + +impl Drop for SecurityDescriptor { + fn drop(&mut self) { + if self.owned { + unsafe { + let _ = LocalFree(Some(HLOCAL(self.pointer.0.cast()))); + } + } + } +} diff --git a/resources/filecontent/src/main.rs b/resources/filecontent/src/main.rs index a39582174..c562b3181 100644 --- a/resources/filecontent/src/main.rs +++ b/resources/filecontent/src/main.rs @@ -1,11 +1,13 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +#[cfg(windows)] +mod acl; mod file; mod types; use crate::file::{get, set, test}; -use crate::types::FileContent; +use crate::types::{FileContent, FilesystemAcl}; use rust_i18n::t; use serde::Serialize; use serde_json::json; @@ -25,6 +27,10 @@ fn main() -> ExitCode { return fail(EXIT_INVALID_ARGS, &t!("main.missingOperation")); }; + if operation == "acl" { + return handle_acl(&args[1..]); + } + let input = match parse_input_arg(&args[1..]) { Ok(input) => input, Err(code) => return code, @@ -49,6 +55,39 @@ fn main() -> ExitCode { } } +fn handle_acl(args: &[String]) -> ExitCode { + let Some(operation) = args.first() else { + return fail(EXIT_INVALID_ARGS, &t!("acl.missingOperation")); + }; + let input = match parse_input_arg(&args[1..]) { + Ok(input) => input, + Err(code) => return code, + }; + let input = match require_acl_input(input) { + Ok(input) => input, + Err(code) => return code, + }; + + #[cfg(windows)] + { + match operation.as_str() { + "get" => handle_result(acl::get(&input)), + "set" => handle_result(acl::set(&input)), + "test" => handle_result(acl::test(&input)), + _ => fail( + EXIT_INVALID_ARGS, + &t!("acl.unknownOperation", operation = operation), + ), + } + } + + #[cfg(not(windows))] + { + let _ = (operation, input); + fail(EXIT_RESOURCE_ERROR, &t!("acl.windowsOnly")) + } +} + fn parse_input_arg(args: &[String]) -> Result, ExitCode> { let mut index = 0; while index < args.len() { @@ -76,6 +115,19 @@ fn require_input(input: Option<&str>) -> Result { }) } +fn require_acl_input(input: Option<&str>) -> Result { + let Some(input) = input else { + return Err(fail(EXIT_INVALID_ARGS, &t!("main.missingInput"))); + }; + + serde_json::from_str(input).map_err(|error| { + fail( + EXIT_INVALID_INPUT, + &t!("main.invalidJson", error = error.to_string()), + ) + }) +} + fn handle_result(result: Result) -> ExitCode { let value = match result { Ok(value) => value, diff --git a/resources/filecontent/src/types.rs b/resources/filecontent/src/types.rs index a6d387a7e..15dc604d7 100644 --- a/resources/filecontent/src/types.rs +++ b/resources/filecontent/src/types.rs @@ -18,3 +18,13 @@ pub struct FileContent { #[serde(rename = "_inDesiredState", skip_serializing_if = "Option::is_none")] pub in_desired_state: Option, } + +#[derive(Debug, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct FilesystemAcl { + pub path: String, + #[serde(rename = "SDDL")] + pub sddl: String, + #[serde(rename = "_inDesiredState", skip_serializing_if = "Option::is_none")] + pub in_desired_state: Option, +} diff --git a/resources/filecontent/tests/filecontent_get.tests.ps1 b/resources/filecontent/tests/filecontent_get.tests.ps1 index 7e185e08d..4b6b00828 100644 --- a/resources/filecontent/tests/filecontent_get.tests.ps1 +++ b/resources/filecontent/tests/filecontent_get.tests.ps1 @@ -39,14 +39,14 @@ Describe 'FileContent get tests' { It 'Accepts the short input argument' { $json = @{ path = $filePath } | ConvertTo-Json -Compress - $actual = filecontent get -i $json 2>$TestDrive/error.log | ConvertFrom-Json + $actual = file_dsc get -i $json 2>$TestDrive/error.log | ConvertFrom-Json $LASTEXITCODE | Should -Be 0 -Because (Get-Content -Raw $TestDrive/error.log) $actual._exist | Should -BeTrue } It 'Reports a missing input value' { - $null = filecontent get --input 2>$TestDrive/error.log + $null = file_dsc get --input 2>$TestDrive/error.log $LASTEXITCODE | Should -Be 1 (Get-Content -Raw $TestDrive/error.log) | Should -Match 'Missing value for --input argument' diff --git a/resources/filecontent/tests/filesystem_acl.tests.ps1 b/resources/filecontent/tests/filesystem_acl.tests.ps1 new file mode 100644 index 000000000..49246ce1e --- /dev/null +++ b/resources/filecontent/tests/filesystem_acl.tests.ps1 @@ -0,0 +1,83 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +Describe 'Microsoft.Filesystem/ACL tests' -Skip:(!$IsWindows) { + BeforeAll { + $resourceType = 'Microsoft.Filesystem/ACL' + $currentSid = [Security.Principal.WindowsIdentity]::GetCurrent().User.Value + $desiredSddl = "D:(D;;FR;;;AN)(A;;FA;;;$currentSid)" + + function ConvertTo-CanonicalSddl { + param( + [Parameter(Mandatory)] + [string]$Sddl + ) + + return [Security.AccessControl.RawSecurityDescriptor]::new($Sddl).GetSddlForm('Access') + } + } + + BeforeEach { + $testRoot = Join-Path $TestDrive "$([System.Guid]::NewGuid())" + $null = New-Item -ItemType Directory -Path $testRoot + } + + AfterEach { + Remove-Item -LiteralPath $testRoot -Recurse -Force -ErrorAction Ignore + } + + It 'Sets and gets Allow and Deny ACEs on a ' -ForEach @( + @{ itemType = 'file' } + @{ itemType = 'folder' } + ) { + $path = if ($itemType -eq 'file') { + New-Item -ItemType File -Path (Join-Path $testRoot 'item.txt') | + Select-Object -ExpandProperty FullName + } else { + New-Item -ItemType Directory -Path (Join-Path $testRoot 'folder') | + Select-Object -ExpandProperty FullName + } + $json = @{ path = $path; SDDL = $desiredSddl } | ConvertTo-Json -Compress + + $setOutput = $json | dsc resource set -r $resourceType -f - 2>$TestDrive/error.log + $LASTEXITCODE | Should -Be 0 -Because (Get-Content -Raw $TestDrive/error.log) + $setState = ($setOutput | ConvertFrom-Json).afterState + + $getOutput = $json | dsc resource get -r $resourceType -f - 2>$TestDrive/error.log + $LASTEXITCODE | Should -Be 0 -Because (Get-Content -Raw $TestDrive/error.log) + $getState = ($getOutput | ConvertFrom-Json).actualState + + (ConvertTo-CanonicalSddl -Sddl $setState.SDDL) | + Should -BeExactly (ConvertTo-CanonicalSddl -Sddl $desiredSddl) + (ConvertTo-CanonicalSddl -Sddl $getState.SDDL) | + Should -BeExactly (ConvertTo-CanonicalSddl -Sddl $desiredSddl) + $getState.SDDL | Should -Match '\(D;' + $getState.SDDL | Should -Match '\(A;' + } + + It 'Tests whether Allow and Deny ACEs are in the desired state' { + $path = New-Item -ItemType File -Path (Join-Path $testRoot 'test.txt') | + Select-Object -ExpandProperty FullName + $json = @{ path = $path; SDDL = $desiredSddl } | ConvertTo-Json -Compress + $null = $json | dsc resource set -r $resourceType -f - 2>$TestDrive/error.log + $LASTEXITCODE | Should -Be 0 -Because (Get-Content -Raw $TestDrive/error.log) + + $testOutput = $json | dsc resource test -r $resourceType -f - 2>$TestDrive/error.log + $LASTEXITCODE | Should -Be 0 -Because (Get-Content -Raw $TestDrive/error.log) + + ($testOutput | ConvertFrom-Json).inDesiredState | Should -BeTrue + } + + It 'Rejects SDDL components that the resource does not manage' { + $path = New-Item -ItemType File -Path (Join-Path $testRoot 'invalid.txt') | + Select-Object -ExpandProperty FullName + $json = @{ path = $path; SDDL = "O:BAD:(A;;FA;;;$currentSid)" } | + ConvertTo-Json -Compress + + $null = $json | dsc resource set -r $resourceType -f - 2>$TestDrive/error.log + + $LASTEXITCODE | Should -Not -Be 0 + (Get-Content -Raw $TestDrive/error.log) | + Should -Match 'must contain only a discretionary ACL' + } +} From 4ddd250625ab8ee2f57295ca71b885a9d39558e0 Mon Sep 17 00:00:00 2001 From: "Steve Lee (POWERSHELL HE/HIM) (from Dev Box)" Date: Tue, 13 Oct 2026 10:30:31 -0700 Subject: [PATCH 2/5] Rename filecontent directory to file_dsc Update workspace, build metadata, and build helper tests to use the renamed resource directory. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- Cargo.toml | 10 +++++----- data.build.json | 2 +- resources/{filecontent => file_dsc}/.project.data.json | 0 resources/{filecontent => file_dsc}/Cargo.toml | 0 .../filecontent.dsc.resource.json | 0 .../filesystem_acl.dsc.resource.json | 0 resources/{filecontent => file_dsc}/locales/en-us.toml | 0 resources/{filecontent => file_dsc}/src/acl.rs | 0 resources/{filecontent => file_dsc}/src/file.rs | 0 resources/{filecontent => file_dsc}/src/main.rs | 0 resources/{filecontent => file_dsc}/src/types.rs | 0 .../tests/filecontent_export.tests.ps1 | 0 .../tests/filecontent_get.tests.ps1 | 0 .../tests/filecontent_set.tests.ps1 | 0 .../tests/filecontent_test.tests.ps1 | 0 .../tests/filesystem_acl.tests.ps1 | 0 tests/helpers.build.pester.tests.ps1 | 6 +++--- 17 files changed, 9 insertions(+), 9 deletions(-) rename resources/{filecontent => file_dsc}/.project.data.json (100%) rename resources/{filecontent => file_dsc}/Cargo.toml (100%) rename resources/{filecontent => file_dsc}/filecontent.dsc.resource.json (100%) rename resources/{filecontent => file_dsc}/filesystem_acl.dsc.resource.json (100%) rename resources/{filecontent => file_dsc}/locales/en-us.toml (100%) rename resources/{filecontent => file_dsc}/src/acl.rs (100%) rename resources/{filecontent => file_dsc}/src/file.rs (100%) rename resources/{filecontent => file_dsc}/src/main.rs (100%) rename resources/{filecontent => file_dsc}/src/types.rs (100%) rename resources/{filecontent => file_dsc}/tests/filecontent_export.tests.ps1 (100%) rename resources/{filecontent => file_dsc}/tests/filecontent_get.tests.ps1 (100%) rename resources/{filecontent => file_dsc}/tests/filecontent_set.tests.ps1 (100%) rename resources/{filecontent => file_dsc}/tests/filecontent_test.tests.ps1 (100%) rename resources/{filecontent => file_dsc}/tests/filesystem_acl.tests.ps1 (100%) diff --git a/Cargo.toml b/Cargo.toml index 30b6c8558..51c8cdc53 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,7 +17,7 @@ members = [ "lib/dsc-lib", "resources/dism_dsc", "resources/dscecho", - "resources/filecontent", + "resources/file_dsc", "resources/osinfo", "resources/process", "resources/registry", @@ -52,7 +52,7 @@ default-members = [ "lib/dsc-lib", "resources/dism_dsc", "resources/dscecho", - "resources/filecontent", + "resources/file_dsc", "resources/osinfo", "resources/process", "resources/registry", @@ -88,7 +88,7 @@ Windows = [ "lib/dsc-lib", "resources/dism_dsc", "resources/dscecho", - "resources/filecontent", + "resources/file_dsc", "resources/osinfo", "resources/process", "resources/registry", @@ -116,7 +116,7 @@ macOS = [ "lib/dsc-lib-telemetry", "lib/dsc-lib", "resources/dscecho", - "resources/filecontent", + "resources/file_dsc", "resources/osinfo", "resources/process", "resources/runcommandonset", @@ -138,7 +138,7 @@ Linux = [ "lib/dsc-lib-telemetry", "lib/dsc-lib", "resources/dscecho", - "resources/filecontent", + "resources/file_dsc", "resources/osinfo", "resources/process", "resources/runcommandonset", diff --git a/data.build.json b/data.build.json index 6e2de827c..0bf029028 100644 --- a/data.build.json +++ b/data.build.json @@ -347,7 +347,7 @@ { "Name": "file_dsc", "Kind": "Resource", - "RelativePath": "resources/filecontent", + "RelativePath": "resources/file_dsc", "IsRust": true, "Binaries": [ "file_dsc" diff --git a/resources/filecontent/.project.data.json b/resources/file_dsc/.project.data.json similarity index 100% rename from resources/filecontent/.project.data.json rename to resources/file_dsc/.project.data.json diff --git a/resources/filecontent/Cargo.toml b/resources/file_dsc/Cargo.toml similarity index 100% rename from resources/filecontent/Cargo.toml rename to resources/file_dsc/Cargo.toml diff --git a/resources/filecontent/filecontent.dsc.resource.json b/resources/file_dsc/filecontent.dsc.resource.json similarity index 100% rename from resources/filecontent/filecontent.dsc.resource.json rename to resources/file_dsc/filecontent.dsc.resource.json diff --git a/resources/filecontent/filesystem_acl.dsc.resource.json b/resources/file_dsc/filesystem_acl.dsc.resource.json similarity index 100% rename from resources/filecontent/filesystem_acl.dsc.resource.json rename to resources/file_dsc/filesystem_acl.dsc.resource.json diff --git a/resources/filecontent/locales/en-us.toml b/resources/file_dsc/locales/en-us.toml similarity index 100% rename from resources/filecontent/locales/en-us.toml rename to resources/file_dsc/locales/en-us.toml diff --git a/resources/filecontent/src/acl.rs b/resources/file_dsc/src/acl.rs similarity index 100% rename from resources/filecontent/src/acl.rs rename to resources/file_dsc/src/acl.rs diff --git a/resources/filecontent/src/file.rs b/resources/file_dsc/src/file.rs similarity index 100% rename from resources/filecontent/src/file.rs rename to resources/file_dsc/src/file.rs diff --git a/resources/filecontent/src/main.rs b/resources/file_dsc/src/main.rs similarity index 100% rename from resources/filecontent/src/main.rs rename to resources/file_dsc/src/main.rs diff --git a/resources/filecontent/src/types.rs b/resources/file_dsc/src/types.rs similarity index 100% rename from resources/filecontent/src/types.rs rename to resources/file_dsc/src/types.rs diff --git a/resources/filecontent/tests/filecontent_export.tests.ps1 b/resources/file_dsc/tests/filecontent_export.tests.ps1 similarity index 100% rename from resources/filecontent/tests/filecontent_export.tests.ps1 rename to resources/file_dsc/tests/filecontent_export.tests.ps1 diff --git a/resources/filecontent/tests/filecontent_get.tests.ps1 b/resources/file_dsc/tests/filecontent_get.tests.ps1 similarity index 100% rename from resources/filecontent/tests/filecontent_get.tests.ps1 rename to resources/file_dsc/tests/filecontent_get.tests.ps1 diff --git a/resources/filecontent/tests/filecontent_set.tests.ps1 b/resources/file_dsc/tests/filecontent_set.tests.ps1 similarity index 100% rename from resources/filecontent/tests/filecontent_set.tests.ps1 rename to resources/file_dsc/tests/filecontent_set.tests.ps1 diff --git a/resources/filecontent/tests/filecontent_test.tests.ps1 b/resources/file_dsc/tests/filecontent_test.tests.ps1 similarity index 100% rename from resources/filecontent/tests/filecontent_test.tests.ps1 rename to resources/file_dsc/tests/filecontent_test.tests.ps1 diff --git a/resources/filecontent/tests/filesystem_acl.tests.ps1 b/resources/file_dsc/tests/filesystem_acl.tests.ps1 similarity index 100% rename from resources/filecontent/tests/filesystem_acl.tests.ps1 rename to resources/file_dsc/tests/filesystem_acl.tests.ps1 diff --git a/tests/helpers.build.pester.tests.ps1 b/tests/helpers.build.pester.tests.ps1 index 42a7f5728..6dc26e71f 100644 --- a/tests/helpers.build.pester.tests.ps1 +++ b/tests/helpers.build.pester.tests.ps1 @@ -6,7 +6,7 @@ Describe 'Test-ProjectWithPester' { Import-Module (Join-Path $PSScriptRoot '..' 'helpers.build.psm1') -Force $buildData = Import-DscBuildData -RefreshProjects $dscProject = $buildData.Projects | Where-Object -Property Name -EQ 'dsc' - $fileContentProject = $buildData.Projects | Where-Object -Property Name -EQ 'filecontent' + $fileContentProject = $buildData.Projects | Where-Object -Property Name -EQ 'file_dsc' } BeforeEach { @@ -24,7 +24,7 @@ Describe 'Test-ProjectWithPester' { Should -Invoke Invoke-Pester -ModuleName helpers.build -Exactly 1 -ParameterFilter { $Path.Count -eq 2 -and $Path -contains 'dsc' -and - $Path -contains 'resources/filecontent' + $Path -contains 'resources/file_dsc' } } @@ -43,7 +43,7 @@ Describe 'Test-ProjectWithPester' { Test-ProjectWithPester -Project @($dscProject, $fileContentProject) -Group 'resources' Should -Invoke Invoke-Pester -ModuleName helpers.build -Exactly 1 -ParameterFilter { - $Path.Count -eq 1 -and $Path -contains 'resources/filecontent' + $Path.Count -eq 1 -and $Path -contains 'resources/file_dsc' } } From 4289a39974e882a0db5ae28952f4c65eea2d9ce4 Mon Sep 17 00:00:00 2001 From: "Steve Lee (POWERSHELL HE/HIM) (from Dev Box)" Date: Tue, 13 Oct 2026 12:57:02 -0700 Subject: [PATCH 3/5] Address ACL review and CI feedback Apply DACL protection state, reference the canonical desired-state schema, add negative and protected-DACL tests, and update remaining direct executable tests for file_dsc. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- dsc/tests/dsc_test_resource_errors.tests.ps1 | 6 ++-- .../file_dsc/filesystem_acl.dsc.resource.json | 24 +++++++++---- resources/file_dsc/src/acl.rs | 21 ++++++++++-- .../file_dsc/tests/filesystem_acl.tests.ps1 | 34 +++++++++++++++++++ 4 files changed, 73 insertions(+), 12 deletions(-) diff --git a/dsc/tests/dsc_test_resource_errors.tests.ps1 b/dsc/tests/dsc_test_resource_errors.tests.ps1 index e0d341b08..7792be49c 100644 --- a/dsc/tests/dsc_test_resource_errors.tests.ps1 +++ b/dsc/tests/dsc_test_resource_errors.tests.ps1 @@ -55,21 +55,21 @@ Describe 'Test resource executable error handling' { Describe 'Rust resource executable error handling' { It 'Returns failure when FileContent has no operation' { - $null = filecontent 2>$TestDrive/error.log + $null = file_dsc 2>$TestDrive/error.log $LASTEXITCODE | Should -Be 1 (Get-Content -Raw $TestDrive/error.log) | Should -Match 'Missing operation' } It 'Returns failure when FileContent has no input' { - $null = filecontent get 2>$TestDrive/error.log + $null = file_dsc get 2>$TestDrive/error.log $LASTEXITCODE | Should -Be 1 (Get-Content -Raw $TestDrive/error.log) | Should -Match 'Missing --input argument' } It 'Returns failure when FileContent input is invalid JSON' { - $null = filecontent get --input '{invalid' 2>$TestDrive/error.log + $null = file_dsc get --input '{invalid' 2>$TestDrive/error.log $LASTEXITCODE | Should -Be 2 (Get-Content -Raw $TestDrive/error.log) | Should -Match 'Invalid JSON input' diff --git a/resources/file_dsc/filesystem_acl.dsc.resource.json b/resources/file_dsc/filesystem_acl.dsc.resource.json index 670c9585f..85f67b56f 100644 --- a/resources/file_dsc/filesystem_acl.dsc.resource.json +++ b/resources/file_dsc/filesystem_acl.dsc.resource.json @@ -52,7 +52,7 @@ }, "schema": { "embedded": { - "$schema": "http://json-schema.org/draft-07/schema#", + "$schema": "https://json-schema.org/draft/2020-12/schema", "title": "Filesystem ACL", "description": "Manage the discretionary access control list of a local file or folder.", "type": "object", @@ -71,17 +71,27 @@ "SDDL": { "type": "string", "title": "Security descriptor definition language", - "description": "The discretionary access control list in SDDL form.", + "description": "The complete discretionary access control list in SDDL form. Setting this property replaces the existing DACL, including its protection state.", "minLength": 1 }, "_inDesiredState": { - "type": "boolean", - "title": "In desired state", - "description": "Indicates whether the ACL is in the desired state. Only returned by test.", - "readOnly": true + "$ref": "https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/v3/resource/properties/inDesiredState.json" } }, - "additionalProperties": false + "additionalProperties": false, + "$defs": { + "https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/v3/resource/properties/inDesiredState.json": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/v3/resource/properties/inDesiredState.json", + "title": "Instance is in the Desired State", + "description": "Indicates whether the instance is in the desired state. This property is only returned by the `test` method.", + "type": [ + "boolean", + "null" + ], + "readOnly": true + } + } } } } diff --git a/resources/file_dsc/src/acl.rs b/resources/file_dsc/src/acl.rs index 1c97be511..2185209f4 100644 --- a/resources/file_dsc/src/acl.rs +++ b/resources/file_dsc/src/acl.rs @@ -10,7 +10,9 @@ use windows::Win32::Security::Authorization::{ ConvertStringSecurityDescriptorToSecurityDescriptorW, SDDL_REVISION_1, }; use windows::Win32::Security::{ - DACL_SECURITY_INFORMATION, GetFileSecurityW, PSECURITY_DESCRIPTOR, SetFileSecurityW, + DACL_SECURITY_INFORMATION, GetFileSecurityW, GetSecurityDescriptorControl, + OBJECT_SECURITY_INFORMATION, PROTECTED_DACL_SECURITY_INFORMATION, PSECURITY_DESCRIPTOR, + SE_DACL_PROTECTED, SetFileSecurityW, UNPROTECTED_DACL_SECURITY_INFORMATION, }; use windows::core::{PCWSTR, PWSTR}; @@ -27,11 +29,12 @@ pub fn set(input: &FilesystemAcl) -> Result { validate_input(input)?; let security_descriptor = SecurityDescriptor::from_sddl(&input.sddl)?; let path = to_wide(&input.path); + let security_information = security_descriptor.dacl_security_information()?; unsafe { SetFileSecurityW( PCWSTR(path.as_ptr()), - DACL_SECURITY_INFORMATION, + security_information, security_descriptor.as_ptr(), ) } @@ -177,6 +180,20 @@ impl SecurityDescriptor { self.pointer } + fn dacl_security_information(&self) -> Result { + let mut control = 0_u16; + let mut revision = 0_u32; + unsafe { GetSecurityDescriptorControl(self.pointer, &mut control, &mut revision) } + .map_err(|error| t!("acl.invalidSddl", error = error.to_string()).to_string())?; + + let protection = if control & SE_DACL_PROTECTED.0 != 0 { + PROTECTED_DACL_SECURITY_INFORMATION + } else { + UNPROTECTED_DACL_SECURITY_INFORMATION + }; + Ok(DACL_SECURITY_INFORMATION | protection) + } + fn to_sddl(&self) -> Result { let mut output = PWSTR::null(); unsafe { diff --git a/resources/file_dsc/tests/filesystem_acl.tests.ps1 b/resources/file_dsc/tests/filesystem_acl.tests.ps1 index 49246ce1e..85e2f4bea 100644 --- a/resources/file_dsc/tests/filesystem_acl.tests.ps1 +++ b/resources/file_dsc/tests/filesystem_acl.tests.ps1 @@ -68,6 +68,40 @@ Describe 'Microsoft.Filesystem/ACL tests' -Skip:(!$IsWindows) { ($testOutput | ConvertFrom-Json).inDesiredState | Should -BeTrue } + It 'Reports a mismatched valid DACL as not in the desired state' { + $path = New-Item -ItemType File -Path (Join-Path $testRoot 'mismatch.txt') | + Select-Object -ExpandProperty FullName + $json = @{ path = $path; SDDL = $desiredSddl } | ConvertTo-Json -Compress + $null = $json | dsc resource set -r $resourceType -f - 2>$TestDrive/error.log + $LASTEXITCODE | Should -Be 0 -Because (Get-Content -Raw $TestDrive/error.log) + + $mismatchedJson = @{ + path = $path + SDDL = "D:(D;;FW;;;AN)(A;;FA;;;$currentSid)" + } | ConvertTo-Json -Compress + $testOutput = $mismatchedJson | + dsc resource test -r $resourceType -f - 2>$TestDrive/error.log + $LASTEXITCODE | Should -Be 0 -Because (Get-Content -Raw $TestDrive/error.log) + + ($testOutput | ConvertFrom-Json).inDesiredState | Should -BeFalse + } + + It 'Applies and tests the DACL protection state' { + $path = New-Item -ItemType File -Path (Join-Path $testRoot 'protected.txt') | + Select-Object -ExpandProperty FullName + $protectedSddl = $desiredSddl.Replace('D:', 'D:P') + $json = @{ path = $path; SDDL = $protectedSddl } | ConvertTo-Json -Compress + + $setOutput = $json | dsc resource set -r $resourceType -f - 2>$TestDrive/error.log + $LASTEXITCODE | Should -Be 0 -Because (Get-Content -Raw $TestDrive/error.log) + $setState = ($setOutput | ConvertFrom-Json).afterState + + $setState.SDDL | Should -Match '^D:P' + $testOutput = $json | dsc resource test -r $resourceType -f - 2>$TestDrive/error.log + $LASTEXITCODE | Should -Be 0 -Because (Get-Content -Raw $TestDrive/error.log) + ($testOutput | ConvertFrom-Json).inDesiredState | Should -BeTrue + } + It 'Rejects SDDL components that the resource does not manage' { $path = New-Item -ItemType File -Path (Join-Path $testRoot 'invalid.txt') | Select-Object -ExpandProperty FullName From 3aaa35dd71d75c1e425e50b1d04eb30ce5b48c01 Mon Sep 17 00:00:00 2001 From: "Steve Lee (POWERSHELL HE/HIM) (from Dev Box)" Date: Tue, 13 Oct 2026 13:57:28 -0700 Subject: [PATCH 4/5] Improve ACL validation errors Validate ACL operations before input parsing and preserve filesystem metadata errors when checking the target path. Add regression coverage for both cases. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- dsc/tests/dsc_test_resource_errors.tests.ps1 | 7 +++++++ resources/file_dsc/locales/en-us.toml | 1 + resources/file_dsc/src/acl.rs | 15 +++++++++++++-- resources/file_dsc/src/main.rs | 11 +++++++---- resources/file_dsc/tests/filesystem_acl.tests.ps1 | 13 +++++++++++++ 5 files changed, 41 insertions(+), 6 deletions(-) diff --git a/dsc/tests/dsc_test_resource_errors.tests.ps1 b/dsc/tests/dsc_test_resource_errors.tests.ps1 index 7792be49c..6532f4a50 100644 --- a/dsc/tests/dsc_test_resource_errors.tests.ps1 +++ b/dsc/tests/dsc_test_resource_errors.tests.ps1 @@ -75,6 +75,13 @@ Describe 'Rust resource executable error handling' { (Get-Content -Raw $TestDrive/error.log) | Should -Match 'Invalid JSON input' } + It 'Returns an unknown operation error before parsing ACL input' { + $null = file_dsc acl unknown --input '{invalid' 2>$TestDrive/error.log + + $LASTEXITCODE | Should -Be 1 + (Get-Content -Raw $TestDrive/error.log) | Should -Match "Unknown ACL operation: 'unknown'" + } + It 'Returns failure when DSC Echo input is invalid JSON' { $null = dscecho --input '{invalid' 2>$TestDrive/error.log diff --git a/resources/file_dsc/locales/en-us.toml b/resources/file_dsc/locales/en-us.toml index e83ead5f2..48cdf1849 100644 --- a/resources/file_dsc/locales/en-us.toml +++ b/resources/file_dsc/locales/en-us.toml @@ -31,6 +31,7 @@ windowsOnly = "The filesystem ACL resource is only supported on Windows" emptySddl = "The SDDL property must not be empty" unsupportedSddlComponent = "The SDDL property must contain only a discretionary ACL (D:) component" pathNotFound = "The path '%{path}' does not exist" +pathCheckError = "Failed to inspect the path '%{path}': %{error}" invalidSddl = "The SDDL property is invalid: %{error}" getError = "Failed to read the ACL for '%{path}': %{error}" setError = "Failed to set the ACL for '%{path}': %{error}" diff --git a/resources/file_dsc/src/acl.rs b/resources/file_dsc/src/acl.rs index 2185209f4..e458cd61a 100644 --- a/resources/file_dsc/src/acl.rs +++ b/resources/file_dsc/src/acl.rs @@ -73,8 +73,19 @@ fn validate_input(input: &FilesystemAcl) -> Result<(), String> { { return Err(t!("acl.unsupportedSddlComponent").to_string()); } - if !std::path::Path::new(&input.path).exists() { - return Err(t!("acl.pathNotFound", path = input.path.as_str()).to_string()); + match std::path::Path::new(&input.path).try_exists() { + Ok(true) => {} + Ok(false) => { + return Err(t!("acl.pathNotFound", path = input.path.as_str()).to_string()); + } + Err(error) => { + return Err(t!( + "acl.pathCheckError", + path = input.path.as_str(), + error = error.to_string() + ) + .to_string()); + } } Ok(()) } diff --git a/resources/file_dsc/src/main.rs b/resources/file_dsc/src/main.rs index c562b3181..c12067de2 100644 --- a/resources/file_dsc/src/main.rs +++ b/resources/file_dsc/src/main.rs @@ -59,6 +59,12 @@ fn handle_acl(args: &[String]) -> ExitCode { let Some(operation) = args.first() else { return fail(EXIT_INVALID_ARGS, &t!("acl.missingOperation")); }; + if !matches!(operation.as_str(), "get" | "set" | "test") { + return fail( + EXIT_INVALID_ARGS, + &t!("acl.unknownOperation", operation = operation), + ); + } let input = match parse_input_arg(&args[1..]) { Ok(input) => input, Err(code) => return code, @@ -74,10 +80,7 @@ fn handle_acl(args: &[String]) -> ExitCode { "get" => handle_result(acl::get(&input)), "set" => handle_result(acl::set(&input)), "test" => handle_result(acl::test(&input)), - _ => fail( - EXIT_INVALID_ARGS, - &t!("acl.unknownOperation", operation = operation), - ), + _ => unreachable!(), } } diff --git a/resources/file_dsc/tests/filesystem_acl.tests.ps1 b/resources/file_dsc/tests/filesystem_acl.tests.ps1 index 85e2f4bea..8b2e03a71 100644 --- a/resources/file_dsc/tests/filesystem_acl.tests.ps1 +++ b/resources/file_dsc/tests/filesystem_acl.tests.ps1 @@ -114,4 +114,17 @@ Describe 'Microsoft.Filesystem/ACL tests' -Skip:(!$IsWindows) { (Get-Content -Raw $TestDrive/error.log) | Should -Match 'must contain only a discretionary ACL' } + + It 'Reports path inspection errors without claiming the path is absent' { + $json = @{ + path = "invalid$([char]0)path" + SDDL = $desiredSddl + } | ConvertTo-Json -Compress + + $null = $json | dsc resource get -r $resourceType -f - 2>$TestDrive/error.log + + $LASTEXITCODE | Should -Not -Be 0 + (Get-Content -Raw $TestDrive/error.log) | Should -Match 'Failed to inspect the path' + (Get-Content -Raw $TestDrive/error.log) | Should -Not -Match 'does not exist' + } } From f8defc5ca4140c06dc1bf7b34136eb8d74682c67 Mon Sep 17 00:00:00 2001 From: "Steve Lee (POWERSHELL HE/HIM) (from Dev Box)" Date: Tue, 13 Oct 2026 15:41:14 -0700 Subject: [PATCH 5/5] Bundle file resource manifests Combine the file content and filesystem ACL manifests into one file_dsc.dsc.manifests.json bundle and package that bundle on every supported platform. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- data.build.json | 12 +- resources/file_dsc/.project.data.json | 5 +- .../file_dsc/file_dsc.dsc.manifests.json | 200 ++++++++++++++++++ .../file_dsc/filecontent.dsc.resource.json | 99 --------- .../file_dsc/filesystem_acl.dsc.resource.json | 97 --------- 5 files changed, 205 insertions(+), 208 deletions(-) create mode 100644 resources/file_dsc/file_dsc.dsc.manifests.json delete mode 100644 resources/file_dsc/filecontent.dsc.resource.json delete mode 100644 resources/file_dsc/filesystem_acl.dsc.resource.json diff --git a/data.build.json b/data.build.json index 0bf029028..a91dc0240 100644 --- a/data.build.json +++ b/data.build.json @@ -12,7 +12,7 @@ "dscecho", "echo.dsc.resource.json", "file_dsc", - "filecontent.dsc.resource.json", + "file_dsc.dsc.manifests.json", "assertion.dsc.resource.json", "apt.dsc.resource.json", "apt.dsc.resource.sh", @@ -44,7 +44,7 @@ "dscecho", "echo.dsc.resource.json", "file_dsc", - "filecontent.dsc.resource.json", + "file_dsc.dsc.manifests.json", "assertion.dsc.resource.json", "brew.dsc.resource.json", "brew.dsc.resource.sh", @@ -79,8 +79,7 @@ "dscecho.exe", "echo.dsc.resource.json", "file_dsc.exe", - "filecontent.dsc.resource.json", - "filesystem_acl.dsc.resource.json", + "file_dsc.dsc.manifests.json", "environment_variable.exe", "environment_variable.dsc.manifests.json", "assertion.dsc.resource.json", @@ -354,10 +353,7 @@ ], "CopyFiles": { "All": [ - "filecontent.dsc.resource.json" - ], - "Windows": [ - "filesystem_acl.dsc.resource.json" + "file_dsc.dsc.manifests.json" ] } }, diff --git a/resources/file_dsc/.project.data.json b/resources/file_dsc/.project.data.json index 2af882645..0777a0206 100644 --- a/resources/file_dsc/.project.data.json +++ b/resources/file_dsc/.project.data.json @@ -7,10 +7,7 @@ ], "CopyFiles": { "All": [ - "filecontent.dsc.resource.json" - ], - "Windows": [ - "filesystem_acl.dsc.resource.json" + "file_dsc.dsc.manifests.json" ] } } diff --git a/resources/file_dsc/file_dsc.dsc.manifests.json b/resources/file_dsc/file_dsc.dsc.manifests.json new file mode 100644 index 000000000..f70788ed4 --- /dev/null +++ b/resources/file_dsc/file_dsc.dsc.manifests.json @@ -0,0 +1,200 @@ +{ + "resources": [ + { + "$schema": "https://aka.ms/dsc/schemas/v3/bundled/resource/manifest.json", + "type": "Microsoft.Filesystem.File/Content", + "description": "Manage the UTF-8 content of a local file.", + "tags": [ + "file", + "linux", + "macos", + "windows" + ], + "version": "0.1.0", + "get": { + "executable": "file_dsc", + "args": [ + "get", + { + "jsonInputArg": "--input", + "mandatory": true + } + ] + }, + "set": { + "executable": "file_dsc", + "args": [ + "set", + { + "jsonInputArg": "--input", + "mandatory": true + } + ], + "implementsPretest": false, + "return": "state" + }, + "test": { + "executable": "file_dsc", + "args": [ + "test", + { + "jsonInputArg": "--input", + "mandatory": true + } + ], + "return": "state" + }, + "exitCodes": { + "0": "Success", + "1": "Invalid arguments", + "2": "Invalid input", + "3": "File operation error" + }, + "schema": { + "embedded": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "File Content", + "description": "Manage the UTF-8 content of a local file.", + "type": "object", + "required": [ + "path" + ], + "properties": { + "content": { + "type": "string", + "title": "Content", + "description": "The UTF-8 content of the file." + }, + "sha256": { + "type": "string", + "title": "SHA-256", + "description": "The SHA-256 hash of the file content.", + "pattern": "^[A-Fa-f0-9]{64}$" + }, + "sha512": { + "type": "string", + "title": "SHA-512", + "description": "The SHA-512 hash of the file content.", + "pattern": "^[A-Fa-f0-9]{128}$" + }, + "path": { + "type": "string", + "title": "Path", + "description": "The path to the local file.", + "minLength": 1 + }, + "_exist": { + "type": "boolean", + "title": "Exists", + "description": "Indicates whether the file should exist. Set to true or omit to create or update the file and any missing parent directories. Set to false to remove the file." + }, + "_inDesiredState": { + "type": "boolean", + "title": "In desired state", + "description": "Indicates whether the file is in the desired state. Only returned by test.", + "readOnly": true + } + }, + "additionalProperties": false + } + } + }, + { + "$schema": "https://aka.ms/dsc/schemas/v3/bundled/resource/manifest.json", + "type": "Microsoft.Filesystem/ACL", + "description": "Manage the discretionary access control list of a local file or folder.", + "tags": [ + "file", + "folder", + "windows" + ], + "version": "0.1.0", + "get": { + "executable": "file_dsc", + "args": [ + "acl", + "get", + { + "jsonInputArg": "--input", + "mandatory": true + } + ] + }, + "set": { + "executable": "file_dsc", + "args": [ + "acl", + "set", + { + "jsonInputArg": "--input", + "mandatory": true + } + ], + "implementsPretest": false, + "return": "state" + }, + "test": { + "executable": "file_dsc", + "args": [ + "acl", + "test", + { + "jsonInputArg": "--input", + "mandatory": true + } + ], + "return": "state" + }, + "exitCodes": { + "0": "Success", + "1": "Invalid arguments", + "2": "Invalid input", + "3": "Filesystem ACL operation error" + }, + "schema": { + "embedded": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "Filesystem ACL", + "description": "Manage the discretionary access control list of a local file or folder.", + "type": "object", + "required": [ + "path", + "SDDL" + ], + "properties": { + "path": { + "type": "string", + "title": "Path", + "description": "The path to the local file or folder.", + "minLength": 1, + "x-dsc-key": true + }, + "SDDL": { + "type": "string", + "title": "Security descriptor definition language", + "description": "The complete discretionary access control list in SDDL form. Setting this property replaces the existing DACL, including its protection state.", + "minLength": 1 + }, + "_inDesiredState": { + "$ref": "https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/v3/resource/properties/inDesiredState.json" + } + }, + "additionalProperties": false, + "$defs": { + "https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/v3/resource/properties/inDesiredState.json": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/v3/resource/properties/inDesiredState.json", + "title": "Instance is in the Desired State", + "description": "Indicates whether the instance is in the desired state. This property is only returned by the `test` method.", + "type": [ + "boolean", + "null" + ], + "readOnly": true + } + } + } + } + } + ] +} diff --git a/resources/file_dsc/filecontent.dsc.resource.json b/resources/file_dsc/filecontent.dsc.resource.json deleted file mode 100644 index 49f1e32d6..000000000 --- a/resources/file_dsc/filecontent.dsc.resource.json +++ /dev/null @@ -1,99 +0,0 @@ -{ - "$schema": "https://aka.ms/dsc/schemas/v3/bundled/resource/manifest.json", - "type": "Microsoft.Filesystem.File/Content", - "description": "Manage the UTF-8 content of a local file.", - "tags": [ - "file", - "linux", - "macos", - "windows" - ], - "version": "0.1.0", - "get": { - "executable": "file_dsc", - "args": [ - "get", - { - "jsonInputArg": "--input", - "mandatory": true - } - ] - }, - "set": { - "executable": "file_dsc", - "args": [ - "set", - { - "jsonInputArg": "--input", - "mandatory": true - } - ], - "implementsPretest": false, - "return": "state" - }, - "test": { - "executable": "file_dsc", - "args": [ - "test", - { - "jsonInputArg": "--input", - "mandatory": true - } - ], - "return": "state" - }, - "exitCodes": { - "0": "Success", - "1": "Invalid arguments", - "2": "Invalid input", - "3": "File operation error" - }, - "schema": { - "embedded": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "File Content", - "description": "Manage the UTF-8 content of a local file.", - "type": "object", - "required": [ - "path" - ], - "properties": { - "content": { - "type": "string", - "title": "Content", - "description": "The UTF-8 content of the file." - }, - "sha256": { - "type": "string", - "title": "SHA-256", - "description": "The SHA-256 hash of the file content.", - "pattern": "^[A-Fa-f0-9]{64}$" - }, - "sha512": { - "type": "string", - "title": "SHA-512", - "description": "The SHA-512 hash of the file content.", - "pattern": "^[A-Fa-f0-9]{128}$" - }, - "path": { - "type": "string", - "title": "Path", - "description": "The path to the local file.", - "minLength": 1 - }, - "_exist": { - "type": "boolean", - "title": "Exists", - "description": "Indicates whether the file should exist. Set to true or omit to create or update the file and any missing parent directories. Set to false to remove the file." - }, - "_inDesiredState": { - "type": "boolean", - "title": "In desired state", - "description": "Indicates whether the file is in the desired state. Only returned by test.", - "readOnly": true - } - }, - "additionalProperties": false - } - } -} diff --git a/resources/file_dsc/filesystem_acl.dsc.resource.json b/resources/file_dsc/filesystem_acl.dsc.resource.json deleted file mode 100644 index 85f67b56f..000000000 --- a/resources/file_dsc/filesystem_acl.dsc.resource.json +++ /dev/null @@ -1,97 +0,0 @@ -{ - "$schema": "https://aka.ms/dsc/schemas/v3/bundled/resource/manifest.json", - "type": "Microsoft.Filesystem/ACL", - "description": "Manage the discretionary access control list of a local file or folder.", - "tags": [ - "file", - "folder", - "windows" - ], - "version": "0.1.0", - "get": { - "executable": "file_dsc", - "args": [ - "acl", - "get", - { - "jsonInputArg": "--input", - "mandatory": true - } - ] - }, - "set": { - "executable": "file_dsc", - "args": [ - "acl", - "set", - { - "jsonInputArg": "--input", - "mandatory": true - } - ], - "implementsPretest": false, - "return": "state" - }, - "test": { - "executable": "file_dsc", - "args": [ - "acl", - "test", - { - "jsonInputArg": "--input", - "mandatory": true - } - ], - "return": "state" - }, - "exitCodes": { - "0": "Success", - "1": "Invalid arguments", - "2": "Invalid input", - "3": "Filesystem ACL operation error" - }, - "schema": { - "embedded": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "title": "Filesystem ACL", - "description": "Manage the discretionary access control list of a local file or folder.", - "type": "object", - "required": [ - "path", - "SDDL" - ], - "properties": { - "path": { - "type": "string", - "title": "Path", - "description": "The path to the local file or folder.", - "minLength": 1, - "x-dsc-key": true - }, - "SDDL": { - "type": "string", - "title": "Security descriptor definition language", - "description": "The complete discretionary access control list in SDDL form. Setting this property replaces the existing DACL, including its protection state.", - "minLength": 1 - }, - "_inDesiredState": { - "$ref": "https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/v3/resource/properties/inDesiredState.json" - } - }, - "additionalProperties": false, - "$defs": { - "https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/v3/resource/properties/inDesiredState.json": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/v3/resource/properties/inDesiredState.json", - "title": "Instance is in the Desired State", - "description": "Indicates whether the instance is in the desired state. This property is only returned by the `test` method.", - "type": [ - "boolean", - "null" - ], - "readOnly": true - } - } - } - } -}