test(access): assert the parameter sets that the judged matrix found untested
Adds Tests/ParameterSets.Access.Tests.ps1, closing the TestNeeded rows of
paramsets/judged-access.csv for Add-NTFSAccess, Remove-NTFSAccess,
Clear-NTFSAccess, Get-NTFSAccess, Get-NTFSOrphanedAccess,
Get-NTFSSimpleAccess, and Get-NTFSEffectiveAccess: 20 new It blocks (26
executed cases) covering pipeline binding (ValueFromPipeline and
ValueFromPipelineByPropertyName, including positional file/folder objects),
-PassThru combined with one denied and one succeeding path, error
ErrorId/category/target for read and write failures, continuation after a
per-item error, state read back with .NET (not -PassThru/Get-NTFSAccess),
SecurityDescriptor-set pipelines and memory-vs-disk behavior, and the
declared output types and PathSimple/SDSimple vs PathComplex/SDComplex
parameter-set boundaries. No row with GapKind=MaintainerDecision is pinned:
the -SecurityDescriptor sets of Add/Remove keep their open per-item error
behavior untested either way.
Finding: a plain piped object with an AppliesTo property does not steer
Add-NTFSAccess/Remove-NTFSAccess to the PathSimple/SDSimple parameter set by
itself; PowerShell resolves the ambiguity to the default set (PathComplex/
SDComplex) and silently drops the unmatched property. The proposed test for
that exact shape (notes section (b), item 2c) could not be written as
specified; the pipeline test for PathSimple instead binds -Path through the
pipeline while -AppliesTo is named explicitly, which does select the set.
Validated in all four configurations (elevated/basic user, Windows
PowerShell 5.1/PowerShell 7): 26 passed, 0 failed, 0 skipped in each. Eight
one-statement production mutations across all seven cmdlets were applied in
an isolated worktree, built, and run against this file; each was detected by
at least one new test, then the source was restored exactly and rebuilt.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
3 weeks ago
<#
Tests the parameter sets of the access cmdlets ( Add-NTFSAccess , Remove-NTFSAccess , Clear-NTFSAccess ,
Get-NTFSAccess , Get-NTFSOrphanedAccess , Get-NTFSSimpleAccess , Get-NTFSEffectiveAccess ) that the judged test matrix
found untested , with the module built in NTFSSecurity \ bin \ Release on files in a sandbox folder . Each test closes a
gap the matrix recorded for the row it names ( state , error , pipeline , continuation , or output ) ; it does not repeat
what Access . Tests . ps1 , PathErrors . Tests . ps1 , PermissionScopes . Tests . ps1 , SecurityDescriptorSets . Tests . ps1 , or
fix(access): reject a list for -AccessType and -AppliesTo, and complete in capitals in Turkish
-AccessType and -AppliesTo of Add-NTFSAccess, Remove-NTFSAccess,
Add-NTFSAudit (-AppliesTo), and Remove-NTFSAudit (-AppliesTo) take one value,
but PowerShell adds up the numbers of a comma list or an array for an
enumeration, so that -AppliesTo 'ThisFolderOnly,FilesOnly' created a
FilesOnly entry and -AccessType 'Allow,Deny' created a Deny entry, without a
message. The conversion in ParameterConversion now rejects a string with a
comma and a collection of more than one element for an enumeration that is
not a flags enumeration, with the same InvalidParameterValue error as for a
misspelled name (terminating for a named value, one error per object for a
piped one). A single name and a collection of one element convert as before,
and the flags parameters still take a list. The valid-value matrices of both
published rc8 and this build give the same entries in both editions, named,
piped by property name, and splatted, apart from the nine list rows.
The completers of the five parameters compared with WildcardOptions.IgnoreCase
only, so that a word typed in capitals did not complete while the culture of
the thread was Turkish (an I and an i are different letters there, and
THISFOLDERO did not match ThisFolderOnly). They compare culture invariant now,
like the filter of Get-ChildItem2.
The tests pin both, red before the change and green after it in four
configurations, and the headers of the two test files no longer say that the
-SecurityDescriptor sets have no handler per descriptor.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
1 week ago
PipelineControl . Tests . ps1 already assert .
test(access): assert the parameter sets that the judged matrix found untested
Adds Tests/ParameterSets.Access.Tests.ps1, closing the TestNeeded rows of
paramsets/judged-access.csv for Add-NTFSAccess, Remove-NTFSAccess,
Clear-NTFSAccess, Get-NTFSAccess, Get-NTFSOrphanedAccess,
Get-NTFSSimpleAccess, and Get-NTFSEffectiveAccess: 20 new It blocks (26
executed cases) covering pipeline binding (ValueFromPipeline and
ValueFromPipelineByPropertyName, including positional file/folder objects),
-PassThru combined with one denied and one succeeding path, error
ErrorId/category/target for read and write failures, continuation after a
per-item error, state read back with .NET (not -PassThru/Get-NTFSAccess),
SecurityDescriptor-set pipelines and memory-vs-disk behavior, and the
declared output types and PathSimple/SDSimple vs PathComplex/SDComplex
parameter-set boundaries. No row with GapKind=MaintainerDecision is pinned:
the -SecurityDescriptor sets of Add/Remove keep their open per-item error
behavior untested either way.
Finding: a plain piped object with an AppliesTo property does not steer
Add-NTFSAccess/Remove-NTFSAccess to the PathSimple/SDSimple parameter set by
itself; PowerShell resolves the ambiguity to the default set (PathComplex/
SDComplex) and silently drops the unmatched property. The proposed test for
that exact shape (notes section (b), item 2c) could not be written as
specified; the pipeline test for PathSimple instead binds -Path through the
pipeline while -AppliesTo is named explicitly, which does select the set.
Validated in all four configurations (elevated/basic user, Windows
PowerShell 5.1/PowerShell 7): 26 passed, 0 failed, 0 skipped in each. Eight
one-statement production mutations across all seven cmdlets were applied in
an isolated worktree, built, and run against this file; each was detected by
at least one new test, then the source was restored exactly and rebuilt.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
3 weeks ago
#>
[ Diagnostics . CodeAnalysis . SuppressMessageAttribute (
'PSUseDeclaredVarsMoreThanAssignments' , '' , Justification = 'Pester shares variables between blocks.'
) ]
param ( )
BeforeDiscovery {
Import-Module -Name ( Join-Path -Path $PSScriptRoot -ChildPath 'TestHelpers.psm1' ) -Force
# Security2.FileSystemAccessRule2 for Add/Remove/Get-NTFSAccess/Get-NTFSOrphanedAccess/Get-NTFSEffectiveAccess,
# Security2.SimpleFileSystemAccessRule for Get-NTFSSimpleAccess; Clear-NTFSAccess declares no output type (no -PassThru).
$declaredOutputTypes = @ (
@ { Name = 'Add-NTFSAccess' ; Types = @ ( 'Security2.FileSystemAccessRule2' ) }
@ { Name = 'Remove-NTFSAccess' ; Types = @ ( 'Security2.FileSystemAccessRule2' ) }
@ { Name = 'Get-NTFSAccess' ; Types = @ ( 'Security2.FileSystemAccessRule2' ) }
@ { Name = 'Get-NTFSOrphanedAccess' ; Types = @ ( 'Security2.FileSystemAccessRule2' ) }
@ { Name = 'Get-NTFSEffectiveAccess' ; Types = @ ( 'Security2.FileSystemAccessRule2' ) }
@ { Name = 'Get-NTFSSimpleAccess' ; Types = @ ( 'Security2.SimpleFileSystemAccessRule' ) }
)
test(access): close budget parameter-set gaps for the access cmdlets
Add 20 new It blocks (32 executed cases once the 13-scope -ForEach of
Add-NTFSAccess/SDSimple expands) to ParameterSets.Access.Tests.ps1, closing
every REMAINING item that worklist-access.md marks budget for rows 1-13:
- Add-NTFSAccess: several accounts and positional binding (PathComplex); a
Deny entry's exact rights with no auto-added Synchronize (PathComplex and
PathSimple); a denied write and a Deny entry without rights through
PathSimple; all 13 -AppliesTo scopes of a security descriptor read back
with .NET, piped and several (SDSimple); piped and several descriptors
asserted for Add itself, not only as a fixture (SDComplex).
- Remove-NTFSAccess: the -PassThru output's count against the complete
survivor list for several piped paths; several accounts and positional
binding (PathComplex); a Deny entry's rights-reduction without
-RemoveSpecific; a missing and a denied path, piped, several (PathSimple);
rights-reduction without -RemoveSpecific and disk independence until
Set-NTFSSecurityDescriptor, piped and several descriptors (SDSimple); the
on-disk write-through only after Set-NTFSSecurityDescriptor, piped and
several descriptors (SDComplex).
- Get-NTFSAccess: what the cmdlet returns without -Path, asserted on an
actual emitted object's type (Path).
- Get-NTFSOrphanedAccess: the category of ReadSecurityError after a failed
ownership retry (Path); -ExcludeInherited and an entry added to a
descriptor only in memory (SD).
- Get-NTFSSimpleAccess: the category and target of a missing path's
ReadError (Path).
- Get-NTFSEffectiveAccess: one call combining a failing and a succeeding
path (Path).
Items the REASON marks MaintainerDecision, Limit, or would-pin are left
untouched, per the group notes of the brief. No product code changed.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
2 weeks ago
# The 13 named -AppliesTo scopes, each as the ACE flags that PermissionScopes.Tests.ps1 also uses, so that an
# entry a security descriptor set added can be read back with .NET independently of the cmdlet.
$sdScopeCases = @ (
@ { Name = 'ThisFolderOnly' ; Inheritance = 'None' ; Propagation = 'None' }
@ { Name = 'ThisFolderSubfoldersAndFiles' ; Inheritance = 'ContainerInherit, ObjectInherit' ; Propagation = 'None' }
@ { Name = 'ThisFolderAndSubfolders' ; Inheritance = 'ContainerInherit' ; Propagation = 'None' }
@ { Name = 'ThisFolderAndFiles' ; Inheritance = 'ObjectInherit' ; Propagation = 'None' }
@ { Name = 'SubfoldersAndFilesOnly' ; Inheritance = 'ContainerInherit, ObjectInherit' ; Propagation = 'InheritOnly' }
@ { Name = 'SubfoldersOnly' ; Inheritance = 'ContainerInherit' ; Propagation = 'InheritOnly' }
@ { Name = 'FilesOnly' ; Inheritance = 'ObjectInherit' ; Propagation = 'InheritOnly' }
@ { Name = 'ThisFolderSubfoldersAndFilesOneLevel' ; Inheritance = 'ContainerInherit, ObjectInherit' ; Propagation = 'NoPropagateInherit' }
@ { Name = 'ThisFolderAndSubfoldersOneLevel' ; Inheritance = 'ContainerInherit' ; Propagation = 'NoPropagateInherit' }
@ { Name = 'ThisFolderAndFilesOneLevel' ; Inheritance = 'ObjectInherit' ; Propagation = 'NoPropagateInherit' }
@ { Name = 'SubfoldersAndFilesOnlyOneLevel' ; Inheritance = 'ContainerInherit, ObjectInherit' ; Propagation = 'InheritOnly, NoPropagateInherit' }
@ { Name = 'SubfoldersOnlyOneLevel' ; Inheritance = 'ContainerInherit' ; Propagation = 'InheritOnly, NoPropagateInherit' }
@ { Name = 'FilesOnlyOneLevel' ; Inheritance = 'ObjectInherit' ; Propagation = 'InheritOnly, NoPropagateInherit' }
)
test(access): assert the parameter sets that the judged matrix found untested
Adds Tests/ParameterSets.Access.Tests.ps1, closing the TestNeeded rows of
paramsets/judged-access.csv for Add-NTFSAccess, Remove-NTFSAccess,
Clear-NTFSAccess, Get-NTFSAccess, Get-NTFSOrphanedAccess,
Get-NTFSSimpleAccess, and Get-NTFSEffectiveAccess: 20 new It blocks (26
executed cases) covering pipeline binding (ValueFromPipeline and
ValueFromPipelineByPropertyName, including positional file/folder objects),
-PassThru combined with one denied and one succeeding path, error
ErrorId/category/target for read and write failures, continuation after a
per-item error, state read back with .NET (not -PassThru/Get-NTFSAccess),
SecurityDescriptor-set pipelines and memory-vs-disk behavior, and the
declared output types and PathSimple/SDSimple vs PathComplex/SDComplex
parameter-set boundaries. No row with GapKind=MaintainerDecision is pinned:
the -SecurityDescriptor sets of Add/Remove keep their open per-item error
behavior untested either way.
Finding: a plain piped object with an AppliesTo property does not steer
Add-NTFSAccess/Remove-NTFSAccess to the PathSimple/SDSimple parameter set by
itself; PowerShell resolves the ambiguity to the default set (PathComplex/
SDComplex) and silently drops the unmatched property. The proposed test for
that exact shape (notes section (b), item 2c) could not be written as
specified; the pipeline test for PathSimple instead binds -Path through the
pipeline while -AppliesTo is named explicitly, which does select the set.
Validated in all four configurations (elevated/basic user, Windows
PowerShell 5.1/PowerShell 7): 26 passed, 0 failed, 0 skipped in each. Eight
one-statement production mutations across all seven cmdlets were applied in
an isolated worktree, built, and run against this file; each was detected by
at least one new test, then the source was restored exactly and rebuilt.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
3 weeks ago
}
BeforeAll {
Import-Module -Name ( Join-Path -Path $PSScriptRoot -ChildPath 'TestHelpers.psm1' ) -Force
$modulePath = Join-Path -Path $PSScriptRoot -ChildPath '..\NTFSSecurity\bin\Release\NTFSSecurity.psd1'
Import-Module -Name $modulePath -Force -ErrorAction Stop
$sandbox = New-TestSandbox -Name 'ParameterSetsAccess'
Push-Location -LiteralPath $sandbox
$privateData = ( Get-Module -Name NTFSSecurity ) . PrivateData
$enablePrivileges = $privateData [ 'EnablePrivileges' ]
$sidType = [System.Security.Principal.SecurityIdentifier]
$currentUser = [System.Security.Principal.WindowsIdentity] :: GetCurrent ( ) . User . Value
fix(access,audit): keep the scope of a piped object from reaching the next one
Independent review of 93ed2c7: ProcessRecord of Add-NTFSAccess,
Remove-NTFSAccess, Add-NTFSAudit, and Remove-NTFSAudit wrote the scope
that it converts from AppliesTo into the fields behind InheritanceFlags
and PropagationFlags. PowerShell resets only the parameters that the
previous piped object bound, so the next object that had no scope, or
only one flag, took the scope of the one before it: AppliesTo
ThisFolderOnly, then none gave None/None twice, and Remove left the
InheritOnly part of every entry. Without the default set (93ed2c7) a
piped path object with AppliesTo reaches that code; for a piped
-SecurityDescriptor object it was a defect of rc7 already.
The flags of a record are locals now. The loop of the two Remove
cmdlets converted the scope a second time into the same fields; the
locals make that redundant, so it is gone.
The tests pipe objects with and without a scope, a CSV file, and
descriptor objects into the four cmdlets: red on the build of 93ed2c7
(8 of 87 cases of the two files, in every elevated configuration; 4 in
the basic-user ones, where the audit rows skip), green on this build.
An empty AppliesTo cell fails to bind for that row only; a test pins it.
The Remove-NTFSAudit PathSimple row cannot tell the sets apart (its
entry is on a file), and its comment says so. Docs/Examples.md no longer
says that a scope is needed so that PowerShell can choose the parameter
set (true before 5.0.0 only); the changelog entry names the new errors.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
3 weeks ago
function Get-TestAccountScope {
# The scopes of the explicit entries of one account, each as 'inheritance flags/propagation flags', joined by ';'
param (
[ Parameter ( Mandatory ) ] [System.Security.AccessControl.FileSystemSecurity] $Acl ,
[ Parameter ( Mandatory ) ] [string] $Account
)
@ ( $Acl . GetAccessRules ( $true , $false , $sidType ) |
Where-Object -FilterScript { $_ . IdentityReference . Value -eq $Account } |
ForEach-Object -Process { '{0}/{1}' -f $_ . InheritanceFlags , $_ . PropagationFlags } ) -join ';'
}
test(access): assert the parameter sets that the judged matrix found untested
Adds Tests/ParameterSets.Access.Tests.ps1, closing the TestNeeded rows of
paramsets/judged-access.csv for Add-NTFSAccess, Remove-NTFSAccess,
Clear-NTFSAccess, Get-NTFSAccess, Get-NTFSOrphanedAccess,
Get-NTFSSimpleAccess, and Get-NTFSEffectiveAccess: 20 new It blocks (26
executed cases) covering pipeline binding (ValueFromPipeline and
ValueFromPipelineByPropertyName, including positional file/folder objects),
-PassThru combined with one denied and one succeeding path, error
ErrorId/category/target for read and write failures, continuation after a
per-item error, state read back with .NET (not -PassThru/Get-NTFSAccess),
SecurityDescriptor-set pipelines and memory-vs-disk behavior, and the
declared output types and PathSimple/SDSimple vs PathComplex/SDComplex
parameter-set boundaries. No row with GapKind=MaintainerDecision is pinned:
the -SecurityDescriptor sets of Add/Remove keep their open per-item error
behavior untested either way.
Finding: a plain piped object with an AppliesTo property does not steer
Add-NTFSAccess/Remove-NTFSAccess to the PathSimple/SDSimple parameter set by
itself; PowerShell resolves the ambiguity to the default set (PathComplex/
SDComplex) and silently drops the unmatched property. The proposed test for
that exact shape (notes section (b), item 2c) could not be written as
specified; the pipeline test for PathSimple instead binds -Path through the
pipeline while -AppliesTo is named explicitly, which does select the set.
Validated in all four configurations (elevated/basic user, Windows
PowerShell 5.1/PowerShell 7): 26 passed, 0 failed, 0 skipped in each. Eight
one-statement production mutations across all seven cmdlets were applied in
an isolated worktree, built, and run against this file; each was detected by
at least one new test, then the source was restored exactly and rebuilt.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
3 weeks ago
}
AfterAll {
$privateData [ 'EnablePrivileges' ] = $enablePrivileges
Pop-Location
Remove-TestSandbox -Sandbox $sandbox
Remove-Module -Name NTFSSecurity -Force -ErrorAction SilentlyContinue
}
Describe 'Add-NTFSAccess' {
Context 'PathComplex parameter set bound through the pipeline' {
It 'Should add one explicit entry for each path bound through the pipeline, by FullName and by property name, leaving the rest of each item unchanged' {
$first = New-TestSandboxItem -Sandbox $sandbox -Name 'AddPipeFirst'
$second = New-TestSandboxItem -Sandbox $sandbox -Name 'AddPipeSecond'
$third = New-TestSandboxItem -Sandbox $sandbox -Name 'AddPipeThird'
$before = foreach ( $path in $first , $second , $third ) {
$acl = Get-Acl -LiteralPath $path
[pscustomobject] @ { Path = $path ; Owner = $acl . GetOwner ( $sidType ) . Value ; Inherited = @ ( $acl . GetAccessRules ( $false , $true , $sidType ) ) . Count }
}
Get-Item2 -Path $first , $second | Add-NTFSAccess -Account 'S-1-1-0' -AccessRights ReadData -ErrorAction Stop
[pscustomobject] @ { Path = $third ; Account = 'S-1-1-0' ; AccessRights = 'ReadData' } | Add-NTFSAccess -ErrorAction Stop
foreach ( $state in $before ) {
$acl = Get-Acl -LiteralPath $state . Path
$acl . GetOwner ( $sidType ) . Value | Should -Be $state . Owner
@ ( $acl . GetAccessRules ( $false , $true , $sidType ) ) | Should -HaveCount $state . Inherited
$explicit = @ ( $acl . GetAccessRules ( $true , $false , $sidType ) | Where-Object -FilterScript { $_ . IdentityReference . Value -eq 'S-1-1-0' } )
$explicit | Should -HaveCount 1
$explicit [ 0 ] . FileSystemRights . HasFlag ( [System.Security.AccessControl.FileSystemRights] :: ReadData ) | Should -BeTrue
}
}
}
Context 'PathComplex parameter set with one denied path and -PassThru' {
BeforeAll {
# So that an elevated session meets the same denial as a basic user (see PathErrors.Tests.ps1).
$privateData [ 'EnablePrivileges' ] = $false
}
AfterAll {
$privateData [ 'EnablePrivileges' ] = $enablePrivileges
}
It 'Should write one AddAceError for the blocked path, leave it unchanged, and return -PassThru entries only for the path it changed' {
$blocked = New-TestSandboxItem -Sandbox $sandbox -Name 'AddPassThruBlocked'
$next = New-TestSandboxItem -Sandbox $sandbox -Name 'AddPassThruNext'
Block-TestWritePermission -Sandbox $sandbox -Path $blocked
$before = ( Get-Acl -LiteralPath $blocked ) . Sddl
$result = @ ( Add-NTFSAccess -Path $blocked , $next -Account 'S-1-1-0' -AccessRights ReadData -PassThru -ErrorVariable addErrors -ErrorAction SilentlyContinue )
$addErrors | Should -HaveCount 1
$addErrors [ 0 ] . FullyQualifiedErrorId | Should -BeLike 'AddAceError,*'
$addErrors [ 0 ] . CategoryInfo . Category | Should -Be 'WriteError'
$addErrors [ 0 ] . TargetObject | Should -Be $blocked
( Get-Acl -LiteralPath $blocked ) . Sddl | Should -BeExactly $before
$result | Should -Not -BeNullOrEmpty
$result | ForEach-Object -Process {
$_ | Should -BeOfType ( [Security2.FileSystemAccessRule2] )
$_ . FullName | Should -Be $next
}
@ ( $result | Where-Object -FilterScript { $_ . Account . Sid -eq 'S-1-1-0' -and -not $_ . IsInherited } ) | Should -HaveCount 1
}
}
fix(access,audit): bind the AppliesTo property of a piped object for paths
Add-NTFSAccess, Remove-NTFSAccess, Add-NTFSAudit, and Remove-NTFSAudit
declared PathComplex as the default parameter set. An object that had
Path, Account, and AccessRights satisfied the mandatory parameters of
the default set, so PowerShell never tried PathSimple and the cmdlet
dropped the AppliesTo property of the object. Since 4b1abc9 made
-AppliesTo mandatory in the Simple sets, the default set is redundant
for every call that names its parameters, and it masked PathSimple for
the pipeline.
Without a default set, an object that has AppliesTo binds PathSimple,
one that has InheritanceFlags or PropagationFlags binds PathComplex, and
one that has neither binds PathComplex with the default scope. An
object that has AppliesTo and InheritanceFlags now fails with
AmbiguousParameterSet, like the same two named parameters. Named calls,
-SecurityDescriptor calls without AppliesTo, and the round trip
Get-NTFSAccess | Add-NTFSAccess keep their behavior.
The tests pipe objects of each shape and fail without the change. The
cmdlet pages and the help file follow the build.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
3 weeks ago
# The property AppliesTo selects PathSimple, InheritanceFlags and PropagationFlags select PathComplex, and an object that
# has none of them gets the default scope. No parameter set is the default: while PathComplex was, an object that had
# AppliesTo bound PathComplex as well, and the cmdlet dropped the property.
Context 'Choosing the parameter set of an object bound through the pipeline by property name' {
It 'Should give the entry for the piped object that has <Shape> the scope <Scope>' -ForEach @ (
@ { Name = 'AddPipedThisFolderOnly' ; Shape = 'AppliesTo ThisFolderOnly' ; Extra = @ { AppliesTo = 'ThisFolderOnly' } ; Scope = 'None/None' }
@ { Name = 'AddPipedSubfolders' ; Shape = 'AppliesTo ThisFolderAndSubfolders' ; Extra = @ { AppliesTo = 'ThisFolderAndSubfolders' } ; Scope = 'ContainerInherit/None' }
@ { Name = 'AddPipedInheritOnly' ; Shape = 'AppliesTo SubfoldersAndFilesOnly' ; Extra = @ { AppliesTo = 'SubfoldersAndFilesOnly' } ; Scope = 'ContainerInherit, ObjectInherit/InheritOnly' }
@ { Name = 'AddPipedFlags' ; Shape = 'InheritanceFlags and PropagationFlags' ; Extra = @ { InheritanceFlags = 'ContainerInherit' ; PropagationFlags = 'InheritOnly' } ; Scope = 'ContainerInherit/InheritOnly' }
@ { Name = 'AddPipedNoScope' ; Shape = 'no scope' ; Extra = @ { } ; Scope = 'ContainerInherit, ObjectInherit/None' }
) {
$folder = New-TestSandboxItem -Sandbox $sandbox -Name $Name -Directory
[pscustomobject] ( @ { Path = $folder ; Account = 'S-1-5-32-546' ; AccessRights = 'ReadData' } + $Extra ) | Add-NTFSAccess -ErrorAction Stop
fix(access,audit): keep the scope of a piped object from reaching the next one
Independent review of 93ed2c7: ProcessRecord of Add-NTFSAccess,
Remove-NTFSAccess, Add-NTFSAudit, and Remove-NTFSAudit wrote the scope
that it converts from AppliesTo into the fields behind InheritanceFlags
and PropagationFlags. PowerShell resets only the parameters that the
previous piped object bound, so the next object that had no scope, or
only one flag, took the scope of the one before it: AppliesTo
ThisFolderOnly, then none gave None/None twice, and Remove left the
InheritOnly part of every entry. Without the default set (93ed2c7) a
piped path object with AppliesTo reaches that code; for a piped
-SecurityDescriptor object it was a defect of rc7 already.
The flags of a record are locals now. The loop of the two Remove
cmdlets converted the scope a second time into the same fields; the
locals make that redundant, so it is gone.
The tests pipe objects with and without a scope, a CSV file, and
descriptor objects into the four cmdlets: red on the build of 93ed2c7
(8 of 87 cases of the two files, in every elevated configuration; 4 in
the basic-user ones, where the audit rows skip), green on this build.
An empty AppliesTo cell fails to bind for that row only; a test pins it.
The Remove-NTFSAudit PathSimple row cannot tell the sets apart (its
entry is on a file), and its comment says so. Docs/Examples.md no longer
says that a scope is needed so that PowerShell can choose the parameter
set (true before 5.0.0 only); the changelog entry names the new errors.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
3 weeks ago
Get-TestAccountScope -Acl ( Get-Acl -LiteralPath $folder ) -Account 'S-1-5-32-546' | Should -BeExactly $Scope
fix(access,audit): bind the AppliesTo property of a piped object for paths
Add-NTFSAccess, Remove-NTFSAccess, Add-NTFSAudit, and Remove-NTFSAudit
declared PathComplex as the default parameter set. An object that had
Path, Account, and AccessRights satisfied the mandatory parameters of
the default set, so PowerShell never tried PathSimple and the cmdlet
dropped the AppliesTo property of the object. Since 4b1abc9 made
-AppliesTo mandatory in the Simple sets, the default set is redundant
for every call that names its parameters, and it masked PathSimple for
the pipeline.
Without a default set, an object that has AppliesTo binds PathSimple,
one that has InheritanceFlags or PropagationFlags binds PathComplex, and
one that has neither binds PathComplex with the default scope. An
object that has AppliesTo and InheritanceFlags now fails with
AmbiguousParameterSet, like the same two named parameters. Named calls,
-SecurityDescriptor calls without AppliesTo, and the round trip
Get-NTFSAccess | Add-NTFSAccess keep their behavior.
The tests pipe objects of each shape and fail without the change. The
cmdlet pages and the help file follow the build.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
3 weeks ago
}
It 'Should reject a piped object that has both AppliesTo and InheritanceFlags with an AmbiguousParameterSet error and change nothing' {
$folder = New-TestSandboxItem -Sandbox $sandbox -Name 'AddPipedBoth' -Directory
$before = ( Get-Acl -LiteralPath $folder ) . Sddl
$object = [pscustomobject] @ { Path = $folder ; Account = 'S-1-1-0' ; AccessRights = 'ReadData' ; AppliesTo = 'ThisFolderOnly' ; InheritanceFlags = 'None' }
{ $object | Add-NTFSAccess -ErrorAction Stop } | Should -Throw -ErrorId 'AmbiguousParameterSet*'
( Get-Acl -LiteralPath $folder ) . Sddl | Should -BeExactly $before
}
}
fix(access,audit): keep the scope of a piped object from reaching the next one
Independent review of 93ed2c7: ProcessRecord of Add-NTFSAccess,
Remove-NTFSAccess, Add-NTFSAudit, and Remove-NTFSAudit wrote the scope
that it converts from AppliesTo into the fields behind InheritanceFlags
and PropagationFlags. PowerShell resets only the parameters that the
previous piped object bound, so the next object that had no scope, or
only one flag, took the scope of the one before it: AppliesTo
ThisFolderOnly, then none gave None/None twice, and Remove left the
InheritOnly part of every entry. Without the default set (93ed2c7) a
piped path object with AppliesTo reaches that code; for a piped
-SecurityDescriptor object it was a defect of rc7 already.
The flags of a record are locals now. The loop of the two Remove
cmdlets converted the scope a second time into the same fields; the
locals make that redundant, so it is gone.
The tests pipe objects with and without a scope, a CSV file, and
descriptor objects into the four cmdlets: red on the build of 93ed2c7
(8 of 87 cases of the two files, in every elevated configuration; 4 in
the basic-user ones, where the audit rows skip), green on this build.
An empty AppliesTo cell fails to bind for that row only; a test pins it.
The Remove-NTFSAudit PathSimple row cannot tell the sets apart (its
entry is on a file), and its comment says so. Docs/Examples.md no longer
says that a scope is needed so that PowerShell can choose the parameter
set (true before 5.0.0 only); the changelog entry names the new errors.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
3 weeks ago
# PowerShell resets only the parameters that the previous object bound from the pipeline. The scope that the cmdlet
# derives from AppliesTo must therefore not stay in the fields behind InheritanceFlags and PropagationFlags: the next
# object that has no scope, or only one flag, would get it.
Context 'Objects with different scopes in one pipeline' {
It 'Should give each piped path object only its own scope: AppliesTo, none, AppliesTo, one flag, none' {
$folders = @ ( 'AddMixedThisFolderOnly' , 'AddMixedNoScope' , 'AddMixedInheritOnly' , 'AddMixedOneFlag' , 'AddMixedNoScopeAgain' |
ForEach-Object -Process { New-TestSandboxItem -Sandbox $sandbox -Name $_ -Directory } )
$entry = @ { Account = 'S-1-5-32-546' ; AccessRights = 'ReadData' }
@ (
[pscustomobject] ( $entry + @ { Path = $folders [ 0 ] ; AppliesTo = 'ThisFolderOnly' } )
[pscustomobject] ( $entry + @ { Path = $folders [ 1 ] } )
[pscustomobject] ( $entry + @ { Path = $folders [ 2 ] ; AppliesTo = 'SubfoldersAndFilesOnly' } )
[pscustomobject] ( $entry + @ { Path = $folders [ 3 ] ; InheritanceFlags = 'ContainerInherit' } )
[pscustomobject] ( $entry + @ { Path = $folders [ 4 ] } )
) | Add-NTFSAccess -ErrorAction Stop
$scopes = $folders | ForEach-Object -Process { Get-TestAccountScope -Acl ( Get-Acl -LiteralPath $_ ) -Account 'S-1-5-32-546' }
$scopes -join '|' | Should -BeExactly 'None/None|ContainerInherit, ObjectInherit/None|ContainerInherit, ObjectInherit/InheritOnly|ContainerInherit/None|ContainerInherit, ObjectInherit/None'
}
docs(access,audit): say what an empty or null AppliesTo does, and test the Import-Csv route
The follow-up review of 9783d2a found that "an empty AppliesTo fails to
bind", in the changelog entry and in the four cmdlet pages, holds only for
$null. A blank cell that Import-Csv reads is an empty string, which the
binder cannot convert: PathSimple does not bind, and the row gets the
default scope, as an object without the property does. An AppliesTo that
is not the name of a scope is ignored the same way (probe
p-empty-appliesto.ps1, both editions, on the package of 9783d2a and on
the baseline). Only $null fails to bind, with
ParameterArgumentValidationErrorNullNotAllowed, for that object.
The pages, the help file, and the changelog entry now say so.
The tests of the CSV route read a CSV file with Import-Csv, the route
that the documentation shows, instead of ConvertFrom-Csv of strings,
which makes a trailing blank cell $null. They pin the blank cell as the
default scope and the $null object as a binding error for that object
only, with the other objects processed.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
3 weeks ago
It 'Should give each row of a CSV file that Import-Csv reads and that has an AppliesTo column its own scope' {
fix(access,audit): keep the scope of a piped object from reaching the next one
Independent review of 93ed2c7: ProcessRecord of Add-NTFSAccess,
Remove-NTFSAccess, Add-NTFSAudit, and Remove-NTFSAudit wrote the scope
that it converts from AppliesTo into the fields behind InheritanceFlags
and PropagationFlags. PowerShell resets only the parameters that the
previous piped object bound, so the next object that had no scope, or
only one flag, took the scope of the one before it: AppliesTo
ThisFolderOnly, then none gave None/None twice, and Remove left the
InheritOnly part of every entry. Without the default set (93ed2c7) a
piped path object with AppliesTo reaches that code; for a piped
-SecurityDescriptor object it was a defect of rc7 already.
The flags of a record are locals now. The loop of the two Remove
cmdlets converted the scope a second time into the same fields; the
locals make that redundant, so it is gone.
The tests pipe objects with and without a scope, a CSV file, and
descriptor objects into the four cmdlets: red on the build of 93ed2c7
(8 of 87 cases of the two files, in every elevated configuration; 4 in
the basic-user ones, where the audit rows skip), green on this build.
An empty AppliesTo cell fails to bind for that row only; a test pins it.
The Remove-NTFSAudit PathSimple row cannot tell the sets apart (its
entry is on a file), and its comment says so. Docs/Examples.md no longer
says that a scope is needed so that PowerShell can choose the parameter
set (true before 5.0.0 only); the changelog entry names the new errors.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
3 weeks ago
$folders = @ ( 'AddCsvThisFolderOnly' , 'AddCsvSubfolders' , 'AddCsvInheritOnly' |
ForEach-Object -Process { New-TestSandboxItem -Sandbox $sandbox -Name $_ -Directory } )
docs(access,audit): say what an empty or null AppliesTo does, and test the Import-Csv route
The follow-up review of 9783d2a found that "an empty AppliesTo fails to
bind", in the changelog entry and in the four cmdlet pages, holds only for
$null. A blank cell that Import-Csv reads is an empty string, which the
binder cannot convert: PathSimple does not bind, and the row gets the
default scope, as an object without the property does. An AppliesTo that
is not the name of a scope is ignored the same way (probe
p-empty-appliesto.ps1, both editions, on the package of 9783d2a and on
the baseline). Only $null fails to bind, with
ParameterArgumentValidationErrorNullNotAllowed, for that object.
The pages, the help file, and the changelog entry now say so.
The tests of the CSV route read a CSV file with Import-Csv, the route
that the documentation shows, instead of ConvertFrom-Csv of strings,
which makes a trailing blank cell $null. They pin the blank cell as the
default scope and the $null object as a binding error for that object
only, with the other objects processed.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
3 weeks ago
$csv = Join-Path -Path $sandbox -ChildPath 'AddCsvScopes.csv'
Assert-TestSandboxPath -Sandbox $sandbox -Path $csv
Set-Content -LiteralPath $csv -Value @ (
fix(access,audit): keep the scope of a piped object from reaching the next one
Independent review of 93ed2c7: ProcessRecord of Add-NTFSAccess,
Remove-NTFSAccess, Add-NTFSAudit, and Remove-NTFSAudit wrote the scope
that it converts from AppliesTo into the fields behind InheritanceFlags
and PropagationFlags. PowerShell resets only the parameters that the
previous piped object bound, so the next object that had no scope, or
only one flag, took the scope of the one before it: AppliesTo
ThisFolderOnly, then none gave None/None twice, and Remove left the
InheritOnly part of every entry. Without the default set (93ed2c7) a
piped path object with AppliesTo reaches that code; for a piped
-SecurityDescriptor object it was a defect of rc7 already.
The flags of a record are locals now. The loop of the two Remove
cmdlets converted the scope a second time into the same fields; the
locals make that redundant, so it is gone.
The tests pipe objects with and without a scope, a CSV file, and
descriptor objects into the four cmdlets: red on the build of 93ed2c7
(8 of 87 cases of the two files, in every elevated configuration; 4 in
the basic-user ones, where the audit rows skip), green on this build.
An empty AppliesTo cell fails to bind for that row only; a test pins it.
The Remove-NTFSAudit PathSimple row cannot tell the sets apart (its
entry is on a file), and its comment says so. Docs/Examples.md no longer
says that a scope is needed so that PowerShell can choose the parameter
set (true before 5.0.0 only); the changelog entry names the new errors.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
3 weeks ago
'Path,Account,AccessRights,AppliesTo'
( '"{0}",S-1-5-32-546,ReadData,ThisFolderOnly' -f $folders [ 0 ] )
( '"{0}",S-1-5-32-546,ReadData,ThisFolderAndSubfolders' -f $folders [ 1 ] )
( '"{0}",S-1-5-32-546,ReadData,SubfoldersAndFilesOnly' -f $folders [ 2 ] )
docs(access,audit): say what an empty or null AppliesTo does, and test the Import-Csv route
The follow-up review of 9783d2a found that "an empty AppliesTo fails to
bind", in the changelog entry and in the four cmdlet pages, holds only for
$null. A blank cell that Import-Csv reads is an empty string, which the
binder cannot convert: PathSimple does not bind, and the row gets the
default scope, as an object without the property does. An AppliesTo that
is not the name of a scope is ignored the same way (probe
p-empty-appliesto.ps1, both editions, on the package of 9783d2a and on
the baseline). Only $null fails to bind, with
ParameterArgumentValidationErrorNullNotAllowed, for that object.
The pages, the help file, and the changelog entry now say so.
The tests of the CSV route read a CSV file with Import-Csv, the route
that the documentation shows, instead of ConvertFrom-Csv of strings,
which makes a trailing blank cell $null. They pin the blank cell as the
default scope and the $null object as a binding error for that object
only, with the other objects processed.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
3 weeks ago
)
fix(access,audit): keep the scope of a piped object from reaching the next one
Independent review of 93ed2c7: ProcessRecord of Add-NTFSAccess,
Remove-NTFSAccess, Add-NTFSAudit, and Remove-NTFSAudit wrote the scope
that it converts from AppliesTo into the fields behind InheritanceFlags
and PropagationFlags. PowerShell resets only the parameters that the
previous piped object bound, so the next object that had no scope, or
only one flag, took the scope of the one before it: AppliesTo
ThisFolderOnly, then none gave None/None twice, and Remove left the
InheritOnly part of every entry. Without the default set (93ed2c7) a
piped path object with AppliesTo reaches that code; for a piped
-SecurityDescriptor object it was a defect of rc7 already.
The flags of a record are locals now. The loop of the two Remove
cmdlets converted the scope a second time into the same fields; the
locals make that redundant, so it is gone.
The tests pipe objects with and without a scope, a CSV file, and
descriptor objects into the four cmdlets: red on the build of 93ed2c7
(8 of 87 cases of the two files, in every elevated configuration; 4 in
the basic-user ones, where the audit rows skip), green on this build.
An empty AppliesTo cell fails to bind for that row only; a test pins it.
The Remove-NTFSAudit PathSimple row cannot tell the sets apart (its
entry is on a file), and its comment says so. Docs/Examples.md no longer
says that a scope is needed so that PowerShell can choose the parameter
set (true before 5.0.0 only); the changelog entry names the new errors.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
3 weeks ago
docs(access,audit): say what an empty or null AppliesTo does, and test the Import-Csv route
The follow-up review of 9783d2a found that "an empty AppliesTo fails to
bind", in the changelog entry and in the four cmdlet pages, holds only for
$null. A blank cell that Import-Csv reads is an empty string, which the
binder cannot convert: PathSimple does not bind, and the row gets the
default scope, as an object without the property does. An AppliesTo that
is not the name of a scope is ignored the same way (probe
p-empty-appliesto.ps1, both editions, on the package of 9783d2a and on
the baseline). Only $null fails to bind, with
ParameterArgumentValidationErrorNullNotAllowed, for that object.
The pages, the help file, and the changelog entry now say so.
The tests of the CSV route read a CSV file with Import-Csv, the route
that the documentation shows, instead of ConvertFrom-Csv of strings,
which makes a trailing blank cell $null. They pin the blank cell as the
default scope and the $null object as a binding error for that object
only, with the other objects processed.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
3 weeks ago
Import-Csv -LiteralPath $csv | Add-NTFSAccess -ErrorAction Stop
fix(access,audit): keep the scope of a piped object from reaching the next one
Independent review of 93ed2c7: ProcessRecord of Add-NTFSAccess,
Remove-NTFSAccess, Add-NTFSAudit, and Remove-NTFSAudit wrote the scope
that it converts from AppliesTo into the fields behind InheritanceFlags
and PropagationFlags. PowerShell resets only the parameters that the
previous piped object bound, so the next object that had no scope, or
only one flag, took the scope of the one before it: AppliesTo
ThisFolderOnly, then none gave None/None twice, and Remove left the
InheritOnly part of every entry. Without the default set (93ed2c7) a
piped path object with AppliesTo reaches that code; for a piped
-SecurityDescriptor object it was a defect of rc7 already.
The flags of a record are locals now. The loop of the two Remove
cmdlets converted the scope a second time into the same fields; the
locals make that redundant, so it is gone.
The tests pipe objects with and without a scope, a CSV file, and
descriptor objects into the four cmdlets: red on the build of 93ed2c7
(8 of 87 cases of the two files, in every elevated configuration; 4 in
the basic-user ones, where the audit rows skip), green on this build.
An empty AppliesTo cell fails to bind for that row only; a test pins it.
The Remove-NTFSAudit PathSimple row cannot tell the sets apart (its
entry is on a file), and its comment says so. Docs/Examples.md no longer
says that a scope is needed so that PowerShell can choose the parameter
set (true before 5.0.0 only); the changelog entry names the new errors.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
3 weeks ago
$scopes = $folders | ForEach-Object -Process { Get-TestAccountScope -Acl ( Get-Acl -LiteralPath $_ ) -Account 'S-1-5-32-546' }
$scopes -join '|' | Should -BeExactly 'None/None|ContainerInherit/None|ContainerInherit, ObjectInherit/InheritOnly'
}
fix(access): report a piped AccessType, AuditFlags, flags, or scope that isn't valid
Add-NTFSAccess, Remove-NTFSAccess, Add-NTFSAudit, and Remove-NTFSAudit
silently kept the default value of AccessType, AuditFlags,
InheritanceFlags, PropagationFlags, and AppliesTo when a piped object's
property could not be converted to the parameter's type: a misspelled
AccessType such as "Denyy", or a blank CSV cell, created an Allow entry
with no message. For a permissions tool, a typo for Deny that silently
grants access is a security-relevant defect (Decision 27).
The five parameters are now declared as object and converted inside
the cmdlet, the way the PowerShell binder converts a typed parameter
(names in any case, comma-separated flag lists, numbers, enum
instances). A value that cannot be converted, including a blank one,
is now an error named InvalidParameterValue: terminating and
catchable when it is named on the command line, exactly as the binder
reported it before; a non-terminating error targeting that path or
descriptor when it comes from a piped object, which leaves that item
unchanged and lets the pipeline continue with the next one. A $null
value is still not an error and keeps the parameter's default. Tab
completion of the five parameters keeps working through explicit
argument completers. Choosing between the Simple and Complex
parameter sets is unaffected and still rejects AppliesTo together
with InheritanceFlags or PropagationFlags as AmbiguousParameterSet,
for every mix of string and enum values.
The four cmdlet pages document the parameters as Type: Object, list
the valid values, and describe the new error; a sentence that said a
piped AppliesTo typo or blank cell was silently ignored is corrected.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
1 week ago
# A blank cell that Import-Csv reads is an empty string: not $null, and not a scope name. Since -AppliesTo is
# now an object parameter (Decision 27, fix B), the blank string still binds it, which selects PathSimple,
# so the row gets an InvalidParameterValue error instead of the default scope; the other rows are unaffected.
It 'Should write an InvalidParameterValue error for the row of a CSV file whose AppliesTo cell is blank, leave its folder alone, and still give the other rows their own scope' {
docs(access,audit): say what an empty or null AppliesTo does, and test the Import-Csv route
The follow-up review of 9783d2a found that "an empty AppliesTo fails to
bind", in the changelog entry and in the four cmdlet pages, holds only for
$null. A blank cell that Import-Csv reads is an empty string, which the
binder cannot convert: PathSimple does not bind, and the row gets the
default scope, as an object without the property does. An AppliesTo that
is not the name of a scope is ignored the same way (probe
p-empty-appliesto.ps1, both editions, on the package of 9783d2a and on
the baseline). Only $null fails to bind, with
ParameterArgumentValidationErrorNullNotAllowed, for that object.
The pages, the help file, and the changelog entry now say so.
The tests of the CSV route read a CSV file with Import-Csv, the route
that the documentation shows, instead of ConvertFrom-Csv of strings,
which makes a trailing blank cell $null. They pin the blank cell as the
default scope and the $null object as a binding error for that object
only, with the other objects processed.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
3 weeks ago
$folders = @ ( 'AddCsvBlankFirst' , 'AddCsvBlankCell' , 'AddCsvBlankLast' |
fix(access,audit): keep the scope of a piped object from reaching the next one
Independent review of 93ed2c7: ProcessRecord of Add-NTFSAccess,
Remove-NTFSAccess, Add-NTFSAudit, and Remove-NTFSAudit wrote the scope
that it converts from AppliesTo into the fields behind InheritanceFlags
and PropagationFlags. PowerShell resets only the parameters that the
previous piped object bound, so the next object that had no scope, or
only one flag, took the scope of the one before it: AppliesTo
ThisFolderOnly, then none gave None/None twice, and Remove left the
InheritOnly part of every entry. Without the default set (93ed2c7) a
piped path object with AppliesTo reaches that code; for a piped
-SecurityDescriptor object it was a defect of rc7 already.
The flags of a record are locals now. The loop of the two Remove
cmdlets converted the scope a second time into the same fields; the
locals make that redundant, so it is gone.
The tests pipe objects with and without a scope, a CSV file, and
descriptor objects into the four cmdlets: red on the build of 93ed2c7
(8 of 87 cases of the two files, in every elevated configuration; 4 in
the basic-user ones, where the audit rows skip), green on this build.
An empty AppliesTo cell fails to bind for that row only; a test pins it.
The Remove-NTFSAudit PathSimple row cannot tell the sets apart (its
entry is on a file), and its comment says so. Docs/Examples.md no longer
says that a scope is needed so that PowerShell can choose the parameter
set (true before 5.0.0 only); the changelog entry names the new errors.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
3 weeks ago
ForEach-Object -Process { New-TestSandboxItem -Sandbox $sandbox -Name $_ -Directory } )
docs(access,audit): say what an empty or null AppliesTo does, and test the Import-Csv route
The follow-up review of 9783d2a found that "an empty AppliesTo fails to
bind", in the changelog entry and in the four cmdlet pages, holds only for
$null. A blank cell that Import-Csv reads is an empty string, which the
binder cannot convert: PathSimple does not bind, and the row gets the
default scope, as an object without the property does. An AppliesTo that
is not the name of a scope is ignored the same way (probe
p-empty-appliesto.ps1, both editions, on the package of 9783d2a and on
the baseline). Only $null fails to bind, with
ParameterArgumentValidationErrorNullNotAllowed, for that object.
The pages, the help file, and the changelog entry now say so.
The tests of the CSV route read a CSV file with Import-Csv, the route
that the documentation shows, instead of ConvertFrom-Csv of strings,
which makes a trailing blank cell $null. They pin the blank cell as the
default scope and the $null object as a binding error for that object
only, with the other objects processed.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
3 weeks ago
$csv = Join-Path -Path $sandbox -ChildPath 'AddCsvBlank.csv'
Assert-TestSandboxPath -Sandbox $sandbox -Path $csv
Set-Content -LiteralPath $csv -Value @ (
fix(access,audit): keep the scope of a piped object from reaching the next one
Independent review of 93ed2c7: ProcessRecord of Add-NTFSAccess,
Remove-NTFSAccess, Add-NTFSAudit, and Remove-NTFSAudit wrote the scope
that it converts from AppliesTo into the fields behind InheritanceFlags
and PropagationFlags. PowerShell resets only the parameters that the
previous piped object bound, so the next object that had no scope, or
only one flag, took the scope of the one before it: AppliesTo
ThisFolderOnly, then none gave None/None twice, and Remove left the
InheritOnly part of every entry. Without the default set (93ed2c7) a
piped path object with AppliesTo reaches that code; for a piped
-SecurityDescriptor object it was a defect of rc7 already.
The flags of a record are locals now. The loop of the two Remove
cmdlets converted the scope a second time into the same fields; the
locals make that redundant, so it is gone.
The tests pipe objects with and without a scope, a CSV file, and
descriptor objects into the four cmdlets: red on the build of 93ed2c7
(8 of 87 cases of the two files, in every elevated configuration; 4 in
the basic-user ones, where the audit rows skip), green on this build.
An empty AppliesTo cell fails to bind for that row only; a test pins it.
The Remove-NTFSAudit PathSimple row cannot tell the sets apart (its
entry is on a file), and its comment says so. Docs/Examples.md no longer
says that a scope is needed so that PowerShell can choose the parameter
set (true before 5.0.0 only); the changelog entry names the new errors.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
3 weeks ago
'Path,Account,AccessRights,AppliesTo'
( '"{0}",S-1-5-32-546,ReadData,ThisFolderOnly' -f $folders [ 0 ] )
( '"{0}",S-1-5-32-546,ReadData,' -f $folders [ 1 ] )
( '"{0}",S-1-5-32-546,ReadData,SubfoldersAndFilesOnly' -f $folders [ 2 ] )
docs(access,audit): say what an empty or null AppliesTo does, and test the Import-Csv route
The follow-up review of 9783d2a found that "an empty AppliesTo fails to
bind", in the changelog entry and in the four cmdlet pages, holds only for
$null. A blank cell that Import-Csv reads is an empty string, which the
binder cannot convert: PathSimple does not bind, and the row gets the
default scope, as an object without the property does. An AppliesTo that
is not the name of a scope is ignored the same way (probe
p-empty-appliesto.ps1, both editions, on the package of 9783d2a and on
the baseline). Only $null fails to bind, with
ParameterArgumentValidationErrorNullNotAllowed, for that object.
The pages, the help file, and the changelog entry now say so.
The tests of the CSV route read a CSV file with Import-Csv, the route
that the documentation shows, instead of ConvertFrom-Csv of strings,
which makes a trailing blank cell $null. They pin the blank cell as the
default scope and the $null object as a binding error for that object
only, with the other objects processed.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
3 weeks ago
)
fix(access,audit): keep the scope of a piped object from reaching the next one
Independent review of 93ed2c7: ProcessRecord of Add-NTFSAccess,
Remove-NTFSAccess, Add-NTFSAudit, and Remove-NTFSAudit wrote the scope
that it converts from AppliesTo into the fields behind InheritanceFlags
and PropagationFlags. PowerShell resets only the parameters that the
previous piped object bound, so the next object that had no scope, or
only one flag, took the scope of the one before it: AppliesTo
ThisFolderOnly, then none gave None/None twice, and Remove left the
InheritOnly part of every entry. Without the default set (93ed2c7) a
piped path object with AppliesTo reaches that code; for a piped
-SecurityDescriptor object it was a defect of rc7 already.
The flags of a record are locals now. The loop of the two Remove
cmdlets converted the scope a second time into the same fields; the
locals make that redundant, so it is gone.
The tests pipe objects with and without a scope, a CSV file, and
descriptor objects into the four cmdlets: red on the build of 93ed2c7
(8 of 87 cases of the two files, in every elevated configuration; 4 in
the basic-user ones, where the audit rows skip), green on this build.
An empty AppliesTo cell fails to bind for that row only; a test pins it.
The Remove-NTFSAudit PathSimple row cannot tell the sets apart (its
entry is on a file), and its comment says so. Docs/Examples.md no longer
says that a scope is needed so that PowerShell can choose the parameter
set (true before 5.0.0 only); the changelog entry names the new errors.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
3 weeks ago
fix(access): report a piped AccessType, AuditFlags, flags, or scope that isn't valid
Add-NTFSAccess, Remove-NTFSAccess, Add-NTFSAudit, and Remove-NTFSAudit
silently kept the default value of AccessType, AuditFlags,
InheritanceFlags, PropagationFlags, and AppliesTo when a piped object's
property could not be converted to the parameter's type: a misspelled
AccessType such as "Denyy", or a blank CSV cell, created an Allow entry
with no message. For a permissions tool, a typo for Deny that silently
grants access is a security-relevant defect (Decision 27).
The five parameters are now declared as object and converted inside
the cmdlet, the way the PowerShell binder converts a typed parameter
(names in any case, comma-separated flag lists, numbers, enum
instances). A value that cannot be converted, including a blank one,
is now an error named InvalidParameterValue: terminating and
catchable when it is named on the command line, exactly as the binder
reported it before; a non-terminating error targeting that path or
descriptor when it comes from a piped object, which leaves that item
unchanged and lets the pipeline continue with the next one. A $null
value is still not an error and keeps the parameter's default. Tab
completion of the five parameters keeps working through explicit
argument completers. Choosing between the Simple and Complex
parameter sets is unaffected and still rejects AppliesTo together
with InheritanceFlags or PropagationFlags as AmbiguousParameterSet,
for every mix of string and enum values.
The four cmdlet pages document the parameters as Type: Object, list
the valid values, and describe the new error; a sentence that said a
piped AppliesTo typo or blank cell was silently ignored is corrected.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
1 week ago
Import-Csv -LiteralPath $csv | Add-NTFSAccess -ErrorVariable addErrors -ErrorAction SilentlyContinue
docs(access,audit): say what an empty or null AppliesTo does, and test the Import-Csv route
The follow-up review of 9783d2a found that "an empty AppliesTo fails to
bind", in the changelog entry and in the four cmdlet pages, holds only for
$null. A blank cell that Import-Csv reads is an empty string, which the
binder cannot convert: PathSimple does not bind, and the row gets the
default scope, as an object without the property does. An AppliesTo that
is not the name of a scope is ignored the same way (probe
p-empty-appliesto.ps1, both editions, on the package of 9783d2a and on
the baseline). Only $null fails to bind, with
ParameterArgumentValidationErrorNullNotAllowed, for that object.
The pages, the help file, and the changelog entry now say so.
The tests of the CSV route read a CSV file with Import-Csv, the route
that the documentation shows, instead of ConvertFrom-Csv of strings,
which makes a trailing blank cell $null. They pin the blank cell as the
default scope and the $null object as a binding error for that object
only, with the other objects processed.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
3 weeks ago
fix(access): report a piped AccessType, AuditFlags, flags, or scope that isn't valid
Add-NTFSAccess, Remove-NTFSAccess, Add-NTFSAudit, and Remove-NTFSAudit
silently kept the default value of AccessType, AuditFlags,
InheritanceFlags, PropagationFlags, and AppliesTo when a piped object's
property could not be converted to the parameter's type: a misspelled
AccessType such as "Denyy", or a blank CSV cell, created an Allow entry
with no message. For a permissions tool, a typo for Deny that silently
grants access is a security-relevant defect (Decision 27).
The five parameters are now declared as object and converted inside
the cmdlet, the way the PowerShell binder converts a typed parameter
(names in any case, comma-separated flag lists, numbers, enum
instances). A value that cannot be converted, including a blank one,
is now an error named InvalidParameterValue: terminating and
catchable when it is named on the command line, exactly as the binder
reported it before; a non-terminating error targeting that path or
descriptor when it comes from a piped object, which leaves that item
unchanged and lets the pipeline continue with the next one. A $null
value is still not an error and keeps the parameter's default. Tab
completion of the five parameters keeps working through explicit
argument completers. Choosing between the Simple and Complex
parameter sets is unaffected and still rejects AppliesTo together
with InheritanceFlags or PropagationFlags as AmbiguousParameterSet,
for every mix of string and enum values.
The four cmdlet pages document the parameters as Type: Object, list
the valid values, and describe the new error; a sentence that said a
piped AppliesTo typo or blank cell was silently ignored is corrected.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
1 week ago
$addErrors | Should -HaveCount 1
$addErrors [ 0 ] . FullyQualifiedErrorId | Should -BeLike 'InvalidParameterValue,*'
$addErrors [ 0 ] . CategoryInfo . Category | Should -Be 'InvalidArgument'
$addErrors [ 0 ] . TargetObject | Should -Be $folders [ 1 ]
$addErrors [ 0 ] . Exception . Message | Should -Match 'AppliesTo'
docs(access,audit): say what an empty or null AppliesTo does, and test the Import-Csv route
The follow-up review of 9783d2a found that "an empty AppliesTo fails to
bind", in the changelog entry and in the four cmdlet pages, holds only for
$null. A blank cell that Import-Csv reads is an empty string, which the
binder cannot convert: PathSimple does not bind, and the row gets the
default scope, as an object without the property does. An AppliesTo that
is not the name of a scope is ignored the same way (probe
p-empty-appliesto.ps1, both editions, on the package of 9783d2a and on
the baseline). Only $null fails to bind, with
ParameterArgumentValidationErrorNullNotAllowed, for that object.
The pages, the help file, and the changelog entry now say so.
The tests of the CSV route read a CSV file with Import-Csv, the route
that the documentation shows, instead of ConvertFrom-Csv of strings,
which makes a trailing blank cell $null. They pin the blank cell as the
default scope and the $null object as a binding error for that object
only, with the other objects processed.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
3 weeks ago
$scopes = $folders | ForEach-Object -Process { Get-TestAccountScope -Acl ( Get-Acl -LiteralPath $_ ) -Account 'S-1-5-32-546' }
fix(access): report a piped AccessType, AuditFlags, flags, or scope that isn't valid
Add-NTFSAccess, Remove-NTFSAccess, Add-NTFSAudit, and Remove-NTFSAudit
silently kept the default value of AccessType, AuditFlags,
InheritanceFlags, PropagationFlags, and AppliesTo when a piped object's
property could not be converted to the parameter's type: a misspelled
AccessType such as "Denyy", or a blank CSV cell, created an Allow entry
with no message. For a permissions tool, a typo for Deny that silently
grants access is a security-relevant defect (Decision 27).
The five parameters are now declared as object and converted inside
the cmdlet, the way the PowerShell binder converts a typed parameter
(names in any case, comma-separated flag lists, numbers, enum
instances). A value that cannot be converted, including a blank one,
is now an error named InvalidParameterValue: terminating and
catchable when it is named on the command line, exactly as the binder
reported it before; a non-terminating error targeting that path or
descriptor when it comes from a piped object, which leaves that item
unchanged and lets the pipeline continue with the next one. A $null
value is still not an error and keeps the parameter's default. Tab
completion of the five parameters keeps working through explicit
argument completers. Choosing between the Simple and Complex
parameter sets is unaffected and still rejects AppliesTo together
with InheritanceFlags or PropagationFlags as AmbiguousParameterSet,
for every mix of string and enum values.
The four cmdlet pages document the parameters as Type: Object, list
the valid values, and describe the new error; a sentence that said a
piped AppliesTo typo or blank cell was silently ignored is corrected.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
1 week ago
$scopes -join '|' | Should -BeExactly 'None/None||ContainerInherit, ObjectInherit/InheritOnly'
docs(access,audit): say what an empty or null AppliesTo does, and test the Import-Csv route
The follow-up review of 9783d2a found that "an empty AppliesTo fails to
bind", in the changelog entry and in the four cmdlet pages, holds only for
$null. A blank cell that Import-Csv reads is an empty string, which the
binder cannot convert: PathSimple does not bind, and the row gets the
default scope, as an object without the property does. An AppliesTo that
is not the name of a scope is ignored the same way (probe
p-empty-appliesto.ps1, both editions, on the package of 9783d2a and on
the baseline). Only $null fails to bind, with
ParameterArgumentValidationErrorNullNotAllowed, for that object.
The pages, the help file, and the changelog entry now say so.
The tests of the CSV route read a CSV file with Import-Csv, the route
that the documentation shows, instead of ConvertFrom-Csv of strings,
which makes a trailing blank cell $null. They pin the blank cell as the
default scope and the $null object as a binding error for that object
only, with the other objects processed.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
3 weeks ago
}
# $null cannot be bound at all (ConvertFrom-Csv makes it of a trailing blank cell): the object fails, loudly, and the cmdlet goes
# on with the next one.
It 'Should report a binding error for the piped object whose AppliesTo is $null, leave its folder alone, and still process the other objects' {
$folders = @ ( 'AddNullFirst' , 'AddNullMiddle' , 'AddNullLast' |
ForEach-Object -Process { New-TestSandboxItem -Sandbox $sandbox -Name $_ -Directory } )
$entry = @ { Account = 'S-1-5-32-546' ; AccessRights = 'ReadData' }
@ (
[pscustomobject] ( $entry + @ { Path = $folders [ 0 ] ; AppliesTo = 'ThisFolderOnly' } )
[pscustomobject] ( $entry + @ { Path = $folders [ 1 ] ; AppliesTo = $null } )
[pscustomobject] ( $entry + @ { Path = $folders [ 2 ] ; AppliesTo = 'SubfoldersAndFilesOnly' } )
) | Add-NTFSAccess -ErrorVariable addErrors -ErrorAction SilentlyContinue
fix(access,audit): keep the scope of a piped object from reaching the next one
Independent review of 93ed2c7: ProcessRecord of Add-NTFSAccess,
Remove-NTFSAccess, Add-NTFSAudit, and Remove-NTFSAudit wrote the scope
that it converts from AppliesTo into the fields behind InheritanceFlags
and PropagationFlags. PowerShell resets only the parameters that the
previous piped object bound, so the next object that had no scope, or
only one flag, took the scope of the one before it: AppliesTo
ThisFolderOnly, then none gave None/None twice, and Remove left the
InheritOnly part of every entry. Without the default set (93ed2c7) a
piped path object with AppliesTo reaches that code; for a piped
-SecurityDescriptor object it was a defect of rc7 already.
The flags of a record are locals now. The loop of the two Remove
cmdlets converted the scope a second time into the same fields; the
locals make that redundant, so it is gone.
The tests pipe objects with and without a scope, a CSV file, and
descriptor objects into the four cmdlets: red on the build of 93ed2c7
(8 of 87 cases of the two files, in every elevated configuration; 4 in
the basic-user ones, where the audit rows skip), green on this build.
An empty AppliesTo cell fails to bind for that row only; a test pins it.
The Remove-NTFSAudit PathSimple row cannot tell the sets apart (its
entry is on a file), and its comment says so. Docs/Examples.md no longer
says that a scope is needed so that PowerShell can choose the parameter
set (true before 5.0.0 only); the changelog entry names the new errors.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
3 weeks ago
@ ( $addErrors ) | Should -HaveCount 1
docs(access,audit): say what an empty or null AppliesTo does, and test the Import-Csv route
The follow-up review of 9783d2a found that "an empty AppliesTo fails to
bind", in the changelog entry and in the four cmdlet pages, holds only for
$null. A blank cell that Import-Csv reads is an empty string, which the
binder cannot convert: PathSimple does not bind, and the row gets the
default scope, as an object without the property does. An AppliesTo that
is not the name of a scope is ignored the same way (probe
p-empty-appliesto.ps1, both editions, on the package of 9783d2a and on
the baseline). Only $null fails to bind, with
ParameterArgumentValidationErrorNullNotAllowed, for that object.
The pages, the help file, and the changelog entry now say so.
The tests of the CSV route read a CSV file with Import-Csv, the route
that the documentation shows, instead of ConvertFrom-Csv of strings,
which makes a trailing blank cell $null. They pin the blank cell as the
default scope and the $null object as a binding error for that object
only, with the other objects processed.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
3 weeks ago
$addErrors [ 0 ] . FullyQualifiedErrorId | Should -BeLike 'ParameterArgumentValidationErrorNullNotAllowed,*'
fix(access,audit): keep the scope of a piped object from reaching the next one
Independent review of 93ed2c7: ProcessRecord of Add-NTFSAccess,
Remove-NTFSAccess, Add-NTFSAudit, and Remove-NTFSAudit wrote the scope
that it converts from AppliesTo into the fields behind InheritanceFlags
and PropagationFlags. PowerShell resets only the parameters that the
previous piped object bound, so the next object that had no scope, or
only one flag, took the scope of the one before it: AppliesTo
ThisFolderOnly, then none gave None/None twice, and Remove left the
InheritOnly part of every entry. Without the default set (93ed2c7) a
piped path object with AppliesTo reaches that code; for a piped
-SecurityDescriptor object it was a defect of rc7 already.
The flags of a record are locals now. The loop of the two Remove
cmdlets converted the scope a second time into the same fields; the
locals make that redundant, so it is gone.
The tests pipe objects with and without a scope, a CSV file, and
descriptor objects into the four cmdlets: red on the build of 93ed2c7
(8 of 87 cases of the two files, in every elevated configuration; 4 in
the basic-user ones, where the audit rows skip), green on this build.
An empty AppliesTo cell fails to bind for that row only; a test pins it.
The Remove-NTFSAudit PathSimple row cannot tell the sets apart (its
entry is on a file), and its comment says so. Docs/Examples.md no longer
says that a scope is needed so that PowerShell can choose the parameter
set (true before 5.0.0 only); the changelog entry names the new errors.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
3 weeks ago
$scopes = $folders | ForEach-Object -Process { Get-TestAccountScope -Acl ( Get-Acl -LiteralPath $_ ) -Account 'S-1-5-32-546' }
$scopes -join '|' | Should -BeExactly 'None/None||ContainerInherit, ObjectInherit/InheritOnly'
}
It 'Should give each piped descriptor object only its own scope: AppliesTo, then none' {
$folders = @ ( 'AddMixedSDThisFolderOnly' , 'AddMixedSDNoScope' |
ForEach-Object -Process { New-TestSandboxItem -Sandbox $sandbox -Name $_ -Directory } )
$descriptors = @ ( Get-NTFSSecurityDescriptor -Path $folders -ErrorAction Stop )
$entry = @ { Account = 'S-1-5-32-546' ; AccessRights = 'ReadData' }
@ (
[pscustomobject] ( $entry + @ { SecurityDescriptor = $descriptors [ 0 ] ; AppliesTo = 'ThisFolderOnly' } )
[pscustomobject] ( $entry + @ { SecurityDescriptor = $descriptors [ 1 ] } )
) | Add-NTFSAccess -ErrorAction Stop
$scopes = $descriptors | ForEach-Object -Process { Get-TestAccountScope -Acl $_ . SecurityDescriptor -Account 'S-1-5-32-546' }
$scopes -join '|' | Should -BeExactly 'None/None|ContainerInherit, ObjectInherit/None'
}
}
test(access): assert the parameter sets that the judged matrix found untested
Adds Tests/ParameterSets.Access.Tests.ps1, closing the TestNeeded rows of
paramsets/judged-access.csv for Add-NTFSAccess, Remove-NTFSAccess,
Clear-NTFSAccess, Get-NTFSAccess, Get-NTFSOrphanedAccess,
Get-NTFSSimpleAccess, and Get-NTFSEffectiveAccess: 20 new It blocks (26
executed cases) covering pipeline binding (ValueFromPipeline and
ValueFromPipelineByPropertyName, including positional file/folder objects),
-PassThru combined with one denied and one succeeding path, error
ErrorId/category/target for read and write failures, continuation after a
per-item error, state read back with .NET (not -PassThru/Get-NTFSAccess),
SecurityDescriptor-set pipelines and memory-vs-disk behavior, and the
declared output types and PathSimple/SDSimple vs PathComplex/SDComplex
parameter-set boundaries. No row with GapKind=MaintainerDecision is pinned:
the -SecurityDescriptor sets of Add/Remove keep their open per-item error
behavior untested either way.
Finding: a plain piped object with an AppliesTo property does not steer
Add-NTFSAccess/Remove-NTFSAccess to the PathSimple/SDSimple parameter set by
itself; PowerShell resolves the ambiguity to the default set (PathComplex/
SDComplex) and silently drops the unmatched property. The proposed test for
that exact shape (notes section (b), item 2c) could not be written as
specified; the pipeline test for PathSimple instead binds -Path through the
pipeline while -AppliesTo is named explicitly, which does select the set.
Validated in all four configurations (elevated/basic user, Windows
PowerShell 5.1/PowerShell 7): 26 passed, 0 failed, 0 skipped in each. Eight
one-statement production mutations across all seven cmdlets were applied in
an isolated worktree, built, and run against this file; each was detected by
at least one new test, then the source was restored exactly and rebuilt.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
3 weeks ago
Context 'PathSimple parameter set' {
It 'Should report a missing path''s error by category and target, still scope the next path''s entry, and keep binding -Path through the pipeline once -AppliesTo selects the set' {
$missing = Join-Path -Path $sandbox -ChildPath ( 'AddSimpleMissing-{0}' -f [guid] :: NewGuid ( ) . ToString ( 'N' ) . Substring ( 0 , 8 ) )
Assert-TestSandboxPath -Sandbox $sandbox -Path $missing
$file = New-TestSandboxItem -Sandbox $sandbox -Name 'AddSimpleNext'
Add-NTFSAccess -Path $missing , $file -Account 'S-1-1-0' -AccessRights ReadData -AppliesTo ThisFolderOnly -ErrorVariable addErrors -ErrorAction SilentlyContinue
$addErrors | Should -HaveCount 1
$addErrors [ 0 ] . FullyQualifiedErrorId | Should -BeLike 'ReadFileError,*'
$addErrors [ 0 ] . CategoryInfo . Category | Should -Be 'OpenError'
$addErrors [ 0 ] . TargetObject | Should -Be $missing
$fileRule = @ ( ( Get-Acl -LiteralPath $file ) . GetAccessRules ( $true , $false , $sidType ) | Where-Object -FilterScript { $_ . IdentityReference . Value -eq 'S-1-1-0' } )
$fileRule | Should -HaveCount 1
fix(access,audit): bind the AppliesTo property of a piped object for paths
Add-NTFSAccess, Remove-NTFSAccess, Add-NTFSAudit, and Remove-NTFSAudit
declared PathComplex as the default parameter set. An object that had
Path, Account, and AccessRights satisfied the mandatory parameters of
the default set, so PowerShell never tried PathSimple and the cmdlet
dropped the AppliesTo property of the object. Since 4b1abc9 made
-AppliesTo mandatory in the Simple sets, the default set is redundant
for every call that names its parameters, and it masked PathSimple for
the pipeline.
Without a default set, an object that has AppliesTo binds PathSimple,
one that has InheritanceFlags or PropagationFlags binds PathComplex, and
one that has neither binds PathComplex with the default scope. An
object that has AppliesTo and InheritanceFlags now fails with
AmbiguousParameterSet, like the same two named parameters. Named calls,
-SecurityDescriptor calls without AppliesTo, and the round trip
Get-NTFSAccess | Add-NTFSAccess keep their behavior.
The tests pipe objects of each shape and fail without the change. The
cmdlet pages and the help file follow the build.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
3 weeks ago
# -AppliesTo named explicitly selects PathSimple while -Path arrives through the pipeline.
test(access): assert the parameter sets that the judged matrix found untested
Adds Tests/ParameterSets.Access.Tests.ps1, closing the TestNeeded rows of
paramsets/judged-access.csv for Add-NTFSAccess, Remove-NTFSAccess,
Clear-NTFSAccess, Get-NTFSAccess, Get-NTFSOrphanedAccess,
Get-NTFSSimpleAccess, and Get-NTFSEffectiveAccess: 20 new It blocks (26
executed cases) covering pipeline binding (ValueFromPipeline and
ValueFromPipelineByPropertyName, including positional file/folder objects),
-PassThru combined with one denied and one succeeding path, error
ErrorId/category/target for read and write failures, continuation after a
per-item error, state read back with .NET (not -PassThru/Get-NTFSAccess),
SecurityDescriptor-set pipelines and memory-vs-disk behavior, and the
declared output types and PathSimple/SDSimple vs PathComplex/SDComplex
parameter-set boundaries. No row with GapKind=MaintainerDecision is pinned:
the -SecurityDescriptor sets of Add/Remove keep their open per-item error
behavior untested either way.
Finding: a plain piped object with an AppliesTo property does not steer
Add-NTFSAccess/Remove-NTFSAccess to the PathSimple/SDSimple parameter set by
itself; PowerShell resolves the ambiguity to the default set (PathComplex/
SDComplex) and silently drops the unmatched property. The proposed test for
that exact shape (notes section (b), item 2c) could not be written as
specified; the pipeline test for PathSimple instead binds -Path through the
pipeline while -AppliesTo is named explicitly, which does select the set.
Validated in all four configurations (elevated/basic user, Windows
PowerShell 5.1/PowerShell 7): 26 passed, 0 failed, 0 skipped in each. Eight
one-statement production mutations across all seven cmdlets were applied in
an isolated worktree, built, and run against this file; each was detected by
at least one new test, then the source was restored exactly and rebuilt.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
3 weeks ago
$folder = New-TestSandboxItem -Sandbox $sandbox -Name 'AddSimplePipedFolder' -Directory
Get-Item2 -Path $folder | Add-NTFSAccess -Account 'S-1-5-32-546' -AccessRights ReadData -AppliesTo ThisFolderAndSubfolders -ErrorAction Stop
$guestsRule = @ ( ( Get-Acl -LiteralPath $folder ) . GetAccessRules ( $true , $false , $sidType ) | Where-Object -FilterScript { $_ . IdentityReference . Value -eq 'S-1-5-32-546' } )
$guestsRule | Should -HaveCount 1
$guestsRule [ 0 ] . InheritanceFlags | Should -Be ( [System.Security.AccessControl.InheritanceFlags] :: ContainerInherit )
$guestsRule [ 0 ] . PropagationFlags | Should -Be ( [System.Security.AccessControl.PropagationFlags] :: None )
$guestsRule [ 0 ] . FileSystemRights . HasFlag ( [System.Security.AccessControl.FileSystemRights] :: ReadData ) | Should -BeTrue
}
}
test(access): close budget parameter-set gaps for the access cmdlets
Add 20 new It blocks (32 executed cases once the 13-scope -ForEach of
Add-NTFSAccess/SDSimple expands) to ParameterSets.Access.Tests.ps1, closing
every REMAINING item that worklist-access.md marks budget for rows 1-13:
- Add-NTFSAccess: several accounts and positional binding (PathComplex); a
Deny entry's exact rights with no auto-added Synchronize (PathComplex and
PathSimple); a denied write and a Deny entry without rights through
PathSimple; all 13 -AppliesTo scopes of a security descriptor read back
with .NET, piped and several (SDSimple); piped and several descriptors
asserted for Add itself, not only as a fixture (SDComplex).
- Remove-NTFSAccess: the -PassThru output's count against the complete
survivor list for several piped paths; several accounts and positional
binding (PathComplex); a Deny entry's rights-reduction without
-RemoveSpecific; a missing and a denied path, piped, several (PathSimple);
rights-reduction without -RemoveSpecific and disk independence until
Set-NTFSSecurityDescriptor, piped and several descriptors (SDSimple); the
on-disk write-through only after Set-NTFSSecurityDescriptor, piped and
several descriptors (SDComplex).
- Get-NTFSAccess: what the cmdlet returns without -Path, asserted on an
actual emitted object's type (Path).
- Get-NTFSOrphanedAccess: the category of ReadSecurityError after a failed
ownership retry (Path); -ExcludeInherited and an entry added to a
descriptor only in memory (SD).
- Get-NTFSSimpleAccess: the category and target of a missing path's
ReadError (Path).
- Get-NTFSEffectiveAccess: one call combining a failing and a succeeding
path (Path).
Items the REASON marks MaintainerDecision, Limit, or would-pin are left
untouched, per the group notes of the brief. No product code changed.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
2 weeks ago
Context 'PathComplex parameter set, additional state assertions' {
It 'Should add an explicit allow entry for each of several accounts bound positionally, with the default scope' {
$folder = New-TestSandboxItem -Sandbox $sandbox -Name 'AddPositionalSeveral' -Directory
Add-NTFSAccess $folder @ ( 'S-1-1-0' , 'S-1-5-32-546' ) 'ReadData' -ErrorAction Stop
foreach ( $account in 'S-1-1-0' , 'S-1-5-32-546' ) {
$rule = @ ( ( Get-Acl -LiteralPath $folder ) . GetAccessRules ( $true , $false , $sidType ) |
Where-Object -FilterScript { $_ . IdentityReference . Value -eq $account } )
$rule | Should -HaveCount 1
$rule [ 0 ] . AccessControlType | Should -Be ( [System.Security.AccessControl.AccessControlType] :: Allow )
$rule [ 0 ] . FileSystemRights . HasFlag ( [System.Security.AccessControl.FileSystemRights] :: ReadData ) | Should -BeTrue
$rule [ 0 ] . InheritanceFlags | Should -Be ( [System.Security.AccessControl.InheritanceFlags] 'ContainerInherit, ObjectInherit' )
$rule [ 0 ] . PropagationFlags | Should -Be ( [System.Security.AccessControl.PropagationFlags] :: None )
}
}
# Only an Allow entry gets Synchronize added (the page); a Deny entry written through -Path must carry
# exactly the requested rights.
It 'Should write a Deny entry through -Path with exactly the requested rights and no Synchronize' {
$file = New-TestSandboxItem -Sandbox $sandbox -Name 'AddDenySynchronize'
Add-NTFSAccess -Path $file -Account 'S-1-1-0' -AccessRights ReadData -AccessType Deny -ErrorAction Stop
$rule = @ ( ( Get-Acl -LiteralPath $file ) . GetAccessRules ( $true , $false , $sidType ) |
Where-Object -FilterScript { $_ . IdentityReference . Value -eq 'S-1-1-0' } )
$rule | Should -HaveCount 1
$rule [ 0 ] . AccessControlType | Should -Be ( [System.Security.AccessControl.AccessControlType] :: Deny )
$rule [ 0 ] . FileSystemRights | Should -Be ( [System.Security.AccessControl.FileSystemRights] :: ReadData )
}
}
Context 'PathSimple parameter set, Deny entries and a denied write' {
BeforeAll {
# So that an elevated session meets the same denial as a basic user (see PathErrors.Tests.ps1).
$privateData [ 'EnablePrivileges' ] = $false
}
AfterAll {
$privateData [ 'EnablePrivileges' ] = $enablePrivileges
}
It 'Should report AddAceError by category and target for a denied write scoped by -AppliesTo, and leave the item unchanged' {
$blocked = New-TestSandboxItem -Sandbox $sandbox -Name 'AddSimpleBlocked'
Block-TestWritePermission -Sandbox $sandbox -Path $blocked
$before = ( Get-Acl -LiteralPath $blocked ) . Sddl
Add-NTFSAccess -Path $blocked -Account 'S-1-1-0' -AccessRights ReadData -AppliesTo ThisFolderOnly -ErrorVariable addErrors -ErrorAction SilentlyContinue
$addErrors | Should -HaveCount 1
$addErrors [ 0 ] . FullyQualifiedErrorId | Should -BeLike 'AddAceError,*'
$addErrors [ 0 ] . CategoryInfo . Category | Should -Be 'WriteError'
$addErrors [ 0 ] . TargetObject | Should -Be $blocked
( Get-Acl -LiteralPath $blocked ) . Sddl | Should -BeExactly $before
}
# .NET refuses to build a deny entry without rights for this parameter set too; the write-denial code path
# above is shared with PathComplex (Access.Tests.ps1:740), so only the categorization needs its own case here.
It 'Should write an AddAceError for a Deny entry without rights through the PathSimple set, and change nothing' {
$file = New-TestSandboxItem -Sandbox $sandbox -Name 'AddSimpleDenyNone'
$before = ( Get-Acl -LiteralPath $file ) . Sddl
$result = @ ( Add-NTFSAccess -Path $file -Account 'S-1-1-0' -AccessRights None -AccessType Deny -AppliesTo ThisFolderOnly -PassThru -ErrorVariable addErrors -ErrorAction SilentlyContinue )
$result | Should -BeNullOrEmpty
$addErrors | Should -HaveCount 1
$addErrors [ 0 ] . FullyQualifiedErrorId | Should -BeLike 'AddAceError,*'
$addErrors [ 0 ] . CategoryInfo . Category | Should -Be 'WriteError'
( Get-Acl -LiteralPath $file ) . Sddl | Should -BeExactly $before
}
It 'Should write a Deny entry scoped by -AppliesTo with exactly the requested rights and no Synchronize' {
$folder = New-TestSandboxItem -Sandbox $sandbox -Name 'AddSimpleDenyScoped' -Directory
Add-NTFSAccess -Path $folder -Account 'S-1-1-0' -AccessRights ReadData -AccessType Deny -AppliesTo ThisFolderAndSubfolders -ErrorAction Stop
$rule = @ ( ( Get-Acl -LiteralPath $folder ) . GetAccessRules ( $true , $false , $sidType ) |
Where-Object -FilterScript { $_ . IdentityReference . Value -eq 'S-1-1-0' } )
$rule | Should -HaveCount 1
$rule [ 0 ] . AccessControlType | Should -Be ( [System.Security.AccessControl.AccessControlType] :: Deny )
$rule [ 0 ] . FileSystemRights | Should -Be ( [System.Security.AccessControl.FileSystemRights] :: ReadData )
$rule [ 0 ] . InheritanceFlags | Should -Be ( [System.Security.AccessControl.InheritanceFlags] :: ContainerInherit )
$rule [ 0 ] . PropagationFlags | Should -Be ( [System.Security.AccessControl.PropagationFlags] :: None )
}
}
Context 'SDSimple parameter set' {
It 'Should add an entry with the scope <Name> to each of several descriptors bound through the pipeline, read back with .NET, and leave the items unchanged on disk' -ForEach $sdScopeCases {
$first = New-TestSandboxItem -Sandbox $sandbox -Name 'AddSdScopeFirst' -Directory
$second = New-TestSandboxItem -Sandbox $sandbox -Name 'AddSdScopeSecond' -Directory
$before = foreach ( $folder in $first , $second ) { ( Get-Acl -LiteralPath $folder ) . GetSecurityDescriptorSddlForm ( 'Access' ) }
$descriptors = @ ( Get-NTFSSecurityDescriptor -Path $first , $second -ErrorAction Stop )
$descriptors | Add-NTFSAccess -Account 'S-1-5-32-546' -AccessRights ReadData -AppliesTo $Name -ErrorAction Stop
for ( $index = 0 ; $index -lt 2 ; $index + + ) {
$rule = @ ( $descriptors [ $index ] . SecurityDescriptor . GetAccessRules ( $true , $false , $sidType ) |
Where-Object -FilterScript { $_ . IdentityReference . Value -eq 'S-1-5-32-546' } )
$rule | Should -HaveCount 1
$rule [ 0 ] . AccessControlType | Should -Be ( [System.Security.AccessControl.AccessControlType] :: Allow )
$rule [ 0 ] . InheritanceFlags | Should -Be ( [System.Security.AccessControl.InheritanceFlags] $Inheritance )
$rule [ 0 ] . PropagationFlags | Should -Be ( [System.Security.AccessControl.PropagationFlags] $Propagation )
}
( Get-Acl -LiteralPath $first ) . GetSecurityDescriptorSddlForm ( 'Access' ) | Should -BeExactly $before [ 0 ]
( Get-Acl -LiteralPath $second ) . GetSecurityDescriptorSddlForm ( 'Access' ) | Should -BeExactly $before [ 1 ]
}
}
Context 'SDComplex parameter set with several descriptors' {
It 'Should add the entry to each of several descriptors bound through the pipeline, read back with .NET, and leave the items unchanged on disk' {
$first = New-TestSandboxItem -Sandbox $sandbox -Name 'AddSdComplexPipeFirst'
$second = New-TestSandboxItem -Sandbox $sandbox -Name 'AddSdComplexPipeSecond'
$before = foreach ( $file in $first , $second ) { ( Get-Acl -LiteralPath $file ) . Sddl }
$descriptors = @ ( Get-NTFSSecurityDescriptor -Path $first , $second -ErrorAction Stop )
$descriptors | Add-NTFSAccess -Account 'S-1-5-21-1-2-3-9501' -AccessRights ReadData , WriteData -InheritanceFlags None -PropagationFlags None -ErrorAction Stop
for ( $index = 0 ; $index -lt 2 ; $index + + ) {
$rule = @ ( $descriptors [ $index ] . SecurityDescriptor . GetAccessRules ( $true , $false , $sidType ) |
Where-Object -FilterScript { $_ . IdentityReference . Value -eq 'S-1-5-21-1-2-3-9501' } )
$rule | Should -HaveCount 1
$rule [ 0 ] . FileSystemRights . HasFlag ( [System.Security.AccessControl.FileSystemRights] :: ReadData ) | Should -BeTrue
$rule [ 0 ] . FileSystemRights . HasFlag ( [System.Security.AccessControl.FileSystemRights] :: WriteData ) | Should -BeTrue
}
( Get-Acl -LiteralPath $first ) . Sddl | Should -BeExactly $before [ 0 ]
( Get-Acl -LiteralPath $second ) . Sddl | Should -BeExactly $before [ 1 ]
}
}
fix(access): report a failing change of a descriptor as an error per descriptor
In the four -SecurityDescriptor parameter sets of Add-NTFSAccess,
Remove-NTFSAccess, Add-NTFSAudit, and Remove-NTFSAudit, a change that
throws, such as a Deny entry without rights, ended the cmdlet with a
terminating error, while the -Path parameter sets already wrote an
AddAceError or RemoveAceError for the failing item and went on to the
next one. The two kinds of parameter sets now behave the same way.
Each descriptor's change is wrapped in its own try/catch: a failure
writes a non-terminating AddAceError or RemoveAceError, category
WriteError, that targets the descriptor, and the cmdlet continues
with the next descriptor in the pipeline instead of stopping. -PassThru
stays outside the try, so what a later command raises when the
cmdlet writes its output is never caught there. The existing
TestAuditSection check of the audit cmdlets, and everything else
about a successful change, is unchanged.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
1 week ago
# Item 5 of Decision 27: a failing change to one of several descriptors (a Deny entry without rights, which .NET
# refuses to build, the same as Access.Tests.ps1:774 pins for -Path) reports an error for that descriptor and goes
# on with the next one, instead of ending the cmdlet with a terminating error.
Context 'A failing change to one of several descriptors' {
It 'Should write an AddAceError for the descriptor whose change fails in the SDSimple set, continue with the next, and return -PassThru only for the one that succeeded' {
$first = New-TestSandboxItem -Sandbox $sandbox -Name 'SdErrorSimpleFirst'
$second = New-TestSandboxItem -Sandbox $sandbox -Name 'SdErrorSimpleSecond'
$descriptors = @ ( Get-NTFSSecurityDescriptor -Path $first , $second -ErrorAction Stop )
$before = $descriptors [ 0 ] . SecurityDescriptor . GetSecurityDescriptorSddlForm ( 'Access' )
$rows = @ (
[pscustomobject] @ { SecurityDescriptor = $descriptors [ 0 ] ; Account = 'S-1-1-0' ; AccessRights = 'None' ; AccessType = 'Deny' ; AppliesTo = 'ThisFolderOnly' }
[pscustomobject] @ { SecurityDescriptor = $descriptors [ 1 ] ; Account = 'S-1-1-0' ; AccessRights = 'ReadData' ; AccessType = 'Deny' ; AppliesTo = 'ThisFolderOnly' }
)
$result = @ ( $rows | Add-NTFSAccess -PassThru -ErrorVariable sdErrors -ErrorAction SilentlyContinue )
$sdErrors | Should -HaveCount 1
$sdErrors [ 0 ] . FullyQualifiedErrorId | Should -BeLike 'AddAceError,*'
$sdErrors [ 0 ] . CategoryInfo . Category | Should -Be 'WriteError'
$sdErrors [ 0 ] . TargetObject | Should -Be $descriptors [ 0 ]
$descriptors [ 0 ] . SecurityDescriptor . GetSecurityDescriptorSddlForm ( 'Access' ) | Should -BeExactly $before
$result | Should -Not -BeNullOrEmpty
$result | ForEach-Object -Process { $_ . FullName | Should -Be $second }
@ ( $result | Where-Object -FilterScript { $_ . Account . Sid -eq 'S-1-1-0' -and $_ . AccessControlType -eq 'Deny' } ) | Should -HaveCount 1
}
It 'Should write an AddAceError for the descriptor whose change fails in the SDComplex set, continue with the next, and return -PassThru only for the one that succeeded' {
$first = New-TestSandboxItem -Sandbox $sandbox -Name 'SdErrorComplexFirst'
$second = New-TestSandboxItem -Sandbox $sandbox -Name 'SdErrorComplexSecond'
$descriptors = @ ( Get-NTFSSecurityDescriptor -Path $first , $second -ErrorAction Stop )
$before = $descriptors [ 0 ] . SecurityDescriptor . GetSecurityDescriptorSddlForm ( 'Access' )
$rows = @ (
[pscustomobject] @ { SecurityDescriptor = $descriptors [ 0 ] ; Account = 'S-1-1-0' ; AccessRights = 'None' ; AccessType = 'Deny' }
[pscustomobject] @ { SecurityDescriptor = $descriptors [ 1 ] ; Account = 'S-1-1-0' ; AccessRights = 'ReadData' ; AccessType = 'Deny' }
)
$result = @ ( $rows | Add-NTFSAccess -PassThru -ErrorVariable sdErrors -ErrorAction SilentlyContinue )
$sdErrors | Should -HaveCount 1
$sdErrors [ 0 ] . FullyQualifiedErrorId | Should -BeLike 'AddAceError,*'
$sdErrors [ 0 ] . CategoryInfo . Category | Should -Be 'WriteError'
$sdErrors [ 0 ] . TargetObject | Should -Be $descriptors [ 0 ]
$descriptors [ 0 ] . SecurityDescriptor . GetSecurityDescriptorSddlForm ( 'Access' ) | Should -BeExactly $before
$result | Should -Not -BeNullOrEmpty
$result | ForEach-Object -Process { $_ . FullName | Should -Be $second }
@ ( $result | Where-Object -FilterScript { $_ . Account . Sid -eq 'S-1-1-0' -and $_ . AccessControlType -eq 'Deny' } ) | Should -HaveCount 1
}
}
test(access): assert the parameter sets that the judged matrix found untested
Adds Tests/ParameterSets.Access.Tests.ps1, closing the TestNeeded rows of
paramsets/judged-access.csv for Add-NTFSAccess, Remove-NTFSAccess,
Clear-NTFSAccess, Get-NTFSAccess, Get-NTFSOrphanedAccess,
Get-NTFSSimpleAccess, and Get-NTFSEffectiveAccess: 20 new It blocks (26
executed cases) covering pipeline binding (ValueFromPipeline and
ValueFromPipelineByPropertyName, including positional file/folder objects),
-PassThru combined with one denied and one succeeding path, error
ErrorId/category/target for read and write failures, continuation after a
per-item error, state read back with .NET (not -PassThru/Get-NTFSAccess),
SecurityDescriptor-set pipelines and memory-vs-disk behavior, and the
declared output types and PathSimple/SDSimple vs PathComplex/SDComplex
parameter-set boundaries. No row with GapKind=MaintainerDecision is pinned:
the -SecurityDescriptor sets of Add/Remove keep their open per-item error
behavior untested either way.
Finding: a plain piped object with an AppliesTo property does not steer
Add-NTFSAccess/Remove-NTFSAccess to the PathSimple/SDSimple parameter set by
itself; PowerShell resolves the ambiguity to the default set (PathComplex/
SDComplex) and silently drops the unmatched property. The proposed test for
that exact shape (notes section (b), item 2c) could not be written as
specified; the pipeline test for PathSimple instead binds -Path through the
pipeline while -AppliesTo is named explicitly, which does select the set.
Validated in all four configurations (elevated/basic user, Windows
PowerShell 5.1/PowerShell 7): 26 passed, 0 failed, 0 skipped in each. Eight
one-statement production mutations across all seven cmdlets were applied in
an isolated worktree, built, and run against this file; each was detected by
at least one new test, then the source was restored exactly and rebuilt.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
3 weeks ago
}
Describe 'Remove-NTFSAccess' {
Context 'PathComplex parameter set with one denied path and -PassThru' {
BeforeAll {
$privateData [ 'EnablePrivileges' ] = $false
}
AfterAll {
$privateData [ 'EnablePrivileges' ] = $enablePrivileges
}
It 'Should write one RemoveAceError for the blocked path, leave it unchanged, and return -PassThru entries only for the path it changed' {
$blocked = New-TestSandboxItem -Sandbox $sandbox -Name 'RemovePassThruBlocked'
$next = New-TestSandboxItem -Sandbox $sandbox -Name 'RemovePassThruNext'
Add-NTFSAccess -Path $blocked , $next -Account 'S-1-1-0' -AccessRights ReadData -ErrorAction Stop
Block-TestWritePermission -Sandbox $sandbox -Path $blocked
$before = ( Get-Acl -LiteralPath $blocked ) . Sddl
$result = @ ( Remove-NTFSAccess -Path $blocked , $next -Account 'S-1-1-0' -AccessRights ReadData -PassThru -ErrorVariable removeErrors -ErrorAction SilentlyContinue )
$removeErrors | Should -HaveCount 1
$removeErrors [ 0 ] . FullyQualifiedErrorId | Should -BeLike 'RemoveAceError,*'
$removeErrors [ 0 ] . CategoryInfo . Category | Should -Be 'WriteError'
$removeErrors [ 0 ] . TargetObject | Should -Be $blocked
( Get-Acl -LiteralPath $blocked ) . Sddl | Should -BeExactly $before
$result | Should -Not -BeNullOrEmpty
$result | ForEach-Object -Process {
$_ | Should -BeOfType ( [Security2.FileSystemAccessRule2] )
$_ . FullName | Should -Be $next
}
@ ( $result | Where-Object -FilterScript { $_ . Account . Sid -eq 'S-1-1-0' } ) | Should -BeNullOrEmpty
}
}
fix(access,audit): bind the AppliesTo property of a piped object for paths
Add-NTFSAccess, Remove-NTFSAccess, Add-NTFSAudit, and Remove-NTFSAudit
declared PathComplex as the default parameter set. An object that had
Path, Account, and AccessRights satisfied the mandatory parameters of
the default set, so PowerShell never tried PathSimple and the cmdlet
dropped the AppliesTo property of the object. Since 4b1abc9 made
-AppliesTo mandatory in the Simple sets, the default set is redundant
for every call that names its parameters, and it masked PathSimple for
the pipeline.
Without a default set, an object that has AppliesTo binds PathSimple,
one that has InheritanceFlags or PropagationFlags binds PathComplex, and
one that has neither binds PathComplex with the default scope. An
object that has AppliesTo and InheritanceFlags now fails with
AmbiguousParameterSet, like the same two named parameters. Named calls,
-SecurityDescriptor calls without AppliesTo, and the round trip
Get-NTFSAccess | Add-NTFSAccess keep their behavior.
The tests pipe objects of each shape and fail without the change. The
cmdlet pages and the help file follow the build.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
3 weeks ago
# As for Add-NTFSAccess, the property AppliesTo of the piped object selects PathSimple, and the entry that the cmdlet
# addresses is the one of the scope it names: the rest of an entry of a wider scope stays.
Context 'Choosing the parameter set of an object bound through the pipeline by property name' {
It 'Should leave <Meaning> of an entry that applies to this folder, subfolders, and files when the piped object that has <Shape> removes its rights' -ForEach @ (
@ { Name = 'RemovePipedThisFolderOnly' ; Shape = 'AppliesTo ThisFolderOnly' ; Extra = @ { AppliesTo = 'ThisFolderOnly' } ; Meaning = 'the part that only subfolders and files inherit' ; Left = 'ContainerInherit, ObjectInherit/InheritOnly' }
@ { Name = 'RemovePipedNoScope' ; Shape = 'no scope' ; Extra = @ { } ; Meaning = 'nothing' ; Left = '' }
) {
$folder = New-TestSandboxItem -Sandbox $sandbox -Name $Name -Directory
Add-NTFSAccess -Path $folder -Account 'S-1-5-32-546' -AccessRights ReadData -AppliesTo ThisFolderSubfoldersAndFiles -ErrorAction Stop
[pscustomobject] ( @ { Path = $folder ; Account = 'S-1-5-32-546' ; AccessRights = 'ReadData' } + $Extra ) | Remove-NTFSAccess -ErrorAction Stop
fix(access,audit): keep the scope of a piped object from reaching the next one
Independent review of 93ed2c7: ProcessRecord of Add-NTFSAccess,
Remove-NTFSAccess, Add-NTFSAudit, and Remove-NTFSAudit wrote the scope
that it converts from AppliesTo into the fields behind InheritanceFlags
and PropagationFlags. PowerShell resets only the parameters that the
previous piped object bound, so the next object that had no scope, or
only one flag, took the scope of the one before it: AppliesTo
ThisFolderOnly, then none gave None/None twice, and Remove left the
InheritOnly part of every entry. Without the default set (93ed2c7) a
piped path object with AppliesTo reaches that code; for a piped
-SecurityDescriptor object it was a defect of rc7 already.
The flags of a record are locals now. The loop of the two Remove
cmdlets converted the scope a second time into the same fields; the
locals make that redundant, so it is gone.
The tests pipe objects with and without a scope, a CSV file, and
descriptor objects into the four cmdlets: red on the build of 93ed2c7
(8 of 87 cases of the two files, in every elevated configuration; 4 in
the basic-user ones, where the audit rows skip), green on this build.
An empty AppliesTo cell fails to bind for that row only; a test pins it.
The Remove-NTFSAudit PathSimple row cannot tell the sets apart (its
entry is on a file), and its comment says so. Docs/Examples.md no longer
says that a scope is needed so that PowerShell can choose the parameter
set (true before 5.0.0 only); the changelog entry names the new errors.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
3 weeks ago
Get-TestAccountScope -Acl ( Get-Acl -LiteralPath $folder ) -Account 'S-1-5-32-546' | Should -BeExactly $Left
fix(access,audit): bind the AppliesTo property of a piped object for paths
Add-NTFSAccess, Remove-NTFSAccess, Add-NTFSAudit, and Remove-NTFSAudit
declared PathComplex as the default parameter set. An object that had
Path, Account, and AccessRights satisfied the mandatory parameters of
the default set, so PowerShell never tried PathSimple and the cmdlet
dropped the AppliesTo property of the object. Since 4b1abc9 made
-AppliesTo mandatory in the Simple sets, the default set is redundant
for every call that names its parameters, and it masked PathSimple for
the pipeline.
Without a default set, an object that has AppliesTo binds PathSimple,
one that has InheritanceFlags or PropagationFlags binds PathComplex, and
one that has neither binds PathComplex with the default scope. An
object that has AppliesTo and InheritanceFlags now fails with
AmbiguousParameterSet, like the same two named parameters. Named calls,
-SecurityDescriptor calls without AppliesTo, and the round trip
Get-NTFSAccess | Add-NTFSAccess keep their behavior.
The tests pipe objects of each shape and fail without the change. The
cmdlet pages and the help file follow the build.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
3 weeks ago
}
It 'Should reject a piped object that has both AppliesTo and InheritanceFlags with an AmbiguousParameterSet error and change nothing' {
$folder = New-TestSandboxItem -Sandbox $sandbox -Name 'RemovePipedBoth' -Directory
Add-NTFSAccess -Path $folder -Account 'S-1-1-0' -AccessRights ReadData -AppliesTo ThisFolderOnly -ErrorAction Stop
$before = ( Get-Acl -LiteralPath $folder ) . Sddl
$object = [pscustomobject] @ { Path = $folder ; Account = 'S-1-1-0' ; AccessRights = 'ReadData' ; AppliesTo = 'ThisFolderOnly' ; InheritanceFlags = 'None' }
{ $object | Remove-NTFSAccess -ErrorAction Stop } | Should -Throw -ErrorId 'AmbiguousParameterSet*'
( Get-Acl -LiteralPath $folder ) . Sddl | Should -BeExactly $before
}
}
fix(access,audit): keep the scope of a piped object from reaching the next one
Independent review of 93ed2c7: ProcessRecord of Add-NTFSAccess,
Remove-NTFSAccess, Add-NTFSAudit, and Remove-NTFSAudit wrote the scope
that it converts from AppliesTo into the fields behind InheritanceFlags
and PropagationFlags. PowerShell resets only the parameters that the
previous piped object bound, so the next object that had no scope, or
only one flag, took the scope of the one before it: AppliesTo
ThisFolderOnly, then none gave None/None twice, and Remove left the
InheritOnly part of every entry. Without the default set (93ed2c7) a
piped path object with AppliesTo reaches that code; for a piped
-SecurityDescriptor object it was a defect of rc7 already.
The flags of a record are locals now. The loop of the two Remove
cmdlets converted the scope a second time into the same fields; the
locals make that redundant, so it is gone.
The tests pipe objects with and without a scope, a CSV file, and
descriptor objects into the four cmdlets: red on the build of 93ed2c7
(8 of 87 cases of the two files, in every elevated configuration; 4 in
the basic-user ones, where the audit rows skip), green on this build.
An empty AppliesTo cell fails to bind for that row only; a test pins it.
The Remove-NTFSAudit PathSimple row cannot tell the sets apart (its
entry is on a file), and its comment says so. Docs/Examples.md no longer
says that a scope is needed so that PowerShell can choose the parameter
set (true before 5.0.0 only); the changelog entry names the new errors.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
3 weeks ago
# As for Add-NTFSAccess: the scope that the cmdlet derives from AppliesTo must not stay for the next object.
Context 'Objects with different scopes in one pipeline' {
It 'Should remove from each piped path object the entry of its own scope: AppliesTo, none, AppliesTo, none' {
$folders = @ ( 'RemoveMixedFirst' , 'RemoveMixedSecond' , 'RemoveMixedThird' , 'RemoveMixedFourth' | ForEach-Object -Process {
$folder = New-TestSandboxItem -Sandbox $sandbox -Name $_ -Directory
Add-NTFSAccess -Path $folder -Account 'S-1-5-32-546' -AccessRights ReadData -AppliesTo ThisFolderSubfoldersAndFiles -ErrorAction Stop
$folder
} )
$entry = @ { Account = 'S-1-5-32-546' ; AccessRights = 'ReadData' }
@ (
[pscustomobject] ( $entry + @ { Path = $folders [ 0 ] ; AppliesTo = 'ThisFolderOnly' } )
[pscustomobject] ( $entry + @ { Path = $folders [ 1 ] } )
[pscustomobject] ( $entry + @ { Path = $folders [ 2 ] ; AppliesTo = 'ThisFolderOnly' } )
[pscustomobject] ( $entry + @ { Path = $folders [ 3 ] } )
) | Remove-NTFSAccess -ErrorAction Stop
$remaining = $folders | ForEach-Object -Process { Get-TestAccountScope -Acl ( Get-Acl -LiteralPath $_ ) -Account 'S-1-5-32-546' }
$remaining -join '|' | Should -BeExactly 'ContainerInherit, ObjectInherit/InheritOnly||ContainerInherit, ObjectInherit/InheritOnly|'
}
It 'Should remove from each piped descriptor object the entry of its own scope: AppliesTo, then none' {
$folders = @ ( 'RemoveMixedSDFirst' , 'RemoveMixedSDSecond' | ForEach-Object -Process {
$folder = New-TestSandboxItem -Sandbox $sandbox -Name $_ -Directory
Add-NTFSAccess -Path $folder -Account 'S-1-5-32-546' -AccessRights ReadData -AppliesTo ThisFolderSubfoldersAndFiles -ErrorAction Stop
$folder
} )
$descriptors = @ ( Get-NTFSSecurityDescriptor -Path $folders -ErrorAction Stop )
$entry = @ { Account = 'S-1-5-32-546' ; AccessRights = 'ReadData' }
@ (
[pscustomobject] ( $entry + @ { SecurityDescriptor = $descriptors [ 0 ] ; AppliesTo = 'ThisFolderOnly' } )
[pscustomobject] ( $entry + @ { SecurityDescriptor = $descriptors [ 1 ] } )
) | Remove-NTFSAccess -ErrorAction Stop
$remaining = $descriptors | ForEach-Object -Process { Get-TestAccountScope -Acl $_ . SecurityDescriptor -Account 'S-1-5-32-546' }
$remaining -join '|' | Should -BeExactly 'ContainerInherit, ObjectInherit/InheritOnly|'
}
}
test(access): assert the parameter sets that the judged matrix found untested
Adds Tests/ParameterSets.Access.Tests.ps1, closing the TestNeeded rows of
paramsets/judged-access.csv for Add-NTFSAccess, Remove-NTFSAccess,
Clear-NTFSAccess, Get-NTFSAccess, Get-NTFSOrphanedAccess,
Get-NTFSSimpleAccess, and Get-NTFSEffectiveAccess: 20 new It blocks (26
executed cases) covering pipeline binding (ValueFromPipeline and
ValueFromPipelineByPropertyName, including positional file/folder objects),
-PassThru combined with one denied and one succeeding path, error
ErrorId/category/target for read and write failures, continuation after a
per-item error, state read back with .NET (not -PassThru/Get-NTFSAccess),
SecurityDescriptor-set pipelines and memory-vs-disk behavior, and the
declared output types and PathSimple/SDSimple vs PathComplex/SDComplex
parameter-set boundaries. No row with GapKind=MaintainerDecision is pinned:
the -SecurityDescriptor sets of Add/Remove keep their open per-item error
behavior untested either way.
Finding: a plain piped object with an AppliesTo property does not steer
Add-NTFSAccess/Remove-NTFSAccess to the PathSimple/SDSimple parameter set by
itself; PowerShell resolves the ambiguity to the default set (PathComplex/
SDComplex) and silently drops the unmatched property. The proposed test for
that exact shape (notes section (b), item 2c) could not be written as
specified; the pipeline test for PathSimple instead binds -Path through the
pipeline while -AppliesTo is named explicitly, which does select the set.
Validated in all four configurations (elevated/basic user, Windows
PowerShell 5.1/PowerShell 7): 26 passed, 0 failed, 0 skipped in each. Eight
one-statement production mutations across all seven cmdlets were applied in
an isolated worktree, built, and run against this file; each was detected by
at least one new test, then the source was restored exactly and rebuilt.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
3 weeks ago
Context 'PathSimple parameter set' {
It 'Should take only the requested rights from an entry scoped by -AppliesTo without -RemoveSpecific, keep its flags, and leave another account''s entry untouched' {
$folder = New-TestSandboxItem -Sandbox $sandbox -Name 'RemoveSimpleScope' -Directory
Add-NTFSAccess -Path $folder -Account 'S-1-1-0' -AccessRights Modify -AppliesTo ThisFolderSubfoldersAndFiles -ErrorAction Stop
Add-NTFSAccess -Path $folder -Account 'S-1-5-32-546' -AccessRights Delete -AppliesTo ThisFolderSubfoldersAndFiles -ErrorAction Stop
Remove-NTFSAccess -Path $folder -Account 'S-1-1-0' -AccessRights ReadData -AppliesTo ThisFolderSubfoldersAndFiles -ErrorAction Stop
$rules = ( Get-Acl -LiteralPath $folder ) . GetAccessRules ( $true , $false , $sidType )
$everyoneRule = @ ( $rules | Where-Object -FilterScript { $_ . IdentityReference . Value -eq 'S-1-1-0' } )
$everyoneRule | Should -HaveCount 1
$everyoneRule [ 0 ] . FileSystemRights . HasFlag ( [System.Security.AccessControl.FileSystemRights] :: ReadData ) | Should -BeFalse
$everyoneRule [ 0 ] . FileSystemRights . HasFlag ( [System.Security.AccessControl.FileSystemRights] :: WriteData ) | Should -BeTrue
$everyoneRule [ 0 ] . InheritanceFlags | Should -Be ( [System.Security.AccessControl.InheritanceFlags] 'ContainerInherit, ObjectInherit' )
$everyoneRule [ 0 ] . PropagationFlags | Should -Be ( [System.Security.AccessControl.PropagationFlags] :: None )
$guestsRule = @ ( $rules | Where-Object -FilterScript { $_ . IdentityReference . Value -eq 'S-1-5-32-546' } )
$guestsRule | Should -HaveCount 1
$guestsRule [ 0 ] . FileSystemRights . HasFlag ( [System.Security.AccessControl.FileSystemRights] :: Delete ) | Should -BeTrue
}
}
test(access): close budget parameter-set gaps for the access cmdlets
Add 20 new It blocks (32 executed cases once the 13-scope -ForEach of
Add-NTFSAccess/SDSimple expands) to ParameterSets.Access.Tests.ps1, closing
every REMAINING item that worklist-access.md marks budget for rows 1-13:
- Add-NTFSAccess: several accounts and positional binding (PathComplex); a
Deny entry's exact rights with no auto-added Synchronize (PathComplex and
PathSimple); a denied write and a Deny entry without rights through
PathSimple; all 13 -AppliesTo scopes of a security descriptor read back
with .NET, piped and several (SDSimple); piped and several descriptors
asserted for Add itself, not only as a fixture (SDComplex).
- Remove-NTFSAccess: the -PassThru output's count against the complete
survivor list for several piped paths; several accounts and positional
binding (PathComplex); a Deny entry's rights-reduction without
-RemoveSpecific; a missing and a denied path, piped, several (PathSimple);
rights-reduction without -RemoveSpecific and disk independence until
Set-NTFSSecurityDescriptor, piped and several descriptors (SDSimple); the
on-disk write-through only after Set-NTFSSecurityDescriptor, piped and
several descriptors (SDComplex).
- Get-NTFSAccess: what the cmdlet returns without -Path, asserted on an
actual emitted object's type (Path).
- Get-NTFSOrphanedAccess: the category of ReadSecurityError after a failed
ownership retry (Path); -ExcludeInherited and an entry added to a
descriptor only in memory (SD).
- Get-NTFSSimpleAccess: the category and target of a missing path's
ReadError (Path).
- Get-NTFSEffectiveAccess: one call combining a failing and a succeeding
path (Path).
Items the REASON marks MaintainerDecision, Limit, or would-pin are left
untouched, per the group notes of the brief. No product code changed.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
2 weeks ago
Context 'PathComplex parameter set, additional state assertions' {
It 'Should return exactly the surviving entries with -PassThru for each of several paths bound through the pipeline in one call' {
$keeper = 'S-1-1-0'
$target = 'S-1-5-32-546'
$folders = @ (
New-TestSandboxItem -Sandbox $sandbox -Name 'RemovePassThruCountFirst' -Directory
New-TestSandboxItem -Sandbox $sandbox -Name 'RemovePassThruCountSecond' -Directory
)
foreach ( $folder in $folders ) {
Add-NTFSAccess -Path $folder -Account $keeper -AccessRights ReadData -ErrorAction Stop
Add-NTFSAccess -Path $folder -Account $target -AccessRights ReadData -ErrorAction Stop
}
# The independent oracle: every rule of the item that isn't the one being removed.
$expected = foreach ( $folder in $folders ) {
@ ( ( Get-Acl -LiteralPath $folder ) . GetAccessRules ( $true , $true , $sidType ) |
Where-Object -FilterScript { $_ . IdentityReference . Value -ne $target } ) . Count
}
$result = @ ( Get-Item2 -Path $folders | Remove-NTFSAccess -Account $target -AccessRights ReadData -PassThru -ErrorAction Stop )
for ( $index = 0 ; $index -lt $folders . Count ; $index + + ) {
$folderResult = @ ( $result | Where-Object -FilterScript { $_ . FullName -eq $folders [ $index ] } )
$folderResult | Should -HaveCount $expected [ $index ]
$folderResult | ForEach-Object -Process { $_ | Should -BeOfType ( [Security2.FileSystemAccessRule2] ) }
@ ( $folderResult | Where-Object -FilterScript { $_ . Account . Sid -eq $target } ) | Should -BeNullOrEmpty
@ ( $folderResult | Where-Object -FilterScript { $_ . Account . Sid -eq $keeper } ) | Should -HaveCount 1
}
}
It 'Should remove the entries of several accounts bound positionally, leaving another account untouched' {
$folder = New-TestSandboxItem -Sandbox $sandbox -Name 'RemovePositionalSeveral' -Directory
Add-NTFSAccess -Path $folder -Account 'S-1-1-0' -AccessRights ReadData -ErrorAction Stop
Add-NTFSAccess -Path $folder -Account 'S-1-5-32-546' -AccessRights ReadData -ErrorAction Stop
Add-NTFSAccess -Path $folder -Account 'S-1-5-21-1-2-3-9701' -AccessRights ReadData -ErrorAction Stop
Remove-NTFSAccess $folder @ ( 'S-1-1-0' , 'S-1-5-32-546' ) 'ReadData' -ErrorAction Stop
$rules = ( Get-Acl -LiteralPath $folder ) . GetAccessRules ( $true , $false , $sidType )
@ ( $rules | Where-Object -FilterScript { $_ . IdentityReference . Value -in 'S-1-1-0' , 'S-1-5-32-546' } ) | Should -BeNullOrEmpty
@ ( $rules | Where-Object -FilterScript { $_ . IdentityReference . Value -eq 'S-1-5-21-1-2-3-9701' } ) | Should -HaveCount 1
}
}
Context 'PathSimple parameter set, a Deny entry and pipeline errors' {
BeforeAll {
# So that an elevated session meets the same denial as a basic user (see PathErrors.Tests.ps1). Harmless
# for the Deny-reduction test below, which never hits a permission denial.
$privateData [ 'EnablePrivileges' ] = $false
}
AfterAll {
$privateData [ 'EnablePrivileges' ] = $enablePrivileges
}
It 'Should take only the requested rights from a Deny entry scoped by -AppliesTo without -RemoveSpecific, and keep its flags' {
$folder = New-TestSandboxItem -Sandbox $sandbox -Name 'RemoveSimpleDenyReduce' -Directory
Add-NTFSAccess -Path $folder -Account 'S-1-5-21-1-2-3-9702' -AccessRights Modify -AccessType Deny -AppliesTo ThisFolderAndSubfolders -ErrorAction Stop
Remove-NTFSAccess -Path $folder -Account 'S-1-5-21-1-2-3-9702' -AccessRights ReadData -AccessType Deny -AppliesTo ThisFolderAndSubfolders -ErrorAction Stop
$rule = @ ( ( Get-Acl -LiteralPath $folder ) . GetAccessRules ( $true , $false , $sidType ) |
Where-Object -FilterScript { $_ . IdentityReference . Value -eq 'S-1-5-21-1-2-3-9702' } )
$rule | Should -HaveCount 1
$rule [ 0 ] . AccessControlType | Should -Be ( [System.Security.AccessControl.AccessControlType] :: Deny )
$rule [ 0 ] . FileSystemRights . HasFlag ( [System.Security.AccessControl.FileSystemRights] :: ReadData ) | Should -BeFalse
$rule [ 0 ] . FileSystemRights . HasFlag ( [System.Security.AccessControl.FileSystemRights] :: WriteData ) | Should -BeTrue
$rule [ 0 ] . InheritanceFlags | Should -Be ( [System.Security.AccessControl.InheritanceFlags] :: ContainerInherit )
$rule [ 0 ] . PropagationFlags | Should -Be ( [System.Security.AccessControl.PropagationFlags] :: None )
}
It 'Should report a missing path''s and a denied path''s errors by category and target, and still scope the surviving paths'' entries, all bound through the pipeline' {
$missing = Join-Path -Path $sandbox -ChildPath ( 'RemoveSimpleMissing-{0}' -f [guid] :: NewGuid ( ) . ToString ( 'N' ) . Substring ( 0 , 8 ) )
Assert-TestSandboxPath -Sandbox $sandbox -Path $missing
$blocked = New-TestSandboxItem -Sandbox $sandbox -Name 'RemoveSimpleBlocked' -Directory
$survivor1 = New-TestSandboxItem -Sandbox $sandbox -Name 'RemoveSimpleSurvivor1' -Directory
$survivor2 = New-TestSandboxItem -Sandbox $sandbox -Name 'RemoveSimpleSurvivor2' -Directory
foreach ( $folder in $blocked , $survivor1 , $survivor2 ) {
Add-NTFSAccess -Path $folder -Account 'S-1-1-0' -AccessRights ReadData -AppliesTo ThisFolderOnly -ErrorAction Stop
}
Block-TestWritePermission -Sandbox $sandbox -Path $blocked
$before = ( Get-Acl -LiteralPath $blocked ) . Sddl
# Property-name binding of -Path, like the piped objects of the Add-NTFSAccess pipeline tests; -Account,
# -AccessRights and -AppliesTo come from the call itself and apply to every piped object alike.
$paths = foreach ( $value in $missing , $blocked , $survivor1 , $survivor2 ) { [pscustomobject] @ { Path = $value } }
$result = @ ( $paths | Remove-NTFSAccess -Account 'S-1-1-0' -AccessRights ReadData -AppliesTo ThisFolderOnly -PassThru -ErrorVariable removeErrors -ErrorAction SilentlyContinue )
$removeErrors | Should -HaveCount 2
$missingError = @ ( $removeErrors | Where-Object -FilterScript { $_ . TargetObject -eq $missing } )
$missingError | Should -HaveCount 1
$missingError [ 0 ] . FullyQualifiedErrorId | Should -BeLike 'ReadFileError,*'
$missingError [ 0 ] . CategoryInfo . Category | Should -Be 'OpenError'
$blockedError = @ ( $removeErrors | Where-Object -FilterScript { $_ . TargetObject -eq $blocked } )
$blockedError | Should -HaveCount 1
$blockedError [ 0 ] . FullyQualifiedErrorId | Should -BeLike 'RemoveAceError,*'
$blockedError [ 0 ] . CategoryInfo . Category | Should -Be 'WriteError'
( Get-Acl -LiteralPath $blocked ) . Sddl | Should -BeExactly $before
foreach ( $survivor in $survivor1 , $survivor2 ) {
@ ( ( Get-Acl -LiteralPath $survivor ) . GetAccessRules ( $true , $false , $sidType ) |
Where-Object -FilterScript { $_ . IdentityReference . Value -eq 'S-1-1-0' } ) | Should -BeNullOrEmpty
@ ( $result | Where-Object -FilterScript { $_ . FullName -eq $survivor } ) | Should -Not -BeNullOrEmpty
}
}
}
Context 'SDSimple parameter set' {
It 'Should take only the requested rights from an entry scoped by -AppliesTo on a descriptor without -RemoveSpecific, keep its flags, and leave the item on disk unchanged until Set-NTFSSecurityDescriptor' {
$folder = New-TestSandboxItem -Sandbox $sandbox -Name 'RemoveSdSimpleReduce' -Directory
Add-NTFSAccess -Path $folder -Account 'S-1-5-21-1-2-3-9602' -AccessRights Modify -AppliesTo ThisFolderAndSubfolders -ErrorAction Stop
$before = ( Get-Acl -LiteralPath $folder ) . Sddl
$sd = Get-NTFSSecurityDescriptor -Path $folder -ErrorAction Stop
Remove-NTFSAccess -SecurityDescriptor $sd -Account 'S-1-5-21-1-2-3-9602' -AccessRights ReadData -AppliesTo ThisFolderAndSubfolders -ErrorAction Stop
$rule = @ ( $sd . SecurityDescriptor . GetAccessRules ( $true , $false , $sidType ) |
Where-Object -FilterScript { $_ . IdentityReference . Value -eq 'S-1-5-21-1-2-3-9602' } )
$rule | Should -HaveCount 1
$rule [ 0 ] . FileSystemRights . HasFlag ( [System.Security.AccessControl.FileSystemRights] :: ReadData ) | Should -BeFalse
$rule [ 0 ] . FileSystemRights . HasFlag ( [System.Security.AccessControl.FileSystemRights] :: WriteData ) | Should -BeTrue
$rule [ 0 ] . InheritanceFlags | Should -Be ( [System.Security.AccessControl.InheritanceFlags] :: ContainerInherit )
$rule [ 0 ] . PropagationFlags | Should -Be ( [System.Security.AccessControl.PropagationFlags] :: None )
( Get-Acl -LiteralPath $folder ) . Sddl | Should -BeExactly $before
Set-NTFSSecurityDescriptor -SecurityDescriptor $sd -ErrorAction Stop
$onDisk = @ ( ( Get-Acl -LiteralPath $folder ) . GetAccessRules ( $true , $false , $sidType ) |
Where-Object -FilterScript { $_ . IdentityReference . Value -eq 'S-1-5-21-1-2-3-9602' } )
$onDisk | Should -HaveCount 1
$onDisk [ 0 ] . FileSystemRights . HasFlag ( [System.Security.AccessControl.FileSystemRights] :: ReadData ) | Should -BeFalse
}
It 'Should remove the matching entry from each of several descriptors bound through the pipeline for the Simple parameter set, leaving the items unchanged on disk' {
$folders = @ (
New-TestSandboxItem -Sandbox $sandbox -Name 'RemoveSdSimplePipeFirst' -Directory
New-TestSandboxItem -Sandbox $sandbox -Name 'RemoveSdSimplePipeSecond' -Directory
)
foreach ( $folder in $folders ) {
Add-NTFSAccess -Path $folder -Account 'S-1-5-21-1-2-3-9603' -AccessRights ReadData -AppliesTo ThisFolderOnly -ErrorAction Stop
}
$descriptors = @ ( Get-NTFSSecurityDescriptor -Path $folders -ErrorAction Stop )
$descriptors | Remove-NTFSAccess -Account 'S-1-5-21-1-2-3-9603' -AccessRights ReadData -AppliesTo ThisFolderOnly -RemoveSpecific -ErrorAction Stop
foreach ( $sd in $descriptors ) {
@ ( $sd . SecurityDescriptor . GetAccessRules ( $true , $false , $sidType ) |
Where-Object -FilterScript { $_ . IdentityReference . Value -eq 'S-1-5-21-1-2-3-9603' } ) | Should -BeNullOrEmpty
}
foreach ( $folder in $folders ) {
@ ( ( Get-Acl -LiteralPath $folder ) . GetAccessRules ( $true , $false , $sidType ) |
Where-Object -FilterScript { $_ . IdentityReference . Value -eq 'S-1-5-21-1-2-3-9603' } ) | Should -HaveCount 1
}
}
}
Context 'SDComplex parameter set' {
It 'Should remove an on-disk entry from a descriptor in memory only, and write the change through only when Set-NTFSSecurityDescriptor runs' {
$file = New-TestSandboxItem -Sandbox $sandbox -Name 'RemoveSdWriteThrough'
Add-NTFSAccess -Path $file -Account 'S-1-5-21-1-2-3-9601' -AccessRights ReadData -ErrorAction Stop
$sd = Get-NTFSSecurityDescriptor -Path $file -ErrorAction Stop
Remove-NTFSAccess -SecurityDescriptor $sd -Account 'S-1-5-21-1-2-3-9601' -AccessRights ReadData -ErrorAction Stop
@ ( $sd . SecurityDescriptor . GetAccessRules ( $true , $false , $sidType ) |
Where-Object -FilterScript { $_ . IdentityReference . Value -eq 'S-1-5-21-1-2-3-9601' } ) | Should -BeNullOrEmpty
@ ( ( Get-Acl -LiteralPath $file ) . GetAccessRules ( $true , $false , $sidType ) |
Where-Object -FilterScript { $_ . IdentityReference . Value -eq 'S-1-5-21-1-2-3-9601' } ) | Should -HaveCount 1
Set-NTFSSecurityDescriptor -SecurityDescriptor $sd -ErrorAction Stop
@ ( ( Get-Acl -LiteralPath $file ) . GetAccessRules ( $true , $false , $sidType ) |
Where-Object -FilterScript { $_ . IdentityReference . Value -eq 'S-1-5-21-1-2-3-9601' } ) | Should -BeNullOrEmpty
}
It 'Should remove the matching entry from each of several descriptors bound through the pipeline for the Complex parameter set, leaving the items unchanged on disk' {
$files = @ (
New-TestSandboxItem -Sandbox $sandbox -Name 'RemoveSdComplexPipeFirst'
New-TestSandboxItem -Sandbox $sandbox -Name 'RemoveSdComplexPipeSecond'
)
foreach ( $file in $files ) {
Add-NTFSAccess -Path $file -Account 'S-1-5-21-1-2-3-9604' -AccessRights ReadData -ErrorAction Stop
}
$descriptors = @ ( Get-NTFSSecurityDescriptor -Path $files -ErrorAction Stop )
$descriptors | Remove-NTFSAccess -Account 'S-1-5-21-1-2-3-9604' -AccessRights ReadData -ErrorAction Stop
foreach ( $sd in $descriptors ) {
@ ( $sd . SecurityDescriptor . GetAccessRules ( $true , $false , $sidType ) |
Where-Object -FilterScript { $_ . IdentityReference . Value -eq 'S-1-5-21-1-2-3-9604' } ) | Should -BeNullOrEmpty
}
foreach ( $file in $files ) {
@ ( ( Get-Acl -LiteralPath $file ) . GetAccessRules ( $true , $false , $sidType ) |
Where-Object -FilterScript { $_ . IdentityReference . Value -eq 'S-1-5-21-1-2-3-9604' } ) | Should -HaveCount 1
}
}
}
fix(access): report a failing change of a descriptor as an error per descriptor
In the four -SecurityDescriptor parameter sets of Add-NTFSAccess,
Remove-NTFSAccess, Add-NTFSAudit, and Remove-NTFSAudit, a change that
throws, such as a Deny entry without rights, ended the cmdlet with a
terminating error, while the -Path parameter sets already wrote an
AddAceError or RemoveAceError for the failing item and went on to the
next one. The two kinds of parameter sets now behave the same way.
Each descriptor's change is wrapped in its own try/catch: a failure
writes a non-terminating AddAceError or RemoveAceError, category
WriteError, that targets the descriptor, and the cmdlet continues
with the next descriptor in the pipeline instead of stopping. -PassThru
stays outside the try, so what a later command raises when the
cmdlet writes its output is never caught there. The existing
TestAuditSection check of the audit cmdlets, and everything else
about a successful change, is unchanged.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
1 week ago
# Item 5 of Decision 27: a failing change to one of several descriptors (a Deny entry without rights, which .NET
# refuses to build, the same as Access.Tests.ps1:571 pins for -Path) reports an error for that descriptor and goes
# on with the next one, instead of ending the cmdlet with a terminating error.
Context 'A failing change to one of several descriptors' {
It 'Should write a RemoveAceError for the descriptor whose change fails in the SDSimple set, continue with the next, and return -PassThru only for the one that succeeded' {
$first = New-TestSandboxItem -Sandbox $sandbox -Name 'SdErrorSimpleFirst'
$second = New-TestSandboxItem -Sandbox $sandbox -Name 'SdErrorSimpleSecond'
Add-NTFSAccess -Path $first , $second -Account 'S-1-1-0' -AccessRights ReadData -AccessType Deny -AppliesTo ThisFolderOnly -ErrorAction Stop
$descriptors = @ ( Get-NTFSSecurityDescriptor -Path $first , $second -ErrorAction Stop )
$before = $descriptors [ 0 ] . SecurityDescriptor . GetSecurityDescriptorSddlForm ( 'Access' )
$rows = @ (
[pscustomobject] @ { SecurityDescriptor = $descriptors [ 0 ] ; Account = 'S-1-1-0' ; AccessRights = 'None' ; AccessType = 'Deny' ; AppliesTo = 'ThisFolderOnly' }
[pscustomobject] @ { SecurityDescriptor = $descriptors [ 1 ] ; Account = 'S-1-1-0' ; AccessRights = 'ReadData' ; AccessType = 'Deny' ; AppliesTo = 'ThisFolderOnly' }
)
$result = @ ( $rows | Remove-NTFSAccess -PassThru -ErrorVariable sdErrors -ErrorAction SilentlyContinue )
$sdErrors | Should -HaveCount 1
$sdErrors [ 0 ] . FullyQualifiedErrorId | Should -BeLike 'RemoveAceError,*'
$sdErrors [ 0 ] . CategoryInfo . Category | Should -Be 'WriteError'
$sdErrors [ 0 ] . TargetObject | Should -Be $descriptors [ 0 ]
$descriptors [ 0 ] . SecurityDescriptor . GetSecurityDescriptorSddlForm ( 'Access' ) | Should -BeExactly $before
$result | Should -Not -BeNullOrEmpty
$result | ForEach-Object -Process { $_ . FullName | Should -Be $second }
@ ( $result | Where-Object -FilterScript { $_ . Account . Sid -eq 'S-1-1-0' } ) | Should -BeNullOrEmpty
}
It 'Should write a RemoveAceError for the descriptor whose change fails in the SDComplex set, continue with the next, and return -PassThru only for the one that succeeded' {
$first = New-TestSandboxItem -Sandbox $sandbox -Name 'SdErrorComplexFirst'
$second = New-TestSandboxItem -Sandbox $sandbox -Name 'SdErrorComplexSecond'
Add-NTFSAccess -Path $first , $second -Account 'S-1-1-0' -AccessRights ReadData -AccessType Deny -ErrorAction Stop
$descriptors = @ ( Get-NTFSSecurityDescriptor -Path $first , $second -ErrorAction Stop )
$before = $descriptors [ 0 ] . SecurityDescriptor . GetSecurityDescriptorSddlForm ( 'Access' )
$rows = @ (
[pscustomobject] @ { SecurityDescriptor = $descriptors [ 0 ] ; Account = 'S-1-1-0' ; AccessRights = 'None' ; AccessType = 'Deny' }
[pscustomobject] @ { SecurityDescriptor = $descriptors [ 1 ] ; Account = 'S-1-1-0' ; AccessRights = 'ReadData' ; AccessType = 'Deny' }
)
$result = @ ( $rows | Remove-NTFSAccess -PassThru -ErrorVariable sdErrors -ErrorAction SilentlyContinue )
$sdErrors | Should -HaveCount 1
$sdErrors [ 0 ] . FullyQualifiedErrorId | Should -BeLike 'RemoveAceError,*'
$sdErrors [ 0 ] . CategoryInfo . Category | Should -Be 'WriteError'
$sdErrors [ 0 ] . TargetObject | Should -Be $descriptors [ 0 ]
$descriptors [ 0 ] . SecurityDescriptor . GetSecurityDescriptorSddlForm ( 'Access' ) | Should -BeExactly $before
$result | Should -Not -BeNullOrEmpty
$result | ForEach-Object -Process { $_ . FullName | Should -Be $second }
@ ( $result | Where-Object -FilterScript { $_ . Account . Sid -eq 'S-1-1-0' } ) | Should -BeNullOrEmpty
}
}
test(access): assert the parameter sets that the judged matrix found untested
Adds Tests/ParameterSets.Access.Tests.ps1, closing the TestNeeded rows of
paramsets/judged-access.csv for Add-NTFSAccess, Remove-NTFSAccess,
Clear-NTFSAccess, Get-NTFSAccess, Get-NTFSOrphanedAccess,
Get-NTFSSimpleAccess, and Get-NTFSEffectiveAccess: 20 new It blocks (26
executed cases) covering pipeline binding (ValueFromPipeline and
ValueFromPipelineByPropertyName, including positional file/folder objects),
-PassThru combined with one denied and one succeeding path, error
ErrorId/category/target for read and write failures, continuation after a
per-item error, state read back with .NET (not -PassThru/Get-NTFSAccess),
SecurityDescriptor-set pipelines and memory-vs-disk behavior, and the
declared output types and PathSimple/SDSimple vs PathComplex/SDComplex
parameter-set boundaries. No row with GapKind=MaintainerDecision is pinned:
the -SecurityDescriptor sets of Add/Remove keep their open per-item error
behavior untested either way.
Finding: a plain piped object with an AppliesTo property does not steer
Add-NTFSAccess/Remove-NTFSAccess to the PathSimple/SDSimple parameter set by
itself; PowerShell resolves the ambiguity to the default set (PathComplex/
SDComplex) and silently drops the unmatched property. The proposed test for
that exact shape (notes section (b), item 2c) could not be written as
specified; the pipeline test for PathSimple instead binds -Path through the
pipeline while -AppliesTo is named explicitly, which does select the set.
Validated in all four configurations (elevated/basic user, Windows
PowerShell 5.1/PowerShell 7): 26 passed, 0 failed, 0 skipped in each. Eight
one-statement production mutations across all seven cmdlets were applied in
an isolated worktree, built, and run against this file; each was detected by
at least one new test, then the source was restored exactly and rebuilt.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
3 weeks ago
}
Describe 'Clear-NTFSAccess' {
Context 'Path parameter set on folders bound through the pipeline' {
It 'Should clear each piped folder''s explicit entries, including an inherit-only one, and leave inherited entries and the owner untouched' {
$folders = foreach ( $name in 'ClearPipeFirst' , 'ClearPipeSecond' ) {
$folder = New-TestSandboxItem -Sandbox $sandbox -Name $name -Directory
Add-NTFSAccess -Path $folder -Account 'S-1-1-0' -AccessRights ReadData -AppliesTo ThisFolderOnly -ErrorAction Stop
Add-NTFSAccess -Path $folder -Account 'S-1-5-32-546' -AccessRights ReadData -AppliesTo SubfoldersAndFilesOnly -ErrorAction Stop
$folder
}
$before = foreach ( $folder in $folders ) {
$acl = Get-Acl -LiteralPath $folder
[pscustomobject] @ { Path = $folder ; Owner = $acl . GetOwner ( $sidType ) . Value ; Inherited = @ ( $acl . GetAccessRules ( $false , $true , $sidType ) ) . Count }
}
foreach ( $state in $before ) {
$state . Inherited | Should -BeGreaterThan 0
}
Get-Item2 -Path $folders | Clear-NTFSAccess -ErrorAction Stop
foreach ( $state in $before ) {
$acl = Get-Acl -LiteralPath $state . Path
@ ( $acl . GetAccessRules ( $true , $false , $sidType ) ) | Should -BeNullOrEmpty
@ ( $acl . GetAccessRules ( $false , $true , $sidType ) ) | Should -HaveCount $state . Inherited
$acl . AreAccessRulesProtected | Should -BeFalse
$acl . GetOwner ( $sidType ) . Value | Should -Be $state . Owner
}
}
}
Context 'Path parameter set errors' {
BeforeAll {
$privateData [ 'EnablePrivileges' ] = $false
}
AfterAll {
$privateData [ 'EnablePrivileges' ] = $enablePrivileges
}
It 'Should report a missing path''s ReadFileError and a blocked path''s ClearAclError, each by category and target' {
$missing = Join-Path -Path $sandbox -ChildPath ( 'ClearMissing-{0}' -f [guid] :: NewGuid ( ) . ToString ( 'N' ) . Substring ( 0 , 8 ) )
Assert-TestSandboxPath -Sandbox $sandbox -Path $missing
Clear-NTFSAccess -Path $missing -ErrorVariable missingErrors -ErrorAction SilentlyContinue
$missingErrors | Should -HaveCount 1
$missingErrors [ 0 ] . FullyQualifiedErrorId | Should -BeLike 'ReadFileError,*'
$missingErrors [ 0 ] . CategoryInfo . Category | Should -Be 'OpenError'
$missingErrors [ 0 ] . TargetObject | Should -Be $missing
$blocked = New-TestSandboxItem -Sandbox $sandbox -Name 'ClearBlocked'
Block-TestWritePermission -Sandbox $sandbox -Path $blocked
$before = ( Get-Acl -LiteralPath $blocked ) . Sddl
Clear-NTFSAccess -Path $blocked -ErrorVariable blockedErrors -ErrorAction SilentlyContinue
$blockedErrors | Should -HaveCount 1
$blockedErrors [ 0 ] . FullyQualifiedErrorId | Should -BeLike 'ClearAclError,*'
$blockedErrors [ 0 ] . CategoryInfo . Category | Should -Be 'WriteError'
$blockedErrors [ 0 ] . TargetObject | Should -Be $blocked
( Get-Acl -LiteralPath $blocked ) . Sddl | Should -BeExactly $before
}
}
Context 'SD parameter set bound through the pipeline' {
It 'Should clear each piped descriptor''s explicit entries in memory, leave the items unchanged until Set-NTFSSecurityDescriptor, and keep the owner' {
$files = foreach ( $name in 'ClearSdFirst' , 'ClearSdSecond' ) {
$file = New-TestSandboxItem -Sandbox $sandbox -Name $name
Add-NTFSAccess -Path $file -Account 'S-1-1-0' -AccessRights ReadData -ErrorAction Stop
$file
}
$before = foreach ( $file in $files ) {
$acl = Get-Acl -LiteralPath $file
[pscustomobject] @ { Path = $file ; Owner = $acl . GetOwner ( $sidType ) . Value ; Inherited = @ ( $acl . GetAccessRules ( $false , $true , $sidType ) ) . Count ; Sddl = $acl . Sddl }
}
$descriptors = @ ( Get-NTFSSecurityDescriptor -Path $files -ErrorAction Stop )
$descriptors | Clear-NTFSAccess -ErrorAction Stop
for ( $index = 0 ; $index -lt $descriptors . Count ; $index + + ) {
$sd = $descriptors [ $index ]
$state = $before [ $index ]
@ ( $sd . SecurityDescriptor . GetAccessRules ( $true , $false , $sidType ) ) | Should -BeNullOrEmpty
@ ( $sd . SecurityDescriptor . GetAccessRules ( $false , $true , $sidType ) ) | Should -HaveCount $state . Inherited
$sd . SecurityDescriptor . AreAccessRulesProtected | Should -BeFalse
( Get-Acl -LiteralPath $state . Path ) . Sddl | Should -BeExactly $state . Sddl
}
Set-NTFSSecurityDescriptor -SecurityDescriptor $descriptors -ErrorAction Stop
foreach ( $state in $before ) {
$acl = Get-Acl -LiteralPath $state . Path
@ ( $acl . GetAccessRules ( $true , $false , $sidType ) ) | Should -BeNullOrEmpty
$acl . GetOwner ( $sidType ) . Value | Should -Be $state . Owner
}
}
}
}
Describe 'Get-NTFSAccess' {
Context 'Path parameter set' {
It 'Should return the same entries for a folder bound through the pipeline and by position, and report a missing path''s error by category and target' {
$parent = New-TestSandboxItem -Sandbox $sandbox -Name 'GetAccessPositionParent' -Directory
Add-NTFSAccess -Path $parent -Account 'S-1-1-0' -AccessRights ReadData -ErrorAction Stop
$child = Join-Path -Path $parent -ChildPath 'Child'
Assert-TestSandboxPath -Sandbox $sandbox -Path $child
New-Item -ItemType Directory -Path $child | Out-Null
$expected = @ ( Get-NTFSAccess -Path $child -ErrorAction Stop | ForEach-Object -Process { '{0}|{1}|{2}' -f $_ . Account . Sid , $_ . AccessRights , $_ . IsInherited } )
$pipedResult = @ ( Get-Item2 -Path $child | Get-NTFSAccess -ErrorAction Stop )
$positionalFolder = Get-ChildItem -LiteralPath $parent -Directory
$positionalResult = @ ( Get-NTFSAccess $positionalFolder -ErrorAction Stop )
( $pipedResult | ForEach-Object -Process { '{0}|{1}|{2}' -f $_ . Account . Sid , $_ . AccessRights , $_ . IsInherited } ) -join ';' | Should -Be ( $expected -join ';' )
$pipedResult | ForEach-Object -Process { $_ . FullName | Should -Be $child }
( $positionalResult | ForEach-Object -Process { '{0}|{1}|{2}' -f $_ . Account . Sid , $_ . AccessRights , $_ . IsInherited } ) -join ';' | Should -Be ( $expected -join ';' )
$positionalResult | ForEach-Object -Process { $_ . FullName | Should -Be $child }
$missing = Join-Path -Path $sandbox -ChildPath ( 'GetAccessMissing-{0}' -f [guid] :: NewGuid ( ) . ToString ( 'N' ) . Substring ( 0 , 8 ) )
Assert-TestSandboxPath -Sandbox $sandbox -Path $missing
Get-NTFSAccess -Path $missing -ErrorVariable getErrors -ErrorAction SilentlyContinue
$getErrors | Should -HaveCount 1
$getErrors [ 0 ] . FullyQualifiedErrorId | Should -BeLike 'ReadFileError,*'
$getErrors [ 0 ] . CategoryInfo . Category | Should -Be 'OpenError'
$getErrors [ 0 ] . TargetObject | Should -Be $missing
}
}
test(access): close budget parameter-set gaps for the access cmdlets
Add 20 new It blocks (32 executed cases once the 13-scope -ForEach of
Add-NTFSAccess/SDSimple expands) to ParameterSets.Access.Tests.ps1, closing
every REMAINING item that worklist-access.md marks budget for rows 1-13:
- Add-NTFSAccess: several accounts and positional binding (PathComplex); a
Deny entry's exact rights with no auto-added Synchronize (PathComplex and
PathSimple); a denied write and a Deny entry without rights through
PathSimple; all 13 -AppliesTo scopes of a security descriptor read back
with .NET, piped and several (SDSimple); piped and several descriptors
asserted for Add itself, not only as a fixture (SDComplex).
- Remove-NTFSAccess: the -PassThru output's count against the complete
survivor list for several piped paths; several accounts and positional
binding (PathComplex); a Deny entry's rights-reduction without
-RemoveSpecific; a missing and a denied path, piped, several (PathSimple);
rights-reduction without -RemoveSpecific and disk independence until
Set-NTFSSecurityDescriptor, piped and several descriptors (SDSimple); the
on-disk write-through only after Set-NTFSSecurityDescriptor, piped and
several descriptors (SDComplex).
- Get-NTFSAccess: what the cmdlet returns without -Path, asserted on an
actual emitted object's type (Path).
- Get-NTFSOrphanedAccess: the category of ReadSecurityError after a failed
ownership retry (Path); -ExcludeInherited and an entry added to a
descriptor only in memory (SD).
- Get-NTFSSimpleAccess: the category and target of a missing path's
ReadError (Path).
- Get-NTFSEffectiveAccess: one call combining a failing and a succeeding
path (Path).
Items the REASON marks MaintainerDecision, Limit, or would-pin are left
untouched, per the group notes of the brief. No product code changed.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
2 weeks ago
Context 'Path parameter set without -Path' {
It 'Should return the entries of the current location as actual Security2.FileSystemAccessRule2 objects when -Path is omitted' {
$folder = New-TestSandboxItem -Sandbox $sandbox -Name 'GetAccessNoPath' -Directory
Add-NTFSAccess -Path $folder -Account 'S-1-1-0' -AccessRights ReadData -ErrorAction Stop
$expected = @ ( Get-NTFSAccess -Path $folder -ErrorAction Stop )
Push-Location -LiteralPath $folder
try {
$result = @ ( Get-NTFSAccess -ErrorAction Stop )
}
finally {
Pop-Location
}
$result | Should -HaveCount $expected . Count
$result | ForEach-Object -Process {
$_ | Should -BeOfType ( [Security2.FileSystemAccessRule2] )
$_ . FullName | Should -Be $folder
}
@ ( $result | Where-Object -FilterScript { $_ . Account . Sid -eq 'S-1-1-0' -and -not $_ . IsInherited } ) | Should -HaveCount 1
}
}
test(access): assert the parameter sets that the judged matrix found untested
Adds Tests/ParameterSets.Access.Tests.ps1, closing the TestNeeded rows of
paramsets/judged-access.csv for Add-NTFSAccess, Remove-NTFSAccess,
Clear-NTFSAccess, Get-NTFSAccess, Get-NTFSOrphanedAccess,
Get-NTFSSimpleAccess, and Get-NTFSEffectiveAccess: 20 new It blocks (26
executed cases) covering pipeline binding (ValueFromPipeline and
ValueFromPipelineByPropertyName, including positional file/folder objects),
-PassThru combined with one denied and one succeeding path, error
ErrorId/category/target for read and write failures, continuation after a
per-item error, state read back with .NET (not -PassThru/Get-NTFSAccess),
SecurityDescriptor-set pipelines and memory-vs-disk behavior, and the
declared output types and PathSimple/SDSimple vs PathComplex/SDComplex
parameter-set boundaries. No row with GapKind=MaintainerDecision is pinned:
the -SecurityDescriptor sets of Add/Remove keep their open per-item error
behavior untested either way.
Finding: a plain piped object with an AppliesTo property does not steer
Add-NTFSAccess/Remove-NTFSAccess to the PathSimple/SDSimple parameter set by
itself; PowerShell resolves the ambiguity to the default set (PathComplex/
SDComplex) and silently drops the unmatched property. The proposed test for
that exact shape (notes section (b), item 2c) could not be written as
specified; the pipeline test for PathSimple instead binds -Path through the
pipeline while -AppliesTo is named explicitly, which does select the set.
Validated in all four configurations (elevated/basic user, Windows
PowerShell 5.1/PowerShell 7): 26 passed, 0 failed, 0 skipped in each. Eight
one-statement production mutations across all seven cmdlets were applied in
an isolated worktree, built, and run against this file; each was detected by
at least one new test, then the source was restored exactly and rebuilt.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
3 weeks ago
Context 'SD parameter set' {
It 'Should filter an entry added to a descriptor in memory by -Account, partition it with -ExcludeInherited and -ExcludeExplicit, and read two descriptors bound through the pipeline' {
$file = New-TestSandboxItem -Sandbox $sandbox -Name 'GetAccessSdFilter'
$sd = Get-NTFSSecurityDescriptor -Path $file -ErrorAction Stop
Add-NTFSAccess -SecurityDescriptor $sd -Account 'S-1-1-0' -AccessRights ReadData -ErrorAction Stop
$bySd = @ ( Get-NTFSAccess -SecurityDescriptor $sd -Account 'S-1-1-0' -ErrorAction Stop )
$byPath = @ ( Get-NTFSAccess -Path $file -Account 'S-1-1-0' -ErrorAction Stop )
$bySd | Should -HaveCount 1
$bySd [ 0 ] . IsInherited | Should -BeFalse
$byPath | Should -BeNullOrEmpty
$explicitOnly = @ ( Get-NTFSAccess -SecurityDescriptor $sd -ExcludeInherited -ErrorAction Stop )
@ ( $explicitOnly | Where-Object -FilterScript { $_ . Account . Sid -eq 'S-1-1-0' } ) | Should -HaveCount 1
$inheritedOnly = @ ( Get-NTFSAccess -SecurityDescriptor $sd -ExcludeExplicit -ErrorAction Stop )
@ ( $inheritedOnly | Where-Object -FilterScript { $_ . Account . Sid -eq 'S-1-1-0' } ) | Should -BeNullOrEmpty
$file2 = New-TestSandboxItem -Sandbox $sandbox -Name 'GetAccessSdFilter2'
$expectedFirst = @ ( Get-NTFSAccess -Path $file -ErrorAction Stop ) . Count
$expectedSecond = @ ( Get-NTFSAccess -Path $file2 -ErrorAction Stop ) . Count
$piped = @ ( Get-NTFSSecurityDescriptor -Path $file , $file2 -ErrorAction Stop | Get-NTFSAccess -ErrorAction Stop )
@ ( $piped | Where-Object -FilterScript { $_ . FullName -eq $file } ) | Should -HaveCount $expectedFirst
@ ( $piped | Where-Object -FilterScript { $_ . FullName -eq $file2 } ) | Should -HaveCount $expectedSecond
}
}
Context 'Path parameter set, unreadable folder' {
BeforeAll {
# Forcing EnablePrivileges off reproduces the same denial as a basic user for an elevated session, as
# PathErrors.Tests.ps1 does for the same scenario, and as the Get-NTFSOrphanedAccess test below does.
$privateData [ 'EnablePrivileges' ] = $false
}
AfterAll {
$privateData [ 'EnablePrivileges' ] = $enablePrivileges
}
# Get-NTFSAccess, Get-NTFSAudit, Get-NTFSEffectiveAccess, Get-NTFSInheritance, and Get-NTFSSecurityDescriptor
# all report a failed ownership retry as ReadSecurityError categorized as WriteError (GetAccess.cs); this is
# the module's convention. A characterization test: the behavior is already correct, only not yet pinned for
# this cmdlet (6a). Unlike Get-NTFSOrphanedAccess, whose own category for the same failure is ReadError.
It 'Should write a ReadSecurityError categorized as WriteError after a failed ownership retry, and still return the next path''s entries' {
$blocked = New-TestSandboxItem -Sandbox $sandbox -Name 'GetAccessUnreadable'
Block-TestReadPermission -Sandbox $sandbox -Path $blocked
$next = New-TestSandboxItem -Sandbox $sandbox -Name 'GetAccessUnreadableNext'
Add-NTFSAccess -Path $next -Account 'S-1-1-0' -AccessRights ReadData -ErrorAction Stop
$result = @ ( Get-NTFSAccess -Path $blocked , $next -ErrorVariable accessErrors -ErrorAction SilentlyContinue )
$accessErrors | Should -HaveCount 1
$accessErrors [ 0 ] . FullyQualifiedErrorId | Should -BeLike 'ReadSecurityError,*'
$accessErrors [ 0 ] . CategoryInfo . Category | Should -Be 'WriteError'
$accessErrors [ 0 ] . TargetObject | Should -Be $blocked
@ ( $result | Where-Object -FilterScript { $_ . FullName -eq $next -and $_ . Account . Sid -eq 'S-1-1-0' -and -not $_ . IsInherited } ) | Should -HaveCount 1
}
}
test(access): assert the parameter sets that the judged matrix found untested
Adds Tests/ParameterSets.Access.Tests.ps1, closing the TestNeeded rows of
paramsets/judged-access.csv for Add-NTFSAccess, Remove-NTFSAccess,
Clear-NTFSAccess, Get-NTFSAccess, Get-NTFSOrphanedAccess,
Get-NTFSSimpleAccess, and Get-NTFSEffectiveAccess: 20 new It blocks (26
executed cases) covering pipeline binding (ValueFromPipeline and
ValueFromPipelineByPropertyName, including positional file/folder objects),
-PassThru combined with one denied and one succeeding path, error
ErrorId/category/target for read and write failures, continuation after a
per-item error, state read back with .NET (not -PassThru/Get-NTFSAccess),
SecurityDescriptor-set pipelines and memory-vs-disk behavior, and the
declared output types and PathSimple/SDSimple vs PathComplex/SDComplex
parameter-set boundaries. No row with GapKind=MaintainerDecision is pinned:
the -SecurityDescriptor sets of Add/Remove keep their open per-item error
behavior untested either way.
Finding: a plain piped object with an AppliesTo property does not steer
Add-NTFSAccess/Remove-NTFSAccess to the PathSimple/SDSimple parameter set by
itself; PowerShell resolves the ambiguity to the default set (PathComplex/
SDComplex) and silently drops the unmatched property. The proposed test for
that exact shape (notes section (b), item 2c) could not be written as
specified; the pipeline test for PathSimple instead binds -Path through the
pipeline while -AppliesTo is named explicitly, which does select the set.
Validated in all four configurations (elevated/basic user, Windows
PowerShell 5.1/PowerShell 7): 26 passed, 0 failed, 0 skipped in each. Eight
one-statement production mutations across all seven cmdlets were applied in
an isolated worktree, built, and run against this file; each was detected by
at least one new test, then the source was restored exactly and rebuilt.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
3 weeks ago
}
Describe 'Get-NTFSOrphanedAccess' {
Context 'Path parameter set' {
It 'Should mark an orphaned entry explicit on its own folder and inherited on a child, partitioned by -ExcludeInherited and -ExcludeExplicit' {
$folder = New-TestSandboxItem -Sandbox $sandbox -Name 'OrphanedInheritableFolder' -Directory
Add-NTFSAccess -Path $folder -Account 'S-1-5-21-1-2-3-9101' -AccessRights ReadData -AppliesTo ThisFolderSubfoldersAndFiles -ErrorAction Stop
$child = Join-Path -Path $folder -ChildPath 'Child.txt'
Assert-TestSandboxPath -Sandbox $sandbox -Path $child
Set-Content -LiteralPath $child -Value 'Child'
$folderResult = @ ( Get-NTFSOrphanedAccess -Path $folder -ErrorAction Stop )
$childResult = @ ( Get-NTFSOrphanedAccess -Path $child -ErrorAction Stop )
$folderResult | Should -HaveCount 1
$folderResult [ 0 ] . IsInherited | Should -BeFalse
$childResult | Should -HaveCount 1
$childResult [ 0 ] . IsInherited | Should -BeTrue
@ ( Get-NTFSOrphanedAccess -Path $child -ExcludeInherited -ErrorAction Stop ) | Should -BeNullOrEmpty
@ ( Get-NTFSOrphanedAccess -Path $folder -ExcludeExplicit -ErrorAction Stop ) | Should -BeNullOrEmpty
}
It 'Should write a ReadFileError for a missing path, still return the orphaned entry of the next path, and accept both bound through the pipeline' {
$missing = Join-Path -Path $sandbox -ChildPath ( 'OrphanedMissing-{0}' -f [guid] :: NewGuid ( ) . ToString ( 'N' ) . Substring ( 0 , 8 ) )
Assert-TestSandboxPath -Sandbox $sandbox -Path $missing
$withOrphan = New-TestSandboxItem -Sandbox $sandbox -Name 'OrphanedNext'
Add-NTFSAccess -Path $withOrphan -Account 'S-1-5-21-1-2-3-9102' -AccessRights ReadData -ErrorAction Stop
$result = @ ( Get-NTFSOrphanedAccess -Path $missing , $withOrphan -ErrorVariable orphanErrors -ErrorAction SilentlyContinue )
$orphanErrors | Should -HaveCount 1
$orphanErrors [ 0 ] . FullyQualifiedErrorId | Should -BeLike 'ReadFileError,*'
$orphanErrors [ 0 ] . CategoryInfo . Category | Should -Be 'OpenError'
$orphanErrors [ 0 ] . TargetObject | Should -Be $missing
$result | Should -HaveCount 1
$result [ 0 ] | Should -BeOfType ( [Security2.FileSystemAccessRule2] )
$result [ 0 ] . FullName | Should -Be $withOrphan
" $( $result [ 0 ] . Account . Sid ) " | Should -Be 'S-1-5-21-1-2-3-9102'
$withOrphan2 = New-TestSandboxItem -Sandbox $sandbox -Name 'OrphanedNext2'
Add-NTFSAccess -Path $withOrphan2 -Account 'S-1-5-21-1-2-3-9103' -AccessRights ReadData -ErrorAction Stop
$pipedResult = @ ( Get-Item2 -Path $withOrphan , $withOrphan2 | Get-NTFSOrphanedAccess -ErrorAction Stop )
$pipedResult | Should -HaveCount 2
@ ( $pipedResult | Where-Object -FilterScript { $_ . FullName -eq $withOrphan } ) | Should -HaveCount 1
@ ( $pipedResult | Where-Object -FilterScript { $_ . FullName -eq $withOrphan2 } ) | Should -HaveCount 1
}
}
test(access): close budget parameter-set gaps for the access cmdlets
Add 20 new It blocks (32 executed cases once the 13-scope -ForEach of
Add-NTFSAccess/SDSimple expands) to ParameterSets.Access.Tests.ps1, closing
every REMAINING item that worklist-access.md marks budget for rows 1-13:
- Add-NTFSAccess: several accounts and positional binding (PathComplex); a
Deny entry's exact rights with no auto-added Synchronize (PathComplex and
PathSimple); a denied write and a Deny entry without rights through
PathSimple; all 13 -AppliesTo scopes of a security descriptor read back
with .NET, piped and several (SDSimple); piped and several descriptors
asserted for Add itself, not only as a fixture (SDComplex).
- Remove-NTFSAccess: the -PassThru output's count against the complete
survivor list for several piped paths; several accounts and positional
binding (PathComplex); a Deny entry's rights-reduction without
-RemoveSpecific; a missing and a denied path, piped, several (PathSimple);
rights-reduction without -RemoveSpecific and disk independence until
Set-NTFSSecurityDescriptor, piped and several descriptors (SDSimple); the
on-disk write-through only after Set-NTFSSecurityDescriptor, piped and
several descriptors (SDComplex).
- Get-NTFSAccess: what the cmdlet returns without -Path, asserted on an
actual emitted object's type (Path).
- Get-NTFSOrphanedAccess: the category of ReadSecurityError after a failed
ownership retry (Path); -ExcludeInherited and an entry added to a
descriptor only in memory (SD).
- Get-NTFSSimpleAccess: the category and target of a missing path's
ReadError (Path).
- Get-NTFSEffectiveAccess: one call combining a failing and a succeeding
path (Path).
Items the REASON marks MaintainerDecision, Limit, or would-pin are left
untouched, per the group notes of the brief. No product code changed.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
2 weeks ago
Context 'Path parameter set, unreadable folder' {
BeforeAll {
# Forcing EnablePrivileges off reproduces the same denial as a basic user for an elevated session, as
# PathErrors.Tests.ps1 does for the same scenario.
$privateData [ 'EnablePrivileges' ] = $false
}
AfterAll {
$privateData [ 'EnablePrivileges' ] = $enablePrivileges
}
# Unlike Get-NTFSAccess, ReadSecurityError of this cmdlet has one consistent category after a failed
# ownership retry: ReadError (GetOrphanedAccess.cs), so it is safe to pin.
It 'Should write a ReadSecurityError categorized as ReadError after a failed ownership retry, and still return the next path''s orphaned entry' {
$blocked = New-TestSandboxItem -Sandbox $sandbox -Name 'OrphanedUnreadable'
Block-TestReadPermission -Sandbox $sandbox -Path $blocked
$next = New-TestSandboxItem -Sandbox $sandbox -Name 'OrphanedUnreadableNext'
Add-NTFSAccess -Path $next -Account 'S-1-5-21-1-2-3-9301' -AccessRights ReadData -ErrorAction Stop
$result = @ ( Get-NTFSOrphanedAccess -Path $blocked , $next -ErrorVariable orphanErrors -ErrorAction SilentlyContinue )
$orphanErrors | Should -HaveCount 1
$orphanErrors [ 0 ] . FullyQualifiedErrorId | Should -BeLike 'ReadSecurityError,*'
$orphanErrors [ 0 ] . CategoryInfo . Category | Should -Be 'ReadError'
$orphanErrors [ 0 ] . TargetObject | Should -Be $blocked
$result | Should -HaveCount 1
" $( $result [ 0 ] . Account . Sid ) " | Should -Be 'S-1-5-21-1-2-3-9301'
}
}
test(access): assert the parameter sets that the judged matrix found untested
Adds Tests/ParameterSets.Access.Tests.ps1, closing the TestNeeded rows of
paramsets/judged-access.csv for Add-NTFSAccess, Remove-NTFSAccess,
Clear-NTFSAccess, Get-NTFSAccess, Get-NTFSOrphanedAccess,
Get-NTFSSimpleAccess, and Get-NTFSEffectiveAccess: 20 new It blocks (26
executed cases) covering pipeline binding (ValueFromPipeline and
ValueFromPipelineByPropertyName, including positional file/folder objects),
-PassThru combined with one denied and one succeeding path, error
ErrorId/category/target for read and write failures, continuation after a
per-item error, state read back with .NET (not -PassThru/Get-NTFSAccess),
SecurityDescriptor-set pipelines and memory-vs-disk behavior, and the
declared output types and PathSimple/SDSimple vs PathComplex/SDComplex
parameter-set boundaries. No row with GapKind=MaintainerDecision is pinned:
the -SecurityDescriptor sets of Add/Remove keep their open per-item error
behavior untested either way.
Finding: a plain piped object with an AppliesTo property does not steer
Add-NTFSAccess/Remove-NTFSAccess to the PathSimple/SDSimple parameter set by
itself; PowerShell resolves the ambiguity to the default set (PathComplex/
SDComplex) and silently drops the unmatched property. The proposed test for
that exact shape (notes section (b), item 2c) could not be written as
specified; the pipeline test for PathSimple instead binds -Path through the
pipeline while -AppliesTo is named explicitly, which does select the set.
Validated in all four configurations (elevated/basic user, Windows
PowerShell 5.1/PowerShell 7): 26 passed, 0 failed, 0 skipped in each. Eight
one-statement production mutations across all seven cmdlets were applied in
an isolated worktree, built, and run against this file; each was detected by
at least one new test, then the source was restored exactly and rebuilt.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
3 weeks ago
Context 'SD parameter set' {
It 'Should filter a descriptor''s orphaned entries by -Account, report both orphaned Sids by default, exclude them all with -ExcludeExplicit, and read two descriptors bound through the pipeline' {
$file = New-TestSandboxItem -Sandbox $sandbox -Name 'OrphanedSdFile'
Add-NTFSAccess -Path $file -Account 'S-1-5-21-1-2-3-9201' -AccessRights ReadData -ErrorAction Stop
Add-NTFSAccess -Path $file -Account 'S-1-5-21-1-2-3-9202' -AccessRights ReadData -ErrorAction Stop
$sd = Get-NTFSSecurityDescriptor -Path $file -ErrorAction Stop
$filtered = @ ( Get-NTFSOrphanedAccess -SecurityDescriptor $sd -Account 'S-1-5-21-1-2-3-9202' -ErrorAction Stop )
$default = @ ( Get-NTFSOrphanedAccess -SecurityDescriptor $sd -ErrorAction Stop )
$excludeExplicit = @ ( Get-NTFSOrphanedAccess -SecurityDescriptor $sd -ExcludeExplicit -ErrorAction Stop )
$filtered | Should -HaveCount 1
" $( $filtered [ 0 ] . Account . Sid ) " | Should -Be 'S-1-5-21-1-2-3-9202'
( $default | ForEach-Object -Process { " $( $_ . Account . Sid ) " } | Sort-Object ) -join ',' | Should -Be ( ( 'S-1-5-21-1-2-3-9201' , 'S-1-5-21-1-2-3-9202' | Sort-Object ) -join ',' )
$excludeExplicit | Should -BeNullOrEmpty
$file2 = New-TestSandboxItem -Sandbox $sandbox -Name 'OrphanedSdFile2'
Add-NTFSAccess -Path $file2 -Account 'S-1-5-21-1-2-3-9203' -AccessRights ReadData -ErrorAction Stop
$piped = @ ( Get-NTFSSecurityDescriptor -Path $file , $file2 -ErrorAction Stop | Get-NTFSOrphanedAccess -ErrorAction Stop )
@ ( $piped | Where-Object -FilterScript { $_ . FullName -eq $file } ) | Should -HaveCount 2
@ ( $piped | Where-Object -FilterScript { $_ . FullName -eq $file2 } ) | Should -HaveCount 1
}
}
test(access): close budget parameter-set gaps for the access cmdlets
Add 20 new It blocks (32 executed cases once the 13-scope -ForEach of
Add-NTFSAccess/SDSimple expands) to ParameterSets.Access.Tests.ps1, closing
every REMAINING item that worklist-access.md marks budget for rows 1-13:
- Add-NTFSAccess: several accounts and positional binding (PathComplex); a
Deny entry's exact rights with no auto-added Synchronize (PathComplex and
PathSimple); a denied write and a Deny entry without rights through
PathSimple; all 13 -AppliesTo scopes of a security descriptor read back
with .NET, piped and several (SDSimple); piped and several descriptors
asserted for Add itself, not only as a fixture (SDComplex).
- Remove-NTFSAccess: the -PassThru output's count against the complete
survivor list for several piped paths; several accounts and positional
binding (PathComplex); a Deny entry's rights-reduction without
-RemoveSpecific; a missing and a denied path, piped, several (PathSimple);
rights-reduction without -RemoveSpecific and disk independence until
Set-NTFSSecurityDescriptor, piped and several descriptors (SDSimple); the
on-disk write-through only after Set-NTFSSecurityDescriptor, piped and
several descriptors (SDComplex).
- Get-NTFSAccess: what the cmdlet returns without -Path, asserted on an
actual emitted object's type (Path).
- Get-NTFSOrphanedAccess: the category of ReadSecurityError after a failed
ownership retry (Path); -ExcludeInherited and an entry added to a
descriptor only in memory (SD).
- Get-NTFSSimpleAccess: the category and target of a missing path's
ReadError (Path).
- Get-NTFSEffectiveAccess: one call combining a failing and a succeeding
path (Path).
Items the REASON marks MaintainerDecision, Limit, or would-pin are left
untouched, per the group notes of the brief. No product code changed.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
2 weeks ago
Context 'SD parameter set, -ExcludeInherited and an in-memory change' {
It 'Should return the explicit entry with -ExcludeInherited and still report an entry added to the descriptor only in memory' {
$file = New-TestSandboxItem -Sandbox $sandbox -Name 'OrphanedSdMemory'
Add-NTFSAccess -Path $file -Account 'S-1-5-21-1-2-3-9401' -AccessRights ReadData -ErrorAction Stop
$sd = Get-NTFSSecurityDescriptor -Path $file -ErrorAction Stop
$explicitOnly = @ ( Get-NTFSOrphanedAccess -SecurityDescriptor $sd -ExcludeInherited -ErrorAction Stop )
$explicitOnly | Should -HaveCount 1
" $( $explicitOnly [ 0 ] . Account . Sid ) " | Should -Be 'S-1-5-21-1-2-3-9401'
Add-NTFSAccess -SecurityDescriptor $sd -Account 'S-1-5-21-1-2-3-9402' -AccessRights ReadData -ErrorAction Stop
$afterMemoryChange = @ ( Get-NTFSOrphanedAccess -SecurityDescriptor $sd -ErrorAction Stop )
( $afterMemoryChange | ForEach-Object -Process { " $( $_ . Account . Sid ) " } | Sort-Object ) -join ',' |
Should -Be ( ( 'S-1-5-21-1-2-3-9401' , 'S-1-5-21-1-2-3-9402' | Sort-Object ) -join ',' )
# The disk copy stays as it was: only the descriptor in memory saw the second account.
@ ( Get-NTFSOrphanedAccess -Path $file -ErrorAction Stop ) | Should -HaveCount 1
}
}
test(access): assert the parameter sets that the judged matrix found untested
Adds Tests/ParameterSets.Access.Tests.ps1, closing the TestNeeded rows of
paramsets/judged-access.csv for Add-NTFSAccess, Remove-NTFSAccess,
Clear-NTFSAccess, Get-NTFSAccess, Get-NTFSOrphanedAccess,
Get-NTFSSimpleAccess, and Get-NTFSEffectiveAccess: 20 new It blocks (26
executed cases) covering pipeline binding (ValueFromPipeline and
ValueFromPipelineByPropertyName, including positional file/folder objects),
-PassThru combined with one denied and one succeeding path, error
ErrorId/category/target for read and write failures, continuation after a
per-item error, state read back with .NET (not -PassThru/Get-NTFSAccess),
SecurityDescriptor-set pipelines and memory-vs-disk behavior, and the
declared output types and PathSimple/SDSimple vs PathComplex/SDComplex
parameter-set boundaries. No row with GapKind=MaintainerDecision is pinned:
the -SecurityDescriptor sets of Add/Remove keep their open per-item error
behavior untested either way.
Finding: a plain piped object with an AppliesTo property does not steer
Add-NTFSAccess/Remove-NTFSAccess to the PathSimple/SDSimple parameter set by
itself; PowerShell resolves the ambiguity to the default set (PathComplex/
SDComplex) and silently drops the unmatched property. The proposed test for
that exact shape (notes section (b), item 2c) could not be written as
specified; the pipeline test for PathSimple instead binds -Path through the
pipeline while -AppliesTo is named explicitly, which does select the set.
Validated in all four configurations (elevated/basic user, Windows
PowerShell 5.1/PowerShell 7): 26 passed, 0 failed, 0 skipped in each. Eight
one-statement production mutations across all seven cmdlets were applied in
an isolated worktree, built, and run against this file; each was detected by
at least one new test, then the source was restored exactly and rebuilt.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
3 weeks ago
}
Describe 'Get-NTFSSimpleAccess' {
Context 'Path parameter set, unreadable folder' {
BeforeAll {
# Without this, an elevated session auto-enables the Backup privilege in BeginProcessing (every
# BaseCmdletWithPrivControl cmdlet does, see EnableFileSystemPrivileges) and that alone, with no retry
# logic in this cmdlet, bypasses the owner-rights read denial below; forcing it off reproduces the same
# denial as a basic user, like PathErrors.Tests.ps1 does for its own read-denial cases.
$privateData [ 'EnablePrivileges' ] = $false
}
AfterAll {
$privateData [ 'EnablePrivileges' ] = $enablePrivileges
}
It 'Should report an unreadable folder''s error by category and target and still report the next folder''s entries' {
$blocked = New-TestSandboxItem -Sandbox $sandbox -Name 'SimpleBlocked' -Directory
Block-TestReadPermission -Sandbox $sandbox -Path $blocked
$next = New-TestSandboxItem -Sandbox $sandbox -Name 'SimpleNext' -Directory
Add-NTFSAccess -Path $next -Account 'S-1-1-0' -AccessRights ReadData -ErrorAction Stop
$result = @ ( Get-NTFSSimpleAccess -Path $blocked , $next -IncludeRootFolder : $false -ErrorVariable simpleErrors -ErrorAction SilentlyContinue )
$simpleErrors | Should -HaveCount 1
$simpleErrors [ 0 ] . FullyQualifiedErrorId | Should -BeLike 'ReadError,*'
$simpleErrors [ 0 ] . CategoryInfo . Category | Should -Be 'OpenError'
$simpleErrors [ 0 ] . TargetObject | Should -Be $blocked
@ ( $result | Where-Object -FilterScript { $_ . FullName -eq $blocked } ) | Should -BeNullOrEmpty
@ ( $result | Where-Object -FilterScript { $_ . FullName -eq $next } ) | Should -Not -BeNullOrEmpty
}
}
test(access): close budget parameter-set gaps for the access cmdlets
Add 20 new It blocks (32 executed cases once the 13-scope -ForEach of
Add-NTFSAccess/SDSimple expands) to ParameterSets.Access.Tests.ps1, closing
every REMAINING item that worklist-access.md marks budget for rows 1-13:
- Add-NTFSAccess: several accounts and positional binding (PathComplex); a
Deny entry's exact rights with no auto-added Synchronize (PathComplex and
PathSimple); a denied write and a Deny entry without rights through
PathSimple; all 13 -AppliesTo scopes of a security descriptor read back
with .NET, piped and several (SDSimple); piped and several descriptors
asserted for Add itself, not only as a fixture (SDComplex).
- Remove-NTFSAccess: the -PassThru output's count against the complete
survivor list for several piped paths; several accounts and positional
binding (PathComplex); a Deny entry's rights-reduction without
-RemoveSpecific; a missing and a denied path, piped, several (PathSimple);
rights-reduction without -RemoveSpecific and disk independence until
Set-NTFSSecurityDescriptor, piped and several descriptors (SDSimple); the
on-disk write-through only after Set-NTFSSecurityDescriptor, piped and
several descriptors (SDComplex).
- Get-NTFSAccess: what the cmdlet returns without -Path, asserted on an
actual emitted object's type (Path).
- Get-NTFSOrphanedAccess: the category of ReadSecurityError after a failed
ownership retry (Path); -ExcludeInherited and an entry added to a
descriptor only in memory (SD).
- Get-NTFSSimpleAccess: the category and target of a missing path's
ReadError (Path).
- Get-NTFSEffectiveAccess: one call combining a failing and a succeeding
path (Path).
Items the REASON marks MaintainerDecision, Limit, or would-pin are left
untouched, per the group notes of the brief. No product code changed.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
2 weeks ago
Context 'Path parameter set, missing path' {
# Unlike the unreadable-folder case above, a missing path needs no retry and no privilege change: this cmdlet
# does not retry as the owner, so the ReadError of a missing path was pinned by ErrorId only until now.
It 'Should report a missing path''s error by category and target and still report the next folder''s entries' {
$missing = Join-Path -Path $sandbox -ChildPath ( 'SimpleMissing-{0}' -f [guid] :: NewGuid ( ) . ToString ( 'N' ) . Substring ( 0 , 8 ) )
Assert-TestSandboxPath -Sandbox $sandbox -Path $missing
$next = New-TestSandboxItem -Sandbox $sandbox -Name 'SimpleMissingNext' -Directory
Add-NTFSAccess -Path $next -Account 'S-1-1-0' -AccessRights ReadData -ErrorAction Stop
$result = @ ( Get-NTFSSimpleAccess -Path $missing , $next -IncludeRootFolder : $false -ErrorVariable simpleErrors -ErrorAction SilentlyContinue )
$simpleErrors | Should -HaveCount 1
$simpleErrors [ 0 ] . FullyQualifiedErrorId | Should -BeLike 'ReadError,*'
$simpleErrors [ 0 ] . CategoryInfo . Category | Should -Be 'OpenError'
$simpleErrors [ 0 ] . TargetObject | Should -Be $missing
@ ( $result | Where-Object -FilterScript { $_ . FullName -eq $next } ) | Should -Not -BeNullOrEmpty
}
}
test(access): assert the parameter sets that the judged matrix found untested
Adds Tests/ParameterSets.Access.Tests.ps1, closing the TestNeeded rows of
paramsets/judged-access.csv for Add-NTFSAccess, Remove-NTFSAccess,
Clear-NTFSAccess, Get-NTFSAccess, Get-NTFSOrphanedAccess,
Get-NTFSSimpleAccess, and Get-NTFSEffectiveAccess: 20 new It blocks (26
executed cases) covering pipeline binding (ValueFromPipeline and
ValueFromPipelineByPropertyName, including positional file/folder objects),
-PassThru combined with one denied and one succeeding path, error
ErrorId/category/target for read and write failures, continuation after a
per-item error, state read back with .NET (not -PassThru/Get-NTFSAccess),
SecurityDescriptor-set pipelines and memory-vs-disk behavior, and the
declared output types and PathSimple/SDSimple vs PathComplex/SDComplex
parameter-set boundaries. No row with GapKind=MaintainerDecision is pinned:
the -SecurityDescriptor sets of Add/Remove keep their open per-item error
behavior untested either way.
Finding: a plain piped object with an AppliesTo property does not steer
Add-NTFSAccess/Remove-NTFSAccess to the PathSimple/SDSimple parameter set by
itself; PowerShell resolves the ambiguity to the default set (PathComplex/
SDComplex) and silently drops the unmatched property. The proposed test for
that exact shape (notes section (b), item 2c) could not be written as
specified; the pipeline test for PathSimple instead binds -Path through the
pipeline while -AppliesTo is named explicitly, which does select the set.
Validated in all four configurations (elevated/basic user, Windows
PowerShell 5.1/PowerShell 7): 26 passed, 0 failed, 0 skipped in each. Eight
one-statement production mutations across all seven cmdlets were applied in
an isolated worktree, built, and run against this file; each was detected by
at least one new test, then the source was restored exactly and rebuilt.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
3 weeks ago
Context 'SD parameter set' {
It 'Should filter and partition a descriptor''s entries like the equivalent -Path call, and read two descriptors bound through the pipeline' {
$parent = New-TestSandboxItem -Sandbox $sandbox -Name 'SimpleSdParent' -Directory
Add-NTFSAccess -Path $parent -Account 'S-1-5-32-546' -AccessRights ReadData -AppliesTo ThisFolderSubfoldersAndFiles -ErrorAction Stop
$child = Join-Path -Path $parent -ChildPath 'Child'
Assert-TestSandboxPath -Sandbox $sandbox -Path $child
New-Item -ItemType Directory -Path $child | Out-Null
Add-NTFSAccess -Path $child -Account 'S-1-1-0' -AccessRights ReadData -ErrorAction Stop
$sd = Get-NTFSSecurityDescriptor -Path $child -ErrorAction Stop
$bySd = @ ( Get-NTFSSimpleAccess -SecurityDescriptor $sd -ErrorAction Stop )
$byPath = @ ( Get-NTFSSimpleAccess -Path $child -IncludeRootFolder : $false -ErrorAction Stop )
( ( $bySd | ForEach-Object -Process { '{0}|{1}|{2}' -f $_ . Identity . Sid , $_ . AccessRights , $_ . AccessControlType } ) | Sort-Object ) -join ';' |
Should -Be ( ( ( $byPath | ForEach-Object -Process { '{0}|{1}|{2}' -f $_ . Identity . Sid , $_ . AccessRights , $_ . AccessControlType } ) | Sort-Object ) -join ';' )
$filtered = @ ( Get-NTFSSimpleAccess -SecurityDescriptor $sd -Account 'S-1-1-0' -ErrorAction Stop )
$filtered | Should -HaveCount 1
$explicitOnly = @ ( Get-NTFSSimpleAccess -SecurityDescriptor $sd -ExcludeInherited -ErrorAction Stop )
$inheritedOnly = @ ( Get-NTFSSimpleAccess -SecurityDescriptor $sd -ExcludeExplicit -ErrorAction Stop )
( $explicitOnly . Count + $inheritedOnly . Count ) | Should -Be $bySd . Count
@ ( $explicitOnly | Where-Object -FilterScript { $_ . Identity . Sid -eq 'S-1-1-0' } ) | Should -HaveCount 1
@ ( $inheritedOnly | Where-Object -FilterScript { $_ . Identity . Sid -eq 'S-1-5-32-546' } ) | Should -HaveCount 1
$piped = @ ( Get-NTFSSecurityDescriptor -Path $child , $parent -ErrorAction Stop | Get-NTFSSimpleAccess -ErrorAction Stop )
@ ( $piped | Where-Object -FilterScript { $_ . FullName -eq $child } ) | Should -HaveCount $bySd . Count
@ ( $piped | Where-Object -FilterScript { $_ . FullName -eq $parent } ) | Should -Not -BeNullOrEmpty
}
}
}
Describe 'Get-NTFSEffectiveAccess' {
Context 'Path parameter set' {
It 'Should report an allowed right as present and a denied right as absent for a protected DACL' {
$file = New-TestSandboxItem -Sandbox $sandbox -Name 'EffectiveRights'
Disable-NTFSAccessInheritance -Path $file -RemoveInheritedAccessRules -ErrorAction Stop
Add-NTFSAccess -Path $file -Account $currentUser -AccessRights ReadData -ErrorAction Stop
Add-NTFSAccess -Path $file -Account $currentUser -AccessRights WriteData -AccessType Deny -ErrorAction Stop
$result = Get-NTFSEffectiveAccess -Path $file -WarningAction SilentlyContinue -ErrorAction Stop
$result . AccessRights . HasFlag ( [Security2.FileSystemRights2] :: ReadData ) | Should -BeTrue
$result . AccessRights . HasFlag ( [Security2.FileSystemRights2] :: WriteData ) | Should -BeFalse
}
It 'Should return one result per item bound through the pipeline, in order' {
$first = New-TestSandboxItem -Sandbox $sandbox -Name 'EffectivePipeFirst'
$second = New-TestSandboxItem -Sandbox $sandbox -Name 'EffectivePipeSecond'
$result = @ ( Get-Item2 -Path $first , $second | Get-NTFSEffectiveAccess -Account 'S-1-5-32-546' -WarningAction SilentlyContinue -ErrorAction Stop )
$result | Should -HaveCount 2
$result [ 0 ] . FullName | Should -Be $first
$result [ 1 ] . FullName | Should -Be $second
}
test(access): close budget parameter-set gaps for the access cmdlets
Add 20 new It blocks (32 executed cases once the 13-scope -ForEach of
Add-NTFSAccess/SDSimple expands) to ParameterSets.Access.Tests.ps1, closing
every REMAINING item that worklist-access.md marks budget for rows 1-13:
- Add-NTFSAccess: several accounts and positional binding (PathComplex); a
Deny entry's exact rights with no auto-added Synchronize (PathComplex and
PathSimple); a denied write and a Deny entry without rights through
PathSimple; all 13 -AppliesTo scopes of a security descriptor read back
with .NET, piped and several (SDSimple); piped and several descriptors
asserted for Add itself, not only as a fixture (SDComplex).
- Remove-NTFSAccess: the -PassThru output's count against the complete
survivor list for several piped paths; several accounts and positional
binding (PathComplex); a Deny entry's rights-reduction without
-RemoveSpecific; a missing and a denied path, piped, several (PathSimple);
rights-reduction without -RemoveSpecific and disk independence until
Set-NTFSSecurityDescriptor, piped and several descriptors (SDSimple); the
on-disk write-through only after Set-NTFSSecurityDescriptor, piped and
several descriptors (SDComplex).
- Get-NTFSAccess: what the cmdlet returns without -Path, asserted on an
actual emitted object's type (Path).
- Get-NTFSOrphanedAccess: the category of ReadSecurityError after a failed
ownership retry (Path); -ExcludeInherited and an entry added to a
descriptor only in memory (SD).
- Get-NTFSSimpleAccess: the category and target of a missing path's
ReadError (Path).
- Get-NTFSEffectiveAccess: one call combining a failing and a succeeding
path (Path).
Items the REASON marks MaintainerDecision, Limit, or would-pin are left
untouched, per the group notes of the brief. No product code changed.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
2 weeks ago
It 'Should write nothing for a missing path and only the succeeding path''s result in the same call' {
$missing = Join-Path -Path $sandbox -ChildPath ( 'EffectiveMissing-{0}' -f [guid] :: NewGuid ( ) . ToString ( 'N' ) . Substring ( 0 , 8 ) )
Assert-TestSandboxPath -Sandbox $sandbox -Path $missing
$next = New-TestSandboxItem -Sandbox $sandbox -Name 'EffectiveMissingNext'
$result = @ ( Get-NTFSEffectiveAccess -Path $missing , $next -Account 'S-1-1-0' -WarningAction SilentlyContinue -ErrorVariable accessErrors -ErrorAction SilentlyContinue )
$accessErrors | Should -HaveCount 1
$accessErrors [ 0 ] . FullyQualifiedErrorId | Should -BeLike 'ReadFileError,*'
$accessErrors [ 0 ] . CategoryInfo . Category | Should -Be 'OpenError'
$accessErrors [ 0 ] . TargetObject | Should -Be $missing
$result | Should -HaveCount 1
$result [ 0 ] . FullName | Should -Be $next
}
test(access): assert the parameter sets that the judged matrix found untested
Adds Tests/ParameterSets.Access.Tests.ps1, closing the TestNeeded rows of
paramsets/judged-access.csv for Add-NTFSAccess, Remove-NTFSAccess,
Clear-NTFSAccess, Get-NTFSAccess, Get-NTFSOrphanedAccess,
Get-NTFSSimpleAccess, and Get-NTFSEffectiveAccess: 20 new It blocks (26
executed cases) covering pipeline binding (ValueFromPipeline and
ValueFromPipelineByPropertyName, including positional file/folder objects),
-PassThru combined with one denied and one succeeding path, error
ErrorId/category/target for read and write failures, continuation after a
per-item error, state read back with .NET (not -PassThru/Get-NTFSAccess),
SecurityDescriptor-set pipelines and memory-vs-disk behavior, and the
declared output types and PathSimple/SDSimple vs PathComplex/SDComplex
parameter-set boundaries. No row with GapKind=MaintainerDecision is pinned:
the -SecurityDescriptor sets of Add/Remove keep their open per-item error
behavior untested either way.
Finding: a plain piped object with an AppliesTo property does not steer
Add-NTFSAccess/Remove-NTFSAccess to the PathSimple/SDSimple parameter set by
itself; PowerShell resolves the ambiguity to the default set (PathComplex/
SDComplex) and silently drops the unmatched property. The proposed test for
that exact shape (notes section (b), item 2c) could not be written as
specified; the pipeline test for PathSimple instead binds -Path through the
pipeline while -AppliesTo is named explicitly, which does select the set.
Validated in all four configurations (elevated/basic user, Windows
PowerShell 5.1/PowerShell 7): 26 passed, 0 failed, 0 skipped in each. Eight
one-statement production mutations across all seven cmdlets were applied in
an isolated worktree, built, and run against this file; each was detected by
at least one new test, then the source was restored exactly and rebuilt.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
3 weeks ago
}
Context 'SecurityDescriptor parameter set' {
It 'Should follow a descriptor changed only in memory while the -Path result stays on disk, and read two descriptors bound through the pipeline' {
$file = New-TestSandboxItem -Sandbox $sandbox -Name 'EffectiveSdFile'
Disable-NTFSAccessInheritance -Path $file -RemoveInheritedAccessRules -ErrorAction Stop
Add-NTFSAccess -Path $file -Account $currentUser -AccessRights FullControl -ErrorAction Stop
$sd = Get-NTFSSecurityDescriptor -Path $file -ErrorAction Stop
$pathBefore = Get-NTFSEffectiveAccess -Path $file -WarningAction SilentlyContinue -ErrorAction Stop
$sdBefore = Get-NTFSEffectiveAccess -SecurityDescriptor $sd -WarningAction SilentlyContinue -ErrorAction Stop
$pathBefore . AccessRights . HasFlag ( [Security2.FileSystemRights2] :: WriteData ) | Should -BeTrue
$sdBefore . AccessRights . HasFlag ( [Security2.FileSystemRights2] :: WriteData ) | Should -BeTrue
Add-NTFSAccess -SecurityDescriptor $sd -Account $currentUser -AccessType Deny -AccessRights WriteData -ErrorAction Stop
$sdAfter = Get-NTFSEffectiveAccess -SecurityDescriptor $sd -WarningAction SilentlyContinue -ErrorAction Stop
$pathAfter = Get-NTFSEffectiveAccess -Path $file -WarningAction SilentlyContinue -ErrorAction Stop
$sdAfter . AccessRights . HasFlag ( [Security2.FileSystemRights2] :: WriteData ) | Should -BeFalse
$pathAfter . AccessRights . HasFlag ( [Security2.FileSystemRights2] :: WriteData ) | Should -BeTrue
$file2 = New-TestSandboxItem -Sandbox $sandbox -Name 'EffectiveSdFile2'
Disable-NTFSAccessInheritance -Path $file2 -RemoveInheritedAccessRules -ErrorAction Stop
Add-NTFSAccess -Path $file2 -Account $currentUser -AccessRights FullControl -ErrorAction Stop
$piped = @ ( Get-NTFSSecurityDescriptor -Path $file , $file2 -ErrorAction Stop | Get-NTFSEffectiveAccess -WarningAction SilentlyContinue -ErrorAction Stop )
$piped | Should -HaveCount 2
}
}
}
fix(access): report a piped AccessType, AuditFlags, flags, or scope that isn't valid
Add-NTFSAccess, Remove-NTFSAccess, Add-NTFSAudit, and Remove-NTFSAudit
silently kept the default value of AccessType, AuditFlags,
InheritanceFlags, PropagationFlags, and AppliesTo when a piped object's
property could not be converted to the parameter's type: a misspelled
AccessType such as "Denyy", or a blank CSV cell, created an Allow entry
with no message. For a permissions tool, a typo for Deny that silently
grants access is a security-relevant defect (Decision 27).
The five parameters are now declared as object and converted inside
the cmdlet, the way the PowerShell binder converts a typed parameter
(names in any case, comma-separated flag lists, numbers, enum
instances). A value that cannot be converted, including a blank one,
is now an error named InvalidParameterValue: terminating and
catchable when it is named on the command line, exactly as the binder
reported it before; a non-terminating error targeting that path or
descriptor when it comes from a piped object, which leaves that item
unchanged and lets the pipeline continue with the next one. A $null
value is still not an error and keeps the parameter's default. Tab
completion of the five parameters keeps working through explicit
argument completers. Choosing between the Simple and Complex
parameter sets is unaffected and still rejects AppliesTo together
with InheritanceFlags or PropagationFlags as AmbiguousParameterSet,
for every mix of string and enum values.
The four cmdlet pages document the parameters as Type: Object, list
the valid values, and describe the new error; a sentence that said a
piped AppliesTo typo or blank cell was silently ignored is corrected.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
1 week ago
# Decision 27 (fix B): AccessType, InheritanceFlags, PropagationFlags, and AppliesTo of Add-NTFSAccess and
# Remove-NTFSAccess are object parameters, converted in the cmdlet with LanguagePrimitives.ConvertTo, so that a
# piped value the binder cannot convert to the enumeration is an error instead of a silently kept default.
Describe 'Invalid and valid values of AccessType, InheritanceFlags, PropagationFlags, and AppliesTo' {
BeforeDiscovery {
fix(access): end a collection that holds itself and name the typo in the message of a list
The check that rejects a list for -AccessType and -AppliesTo opened every level
of a collection of one element, so that a collection that holds itself never
returned. It opens at most 32 levels now and counts a deeper nesting as a list,
and it stops reading a collection after the second element, so that a long or
endless sequence is not read to its end.
The message names what the caller wrote: the one element of a collection of one
element (also when that element is a collection itself), the first ten elements
of a longer collection followed by "...", and otherwise the text of the value.
Before, a collection of one misspelled name was named by its type.
Tests, written first: 28 rows failed on the previous build in all four
configurations and none fails now. They cover an array of one misspelled name,
a collection nested 40 levels deep for both parameters of both cmdlets, and an
array that holds an array of one name, which stays valid. The assertion of the
value text expects "The value '<text>'", because the old pattern could also
match the name of a type. The comments say that PowerShell combines the numbers
of a list bit by bit, which is what it does, and not that it adds them.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
1 week ago
# A collection nested 40 levels deep is a list too: the conversion opens at most 32 levels, which also ends a
# collection that holds itself (a test with such a collection would not end if the limit were lost).
$accessDeepAccessType = 'Deny'
$accessDeepScope = 'FilesOnly'
foreach ( $level in 1 . . 40 ) {
$accessDeepAccessType = , $accessDeepAccessType
$accessDeepScope = , $accessDeepScope
}
fix(access): build the message of a bad parameter value even when the text of the value throws
The message of the error is built inside the catch block of the conversion, and
Describe formatted the one element of a collection of one element outside of
any guard. An element whose ToString throws (a script method, for example)
ended the command with a terminating ExtendedTypeSystemException instead of
the InvalidParameterValue error for that item, and a piped command stopped at
that object, where the pages say that the error is reported for just that
item. Describe now falls back to the type name whatever the text or the
enumeration of the value does, for the five parameters of the four cmdlets
alike.
Tests, written first: 24 rows failed on the previous build in all four
configurations and none fails now (an array of one object whose ToString
throws, piped for every parameter and named for one parameter of each kind).
New rows also pin what the previous commit claimed but did not test: the
message names the first ten elements of a list and ends with an ellipsis from
the eleventh, and a sequence of 100,000 elements is read only up to its first
elements (a counting sequence, named and piped). A build without the two limits
fails 4 of them and a build with a limit of nine fails 6, in all four
configurations. The comment of the nesting limit now says exactly what it
counts.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
1 week ago
# The message of the error names the value, and building it must not fail when the text of an element does.
$accessThrowingElement = [pscustomobject] @ { Unrelated = 1 }
$accessThrowingElement | Add-Member -MemberType ScriptMethod -Name ToString -Value { throw 'ToString throws' } -Force
fix(access): report a piped AccessType, AuditFlags, flags, or scope that isn't valid
Add-NTFSAccess, Remove-NTFSAccess, Add-NTFSAudit, and Remove-NTFSAudit
silently kept the default value of AccessType, AuditFlags,
InheritanceFlags, PropagationFlags, and AppliesTo when a piped object's
property could not be converted to the parameter's type: a misspelled
AccessType such as "Denyy", or a blank CSV cell, created an Allow entry
with no message. For a permissions tool, a typo for Deny that silently
grants access is a security-relevant defect (Decision 27).
The five parameters are now declared as object and converted inside
the cmdlet, the way the PowerShell binder converts a typed parameter
(names in any case, comma-separated flag lists, numbers, enum
instances). A value that cannot be converted, including a blank one,
is now an error named InvalidParameterValue: terminating and
catchable when it is named on the command line, exactly as the binder
reported it before; a non-terminating error targeting that path or
descriptor when it comes from a piped object, which leaves that item
unchanged and lets the pipeline continue with the next one. A $null
value is still not an error and keeps the parameter's default. Tab
completion of the five parameters keeps working through explicit
argument completers. Choosing between the Simple and Complex
parameter sets is unaffected and still rejects AppliesTo together
with InheritanceFlags or PropagationFlags as AmbiguousParameterSet,
for every mix of string and enum values.
The four cmdlet pages document the parameters as Type: Object, list
the valid values, and describe the new error; a sentence that said a
piped AppliesTo typo or blank cell was silently ignored is corrected.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
1 week ago
$accessBadShapes = @ (
@ { Shape = 'a misspelled name' ; Value = 'Typoo' ; ValueText = 'Typoo' }
@ { Shape = 'a blank string' ; Value = '' ; ValueText = $null }
@ { Shape = 'a white-space string' ; Value = ' ' ; ValueText = $null }
@ { Shape = 'an undefined number' ; Value = 99 ; ValueText = '99' }
@ { Shape = 'a hashtable' ; Value = @ { Unrelated = 1 } ; ValueText = $null }
fix(access): end a collection that holds itself and name the typo in the message of a list
The check that rejects a list for -AccessType and -AppliesTo opened every level
of a collection of one element, so that a collection that holds itself never
returned. It opens at most 32 levels now and counts a deeper nesting as a list,
and it stops reading a collection after the second element, so that a long or
endless sequence is not read to its end.
The message names what the caller wrote: the one element of a collection of one
element (also when that element is a collection itself), the first ten elements
of a longer collection followed by "...", and otherwise the text of the value.
Before, a collection of one misspelled name was named by its type.
Tests, written first: 28 rows failed on the previous build in all four
configurations and none fails now. They cover an array of one misspelled name,
a collection nested 40 levels deep for both parameters of both cmdlets, and an
array that holds an array of one name, which stays valid. The assertion of the
value text expects "The value '<text>'", because the old pattern could also
match the name of a type. The comments say that PowerShell combines the numbers
of a list bit by bit, which is what it does, and not that it adds them.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
1 week ago
@ { Shape = 'an array of one misspelled name' ; Value = @ ( 'Typoo' ) ; ValueText = 'Typoo' }
fix(access): build the message of a bad parameter value even when the text of the value throws
The message of the error is built inside the catch block of the conversion, and
Describe formatted the one element of a collection of one element outside of
any guard. An element whose ToString throws (a script method, for example)
ended the command with a terminating ExtendedTypeSystemException instead of
the InvalidParameterValue error for that item, and a piped command stopped at
that object, where the pages say that the error is reported for just that
item. Describe now falls back to the type name whatever the text or the
enumeration of the value does, for the five parameters of the four cmdlets
alike.
Tests, written first: 24 rows failed on the previous build in all four
configurations and none fails now (an array of one object whose ToString
throws, piped for every parameter and named for one parameter of each kind).
New rows also pin what the previous commit claimed but did not test: the
message names the first ten elements of a list and ends with an ellipsis from
the eleventh, and a sequence of 100,000 elements is read only up to its first
elements (a counting sequence, named and piped). A build without the two limits
fails 4 of them and a build with a limit of nine fails 6, in all four
configurations. The comment of the nesting limit now says exactly what it
counts.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
1 week ago
@ { Shape = 'an array of one object whose ToString throws' ; Value = @ ( $accessThrowingElement ) ; ValueText = $null }
fix(access): report a piped AccessType, AuditFlags, flags, or scope that isn't valid
Add-NTFSAccess, Remove-NTFSAccess, Add-NTFSAudit, and Remove-NTFSAudit
silently kept the default value of AccessType, AuditFlags,
InheritanceFlags, PropagationFlags, and AppliesTo when a piped object's
property could not be converted to the parameter's type: a misspelled
AccessType such as "Denyy", or a blank CSV cell, created an Allow entry
with no message. For a permissions tool, a typo for Deny that silently
grants access is a security-relevant defect (Decision 27).
The five parameters are now declared as object and converted inside
the cmdlet, the way the PowerShell binder converts a typed parameter
(names in any case, comma-separated flag lists, numbers, enum
instances). A value that cannot be converted, including a blank one,
is now an error named InvalidParameterValue: terminating and
catchable when it is named on the command line, exactly as the binder
reported it before; a non-terminating error targeting that path or
descriptor when it comes from a piped object, which leaves that item
unchanged and lets the pipeline continue with the next one. A $null
value is still not an error and keeps the parameter's default. Tab
completion of the five parameters keeps working through explicit
argument completers. Choosing between the Simple and Complex
parameter sets is unaffected and still rejects AppliesTo together
with InheritanceFlags or PropagationFlags as AmbiguousParameterSet,
for every mix of string and enum values.
The four cmdlet pages document the parameters as Type: Object, list
the valid values, and describe the new error; a sentence that said a
piped AppliesTo typo or blank cell was silently ignored is corrected.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
1 week ago
)
$accessTypeBadShapes = @ (
@ { Shape = 'a misspelled name' ; Value = 'Denyy' ; ValueText = 'Denyy' }
@ { Shape = 'a blank string' ; Value = '' ; ValueText = $null }
@ { Shape = 'a white-space string' ; Value = ' ' ; ValueText = $null }
@ { Shape = 'an undefined number' ; Value = 99 ; ValueText = '99' }
@ { Shape = 'a hashtable' ; Value = @ { Unrelated = 1 } ; ValueText = $null }
fix(access): end a collection that holds itself and name the typo in the message of a list
The check that rejects a list for -AccessType and -AppliesTo opened every level
of a collection of one element, so that a collection that holds itself never
returned. It opens at most 32 levels now and counts a deeper nesting as a list,
and it stops reading a collection after the second element, so that a long or
endless sequence is not read to its end.
The message names what the caller wrote: the one element of a collection of one
element (also when that element is a collection itself), the first ten elements
of a longer collection followed by "...", and otherwise the text of the value.
Before, a collection of one misspelled name was named by its type.
Tests, written first: 28 rows failed on the previous build in all four
configurations and none fails now. They cover an array of one misspelled name,
a collection nested 40 levels deep for both parameters of both cmdlets, and an
array that holds an array of one name, which stays valid. The assertion of the
value text expects "The value '<text>'", because the old pattern could also
match the name of a type. The comments say that PowerShell combines the numbers
of a list bit by bit, which is what it does, and not that it adds them.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
1 week ago
@ { Shape = 'an array of one misspelled name' ; Value = @ ( 'Denyy' ) ; ValueText = 'Denyy' }
fix(access): build the message of a bad parameter value even when the text of the value throws
The message of the error is built inside the catch block of the conversion, and
Describe formatted the one element of a collection of one element outside of
any guard. An element whose ToString throws (a script method, for example)
ended the command with a terminating ExtendedTypeSystemException instead of
the InvalidParameterValue error for that item, and a piped command stopped at
that object, where the pages say that the error is reported for just that
item. Describe now falls back to the type name whatever the text or the
enumeration of the value does, for the five parameters of the four cmdlets
alike.
Tests, written first: 24 rows failed on the previous build in all four
configurations and none fails now (an array of one object whose ToString
throws, piped for every parameter and named for one parameter of each kind).
New rows also pin what the previous commit claimed but did not test: the
message names the first ten elements of a list and ends with an ellipsis from
the eleventh, and a sequence of 100,000 elements is read only up to its first
elements (a counting sequence, named and piped). A build without the two limits
fails 4 of them and a build with a limit of nine fails 6, in all four
configurations. The comment of the nesting limit now says exactly what it
counts.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
1 week ago
@ { Shape = 'an array of one object whose ToString throws' ; Value = @ ( $accessThrowingElement ) ; ValueText = $null }
fix(access): reject a list for -AccessType and -AppliesTo, and complete in capitals in Turkish
-AccessType and -AppliesTo of Add-NTFSAccess, Remove-NTFSAccess,
Add-NTFSAudit (-AppliesTo), and Remove-NTFSAudit (-AppliesTo) take one value,
but PowerShell adds up the numbers of a comma list or an array for an
enumeration, so that -AppliesTo 'ThisFolderOnly,FilesOnly' created a
FilesOnly entry and -AccessType 'Allow,Deny' created a Deny entry, without a
message. The conversion in ParameterConversion now rejects a string with a
comma and a collection of more than one element for an enumeration that is
not a flags enumeration, with the same InvalidParameterValue error as for a
misspelled name (terminating for a named value, one error per object for a
piped one). A single name and a collection of one element convert as before,
and the flags parameters still take a list. The valid-value matrices of both
published rc8 and this build give the same entries in both editions, named,
piped by property name, and splatted, apart from the nine list rows.
The completers of the five parameters compared with WildcardOptions.IgnoreCase
only, so that a word typed in capitals did not complete while the culture of
the thread was Turkish (an I and an i are different letters there, and
THISFOLDERO did not match ThisFolderOnly). They compare culture invariant now,
like the filter of Get-ChildItem2.
The tests pin both, red before the change and green after it in four
configurations, and the headers of the two test files no longer say that the
-SecurityDescriptor sets have no handler per descriptor.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
1 week ago
@ { Shape = 'a comma list of two names' ; Value = 'Allow,Deny' ; ValueText = 'Allow,Deny' }
@ { Shape = 'an array of two names' ; Value = @ ( 'Allow' , 'Deny' ) ; ValueText = 'Allow, Deny' }
fix(access): reject a list of any kind of collection for -AccessType and -AppliesTo
The rejection of a list for the enumerations that are not flags
enumerations looked only at a string with a comma and at an IList of more
than one element. A collection that is not an IList (a HashSet, a Queue, the
keys of a hashtable, a LINQ sequence), a collection of one element that
holds a comma list (@('Allow,Deny'), such as a JSON array of one string), and
a collection that holds a collection of several elements still reached
LanguagePrimitives.ConvertTo, which added up the numbers: -AppliesTo
$scopes.Keys created a ThisFolderAndFiles entry that nobody named.
ParameterConversion takes the one value out of the argument now: a wrapped
PSObject counts as what it wraps, a collection of one element converts as
its element (also when that element is a collection of one element), and a
collection of several elements or an element with a comma is the list that is
rejected. A dictionary, an empty collection, and a collection that cannot be
enumerated keep going to the conversion, which reports them as before. A
blank value inside a collection of one element is reported as blank.
The tests add the shapes to the bad-value tables of the four cmdlets (32
rows, red before the change and green after it in four configurations) and
pin that a list of one name and a hash set of one name stay valid. The
valid-value matrices of rc8 and this build still differ in the nine list rows
only, and a probe of 50 shapes shows every collection of several values
rejected and every single value accepted, in both editions.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
1 week ago
@ { Shape = 'an array of one comma list' ; Value = @ ( 'Allow,Deny' ) ; ValueText = 'Allow,Deny' }
@ { Shape = 'an array that holds an array of two names' ; Value = , @ ( 'Allow' , 'Deny' ) ; ValueText = 'Allow, Deny' }
@ { Shape = 'a hash set of two names' ; Value = [System.Collections.Generic.HashSet[string]] :: new ( [string[]] @ ( 'Allow' , 'Deny' ) ) ; ValueText = $null }
@ { Shape = 'the keys of a hashtable' ; Value = ( @ { Allow = 1 ; Deny = 2 } ) . Keys ; ValueText = $null }
fix(access): end a collection that holds itself and name the typo in the message of a list
The check that rejects a list for -AccessType and -AppliesTo opened every level
of a collection of one element, so that a collection that holds itself never
returned. It opens at most 32 levels now and counts a deeper nesting as a list,
and it stops reading a collection after the second element, so that a long or
endless sequence is not read to its end.
The message names what the caller wrote: the one element of a collection of one
element (also when that element is a collection itself), the first ten elements
of a longer collection followed by "...", and otherwise the text of the value.
Before, a collection of one misspelled name was named by its type.
Tests, written first: 28 rows failed on the previous build in all four
configurations and none fails now. They cover an array of one misspelled name,
a collection nested 40 levels deep for both parameters of both cmdlets, and an
array that holds an array of one name, which stays valid. The assertion of the
value text expects "The value '<text>'", because the old pattern could also
match the name of a type. The comments say that PowerShell combines the numbers
of a list bit by bit, which is what it does, and not that it adds them.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
1 week ago
@ { Shape = 'an array nested 40 levels deep' ; Value = $accessDeepAccessType ; ValueText = $null }
fix(access): report a piped AccessType, AuditFlags, flags, or scope that isn't valid
Add-NTFSAccess, Remove-NTFSAccess, Add-NTFSAudit, and Remove-NTFSAudit
silently kept the default value of AccessType, AuditFlags,
InheritanceFlags, PropagationFlags, and AppliesTo when a piped object's
property could not be converted to the parameter's type: a misspelled
AccessType such as "Denyy", or a blank CSV cell, created an Allow entry
with no message. For a permissions tool, a typo for Deny that silently
grants access is a security-relevant defect (Decision 27).
The five parameters are now declared as object and converted inside
the cmdlet, the way the PowerShell binder converts a typed parameter
(names in any case, comma-separated flag lists, numbers, enum
instances). A value that cannot be converted, including a blank one,
is now an error named InvalidParameterValue: terminating and
catchable when it is named on the command line, exactly as the binder
reported it before; a non-terminating error targeting that path or
descriptor when it comes from a piped object, which leaves that item
unchanged and lets the pipeline continue with the next one. A $null
value is still not an error and keeps the parameter's default. Tab
completion of the five parameters keeps working through explicit
argument completers. Choosing between the Simple and Complex
parameter sets is unaffected and still rejects AppliesTo together
with InheritanceFlags or PropagationFlags as AmbiguousParameterSet,
for every mix of string and enum values.
The four cmdlet pages document the parameters as Type: Object, list
the valid values, and describe the new error; a sentence that said a
piped AppliesTo typo or blank cell was silently ignored is corrected.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
1 week ago
)
fix(access): end a collection that holds itself and name the typo in the message of a list
The check that rejects a list for -AccessType and -AppliesTo opened every level
of a collection of one element, so that a collection that holds itself never
returned. It opens at most 32 levels now and counts a deeper nesting as a list,
and it stops reading a collection after the second element, so that a long or
endless sequence is not read to its end.
The message names what the caller wrote: the one element of a collection of one
element (also when that element is a collection itself), the first ten elements
of a longer collection followed by "...", and otherwise the text of the value.
Before, a collection of one misspelled name was named by its type.
Tests, written first: 28 rows failed on the previous build in all four
configurations and none fails now. They cover an array of one misspelled name,
a collection nested 40 levels deep for both parameters of both cmdlets, and an
array that holds an array of one name, which stays valid. The assertion of the
value text expects "The value '<text>'", because the old pattern could also
match the name of a type. The comments say that PowerShell combines the numbers
of a list bit by bit, which is what it does, and not that it adds them.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
1 week ago
# AccessType and AppliesTo name one value each, not a combination: PowerShell combines the numbers of a list bit by
# bit, so that one of the values wins silently or, for AppliesTo, a scope that the caller did not name is chosen.
# Every kind of collection counts, also a collection of one element that holds a list.
fix(access): reject a list for -AccessType and -AppliesTo, and complete in capitals in Turkish
-AccessType and -AppliesTo of Add-NTFSAccess, Remove-NTFSAccess,
Add-NTFSAudit (-AppliesTo), and Remove-NTFSAudit (-AppliesTo) take one value,
but PowerShell adds up the numbers of a comma list or an array for an
enumeration, so that -AppliesTo 'ThisFolderOnly,FilesOnly' created a
FilesOnly entry and -AccessType 'Allow,Deny' created a Deny entry, without a
message. The conversion in ParameterConversion now rejects a string with a
comma and a collection of more than one element for an enumeration that is
not a flags enumeration, with the same InvalidParameterValue error as for a
misspelled name (terminating for a named value, one error per object for a
piped one). A single name and a collection of one element convert as before,
and the flags parameters still take a list. The valid-value matrices of both
published rc8 and this build give the same entries in both editions, named,
piped by property name, and splatted, apart from the nine list rows.
The completers of the five parameters compared with WildcardOptions.IgnoreCase
only, so that a word typed in capitals did not complete while the culture of
the thread was Turkish (an I and an i are different letters there, and
THISFOLDERO did not match ThisFolderOnly). They compare culture invariant now,
like the filter of Get-ChildItem2.
The tests pin both, red before the change and green after it in four
configurations, and the headers of the two test files no longer say that the
-SecurityDescriptor sets have no handler per descriptor.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
1 week ago
$accessAppliesToListShapes = @ (
@ { Shape = 'a comma list of two names' ; Value = 'ThisFolderAndSubfolders,ThisFolderAndFiles' ; ValueText = 'ThisFolderAndSubfolders,ThisFolderAndFiles' }
@ { Shape = 'an array of two names' ; Value = @ ( 'ThisFolderAndSubfolders' , 'ThisFolderAndFiles' ) ; ValueText = 'ThisFolderAndSubfolders, ThisFolderAndFiles' }
fix(access): reject a list of any kind of collection for -AccessType and -AppliesTo
The rejection of a list for the enumerations that are not flags
enumerations looked only at a string with a comma and at an IList of more
than one element. A collection that is not an IList (a HashSet, a Queue, the
keys of a hashtable, a LINQ sequence), a collection of one element that
holds a comma list (@('Allow,Deny'), such as a JSON array of one string), and
a collection that holds a collection of several elements still reached
LanguagePrimitives.ConvertTo, which added up the numbers: -AppliesTo
$scopes.Keys created a ThisFolderAndFiles entry that nobody named.
ParameterConversion takes the one value out of the argument now: a wrapped
PSObject counts as what it wraps, a collection of one element converts as
its element (also when that element is a collection of one element), and a
collection of several elements or an element with a comma is the list that is
rejected. A dictionary, an empty collection, and a collection that cannot be
enumerated keep going to the conversion, which reports them as before. A
blank value inside a collection of one element is reported as blank.
The tests add the shapes to the bad-value tables of the four cmdlets (32
rows, red before the change and green after it in four configurations) and
pin that a list of one name and a hash set of one name stay valid. The
valid-value matrices of rc8 and this build still differ in the nine list rows
only, and a probe of 50 shapes shows every collection of several values
rejected and every single value accepted, in both editions.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
1 week ago
@ { Shape = 'an array of one comma list' ; Value = @ ( 'ThisFolderAndSubfolders,ThisFolderAndFiles' ) ; ValueText = 'ThisFolderAndSubfolders,ThisFolderAndFiles' }
@ { Shape = 'a hash set of two names' ; Value = [System.Collections.Generic.HashSet[string]] :: new ( [string[]] @ ( 'ThisFolderAndSubfolders' , 'ThisFolderAndFiles' ) ) ; ValueText = $null }
@ { Shape = 'the keys of a hashtable' ; Value = ( @ { ThisFolderAndSubfolders = 1 ; ThisFolderAndFiles = 2 } ) . Keys ; ValueText = $null }
fix(access): end a collection that holds itself and name the typo in the message of a list
The check that rejects a list for -AccessType and -AppliesTo opened every level
of a collection of one element, so that a collection that holds itself never
returned. It opens at most 32 levels now and counts a deeper nesting as a list,
and it stops reading a collection after the second element, so that a long or
endless sequence is not read to its end.
The message names what the caller wrote: the one element of a collection of one
element (also when that element is a collection itself), the first ten elements
of a longer collection followed by "...", and otherwise the text of the value.
Before, a collection of one misspelled name was named by its type.
Tests, written first: 28 rows failed on the previous build in all four
configurations and none fails now. They cover an array of one misspelled name,
a collection nested 40 levels deep for both parameters of both cmdlets, and an
array that holds an array of one name, which stays valid. The assertion of the
value text expects "The value '<text>'", because the old pattern could also
match the name of a type. The comments say that PowerShell combines the numbers
of a list bit by bit, which is what it does, and not that it adds them.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
1 week ago
@ { Shape = 'an array nested 40 levels deep' ; Value = $accessDeepScope ; ValueText = $null }
fix(access): reject a list for -AccessType and -AppliesTo, and complete in capitals in Turkish
-AccessType and -AppliesTo of Add-NTFSAccess, Remove-NTFSAccess,
Add-NTFSAudit (-AppliesTo), and Remove-NTFSAudit (-AppliesTo) take one value,
but PowerShell adds up the numbers of a comma list or an array for an
enumeration, so that -AppliesTo 'ThisFolderOnly,FilesOnly' created a
FilesOnly entry and -AccessType 'Allow,Deny' created a Deny entry, without a
message. The conversion in ParameterConversion now rejects a string with a
comma and a collection of more than one element for an enumeration that is
not a flags enumeration, with the same InvalidParameterValue error as for a
misspelled name (terminating for a named value, one error per object for a
piped one). A single name and a collection of one element convert as before,
and the flags parameters still take a list. The valid-value matrices of both
published rc8 and this build give the same entries in both editions, named,
piped by property name, and splatted, apart from the nine list rows.
The completers of the five parameters compared with WildcardOptions.IgnoreCase
only, so that a word typed in capitals did not complete while the culture of
the thread was Turkish (an I and an i are different letters there, and
THISFOLDERO did not match ThisFolderOnly). They compare culture invariant now,
like the filter of Get-ChildItem2.
The tests pin both, red before the change and green after it in four
configurations, and the headers of the two test files no longer say that the
-SecurityDescriptor sets have no handler per descriptor.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
1 week ago
)
fix(access): build the message of a bad parameter value even when the text of the value throws
The message of the error is built inside the catch block of the conversion, and
Describe formatted the one element of a collection of one element outside of
any guard. An element whose ToString throws (a script method, for example)
ended the command with a terminating ExtendedTypeSystemException instead of
the InvalidParameterValue error for that item, and a piped command stopped at
that object, where the pages say that the error is reported for just that
item. Describe now falls back to the type name whatever the text or the
enumeration of the value does, for the five parameters of the four cmdlets
alike.
Tests, written first: 24 rows failed on the previous build in all four
configurations and none fails now (an array of one object whose ToString
throws, piped for every parameter and named for one parameter of each kind).
New rows also pin what the previous commit claimed but did not test: the
message names the first ten elements of a list and ends with an ellipsis from
the eleventh, and a sequence of 100,000 elements is read only up to its first
elements (a counting sequence, named and piped). A build without the two limits
fails 4 of them and a build with a limit of nine fails 6, in all four
configurations. The comment of the nesting limit now says exactly what it
counts.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
1 week ago
# The cross product of the two cmdlets that share these parameters and every bad shape: 2 * (14 + 3 * 7 + 6) = 82 rows.
fix(access): report a piped AccessType, AuditFlags, flags, or scope that isn't valid
Add-NTFSAccess, Remove-NTFSAccess, Add-NTFSAudit, and Remove-NTFSAudit
silently kept the default value of AccessType, AuditFlags,
InheritanceFlags, PropagationFlags, and AppliesTo when a piped object's
property could not be converted to the parameter's type: a misspelled
AccessType such as "Denyy", or a blank CSV cell, created an Allow entry
with no message. For a permissions tool, a typo for Deny that silently
grants access is a security-relevant defect (Decision 27).
The five parameters are now declared as object and converted inside
the cmdlet, the way the PowerShell binder converts a typed parameter
(names in any case, comma-separated flag lists, numbers, enum
instances). A value that cannot be converted, including a blank one,
is now an error named InvalidParameterValue: terminating and
catchable when it is named on the command line, exactly as the binder
reported it before; a non-terminating error targeting that path or
descriptor when it comes from a piped object, which leaves that item
unchanged and lets the pipeline continue with the next one. A $null
value is still not an error and keeps the parameter's default. Tab
completion of the five parameters keeps working through explicit
argument completers. Choosing between the Simple and Complex
parameter sets is unaffected and still rejects AppliesTo together
with InheritanceFlags or PropagationFlags as AmbiguousParameterSet,
for every mix of string and enum values.
The four cmdlet pages document the parameters as Type: Object, list
the valid values, and describe the new error; a sentence that said a
piped AppliesTo typo or blank cell was silently ignored is corrected.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
1 week ago
$accessInvalidParameterCases = foreach ( $cmdlet in 'Add-NTFSAccess' , 'Remove-NTFSAccess' ) {
foreach ( $shapeCase in $accessTypeBadShapes ) {
@ { Cmdlet = $cmdlet ; Parameter = 'AccessType' ; Shape = $shapeCase . Shape ; Value = $shapeCase . Value ; ValueText = $shapeCase . ValueText }
}
foreach ( $parameterName in 'InheritanceFlags' , 'PropagationFlags' , 'AppliesTo' ) {
foreach ( $shapeCase in $accessBadShapes ) {
@ { Cmdlet = $cmdlet ; Parameter = $parameterName ; Shape = $shapeCase . Shape ; Value = $shapeCase . Value ; ValueText = $shapeCase . ValueText }
}
}
fix(access): reject a list for -AccessType and -AppliesTo, and complete in capitals in Turkish
-AccessType and -AppliesTo of Add-NTFSAccess, Remove-NTFSAccess,
Add-NTFSAudit (-AppliesTo), and Remove-NTFSAudit (-AppliesTo) take one value,
but PowerShell adds up the numbers of a comma list or an array for an
enumeration, so that -AppliesTo 'ThisFolderOnly,FilesOnly' created a
FilesOnly entry and -AccessType 'Allow,Deny' created a Deny entry, without a
message. The conversion in ParameterConversion now rejects a string with a
comma and a collection of more than one element for an enumeration that is
not a flags enumeration, with the same InvalidParameterValue error as for a
misspelled name (terminating for a named value, one error per object for a
piped one). A single name and a collection of one element convert as before,
and the flags parameters still take a list. The valid-value matrices of both
published rc8 and this build give the same entries in both editions, named,
piped by property name, and splatted, apart from the nine list rows.
The completers of the five parameters compared with WildcardOptions.IgnoreCase
only, so that a word typed in capitals did not complete while the culture of
the thread was Turkish (an I and an i are different letters there, and
THISFOLDERO did not match ThisFolderOnly). They compare culture invariant now,
like the filter of Get-ChildItem2.
The tests pin both, red before the change and green after it in four
configurations, and the headers of the two test files no longer say that the
-SecurityDescriptor sets have no handler per descriptor.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
1 week ago
foreach ( $shapeCase in $accessAppliesToListShapes ) {
@ { Cmdlet = $cmdlet ; Parameter = 'AppliesTo' ; Shape = $shapeCase . Shape ; Value = $shapeCase . Value ; ValueText = $shapeCase . ValueText }
}
fix(access): report a piped AccessType, AuditFlags, flags, or scope that isn't valid
Add-NTFSAccess, Remove-NTFSAccess, Add-NTFSAudit, and Remove-NTFSAudit
silently kept the default value of AccessType, AuditFlags,
InheritanceFlags, PropagationFlags, and AppliesTo when a piped object's
property could not be converted to the parameter's type: a misspelled
AccessType such as "Denyy", or a blank CSV cell, created an Allow entry
with no message. For a permissions tool, a typo for Deny that silently
grants access is a security-relevant defect (Decision 27).
The five parameters are now declared as object and converted inside
the cmdlet, the way the PowerShell binder converts a typed parameter
(names in any case, comma-separated flag lists, numbers, enum
instances). A value that cannot be converted, including a blank one,
is now an error named InvalidParameterValue: terminating and
catchable when it is named on the command line, exactly as the binder
reported it before; a non-terminating error targeting that path or
descriptor when it comes from a piped object, which leaves that item
unchanged and lets the pipeline continue with the next one. A $null
value is still not an error and keeps the parameter's default. Tab
completion of the five parameters keeps working through explicit
argument completers. Choosing between the Simple and Complex
parameter sets is unaffected and still rejects AppliesTo together
with InheritanceFlags or PropagationFlags as AmbiguousParameterSet,
for every mix of string and enum values.
The four cmdlet pages document the parameters as Type: Object, list
the valid values, and describe the new error; a sentence that said a
piped AppliesTo typo or blank cell was silently ignored is corrected.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
1 week ago
}
fix(access): build the message of a bad parameter value even when the text of the value throws
The message of the error is built inside the catch block of the conversion, and
Describe formatted the one element of a collection of one element outside of
any guard. An element whose ToString throws (a script method, for example)
ended the command with a terminating ExtendedTypeSystemException instead of
the InvalidParameterValue error for that item, and a piped command stopped at
that object, where the pages say that the error is reported for just that
item. Describe now falls back to the type name whatever the text or the
enumeration of the value does, for the five parameters of the four cmdlets
alike.
Tests, written first: 24 rows failed on the previous build in all four
configurations and none fails now (an array of one object whose ToString
throws, piped for every parameter and named for one parameter of each kind).
New rows also pin what the previous commit claimed but did not test: the
message names the first ten elements of a list and ends with an ellipsis from
the eleventh, and a sequence of 100,000 elements is read only up to its first
elements (a counting sequence, named and piped). A build without the two limits
fails 4 of them and a build with a limit of nine fails 6, in all four
configurations. The comment of the nesting limit now says exactly what it
counts.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
1 week ago
# The message names a list by its first ten elements: ten are named in full, eleven end with an ellipsis. The
# elements of a flags parameter are converted one by one, so these numbers fail there too (4 is not a flag). A
# value whose text cannot be built is named by its type, in the short form that Type.ToString gives.
fix(access): build the message of a bad parameter value even when the text of the value throws
The message of the error is built inside the catch block of the conversion, and
Describe formatted the one element of a collection of one element outside of
any guard. An element whose ToString throws (a script method, for example)
ended the command with a terminating ExtendedTypeSystemException instead of
the InvalidParameterValue error for that item, and a piped command stopped at
that object, where the pages say that the error is reported for just that
item. Describe now falls back to the type name whatever the text or the
enumeration of the value does, for the five parameters of the four cmdlets
alike.
Tests, written first: 24 rows failed on the previous build in all four
configurations and none fails now (an array of one object whose ToString
throws, piped for every parameter and named for one parameter of each kind).
New rows also pin what the previous commit claimed but did not test: the
message names the first ten elements of a list and ends with an ellipsis from
the eleventh, and a sequence of 100,000 elements is read only up to its first
elements (a counting sequence, named and piped). A build without the two limits
fails 4 of them and a build with a limit of nine fails 6, in all four
configurations. The comment of the nesting limit now says exactly what it
counts.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
1 week ago
$accessInvalidParameterCases + = @ (
@ { Cmdlet = 'Add-NTFSAccess' ; Parameter = 'AccessType' ; Shape = 'ten numbers' ; Value = 1 . . 10 ; ValueText = '1, 2, 3, 4, 5, 6, 7, 8, 9, 10' }
@ { Cmdlet = 'Add-NTFSAccess' ; Parameter = 'AccessType' ; Shape = 'eleven numbers' ; Value = 1 . . 11 ; ValueText = '1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...' }
@ { Cmdlet = 'Add-NTFSAccess' ; Parameter = 'InheritanceFlags' ; Shape = 'ten numbers' ; Value = 1 . . 10 ; ValueText = '1, 2, 3, 4, 5, 6, 7, 8, 9, 10' }
@ { Cmdlet = 'Add-NTFSAccess' ; Parameter = 'InheritanceFlags' ; Shape = 'eleven numbers' ; Value = 1 . . 11 ; ValueText = '1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...' }
@ { Cmdlet = 'Add-NTFSAccess' ; Parameter = 'AccessType' ; Shape = 'a generic list of one object whose ToString throws' ; Value = [System.Collections.Generic.List[object]] :: new ( [object[]] @ ( $accessThrowingElement ) ) ; ValueText = 'System.Collections.Generic.List`1[System.Object]' }
@ { Cmdlet = 'Add-NTFSAccess' ; Parameter = 'InheritanceFlags' ; Shape = 'a generic list of one object whose ToString throws' ; Value = [System.Collections.Generic.List[object]] :: new ( [object[]] @ ( $accessThrowingElement ) ) ; ValueText = 'System.Collections.Generic.List`1[System.Object]' }
fix(access): build the message of a bad parameter value even when the text of the value throws
The message of the error is built inside the catch block of the conversion, and
Describe formatted the one element of a collection of one element outside of
any guard. An element whose ToString throws (a script method, for example)
ended the command with a terminating ExtendedTypeSystemException instead of
the InvalidParameterValue error for that item, and a piped command stopped at
that object, where the pages say that the error is reported for just that
item. Describe now falls back to the type name whatever the text or the
enumeration of the value does, for the five parameters of the four cmdlets
alike.
Tests, written first: 24 rows failed on the previous build in all four
configurations and none fails now (an array of one object whose ToString
throws, piped for every parameter and named for one parameter of each kind).
New rows also pin what the previous commit claimed but did not test: the
message names the first ten elements of a list and ends with an ellipsis from
the eleventh, and a sequence of 100,000 elements is read only up to its first
elements (a counting sequence, named and piped). A build without the two limits
fails 4 of them and a build with a limit of nine fails 6, in all four
configurations. The comment of the nesting limit now says exactly what it
counts.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
1 week ago
)
fix(access): report a piped AccessType, AuditFlags, flags, or scope that isn't valid
Add-NTFSAccess, Remove-NTFSAccess, Add-NTFSAudit, and Remove-NTFSAudit
silently kept the default value of AccessType, AuditFlags,
InheritanceFlags, PropagationFlags, and AppliesTo when a piped object's
property could not be converted to the parameter's type: a misspelled
AccessType such as "Denyy", or a blank CSV cell, created an Allow entry
with no message. For a permissions tool, a typo for Deny that silently
grants access is a security-relevant defect (Decision 27).
The five parameters are now declared as object and converted inside
the cmdlet, the way the PowerShell binder converts a typed parameter
(names in any case, comma-separated flag lists, numbers, enum
instances). A value that cannot be converted, including a blank one,
is now an error named InvalidParameterValue: terminating and
catchable when it is named on the command line, exactly as the binder
reported it before; a non-terminating error targeting that path or
descriptor when it comes from a piped object, which leaves that item
unchanged and lets the pipeline continue with the next one. A $null
value is still not an error and keeps the parameter's default. Tab
completion of the five parameters keeps working through explicit
argument completers. Choosing between the Simple and Complex
parameter sets is unaffected and still rejects AppliesTo together
with InheritanceFlags or PropagationFlags as AmbiguousParameterSet,
for every mix of string and enum values.
The four cmdlet pages document the parameters as Type: Object, list
the valid values, and describe the new error; a sentence that said a
piped AppliesTo typo or blank cell was silently ignored is corrected.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
1 week ago
$accessSdInvalidParameterCases = foreach ( $cmdlet in 'Add-NTFSAccess' , 'Remove-NTFSAccess' ) {
@ { Cmdlet = $cmdlet ; Parameter = 'AccessType' ; Value = 'Denyy' ; ValueText = 'Denyy' }
@ { Cmdlet = $cmdlet ; Parameter = 'AppliesTo' ; Value = 'Typoo' ; ValueText = 'Typoo' }
}
$accessNoCarryOverCases = @ (
@ { Cmdlet = 'Add-NTFSAccess' ; Parameter = 'AccessType' ; Value = 'Denyy' }
@ { Cmdlet = 'Add-NTFSAccess' ; Parameter = 'InheritanceFlags' ; Value = 'Typoo' }
@ { Cmdlet = 'Add-NTFSAccess' ; Parameter = 'PropagationFlags' ; Value = 'Typoo' }
@ { Cmdlet = 'Add-NTFSAccess' ; Parameter = 'AppliesTo' ; Value = 'Typoo' }
)
$accessNamedInvalidParameterCases = foreach ( $cmdlet in 'Add-NTFSAccess' , 'Remove-NTFSAccess' ) {
@ { Cmdlet = $cmdlet ; Parameter = 'AccessType' ; Value = 'Denyy' }
@ { Cmdlet = $cmdlet ; Parameter = 'InheritanceFlags' ; Value = 'Typoo' }
@ { Cmdlet = $cmdlet ; Parameter = 'PropagationFlags' ; Value = 'Typoo' }
@ { Cmdlet = $cmdlet ; Parameter = 'AppliesTo' ; Value = 'Typoo' }
fix(access): reject a list for -AccessType and -AppliesTo, and complete in capitals in Turkish
-AccessType and -AppliesTo of Add-NTFSAccess, Remove-NTFSAccess,
Add-NTFSAudit (-AppliesTo), and Remove-NTFSAudit (-AppliesTo) take one value,
but PowerShell adds up the numbers of a comma list or an array for an
enumeration, so that -AppliesTo 'ThisFolderOnly,FilesOnly' created a
FilesOnly entry and -AccessType 'Allow,Deny' created a Deny entry, without a
message. The conversion in ParameterConversion now rejects a string with a
comma and a collection of more than one element for an enumeration that is
not a flags enumeration, with the same InvalidParameterValue error as for a
misspelled name (terminating for a named value, one error per object for a
piped one). A single name and a collection of one element convert as before,
and the flags parameters still take a list. The valid-value matrices of both
published rc8 and this build give the same entries in both editions, named,
piped by property name, and splatted, apart from the nine list rows.
The completers of the five parameters compared with WildcardOptions.IgnoreCase
only, so that a word typed in capitals did not complete while the culture of
the thread was Turkish (an I and an i are different letters there, and
THISFOLDERO did not match ThisFolderOnly). They compare culture invariant now,
like the filter of Get-ChildItem2.
The tests pin both, red before the change and green after it in four
configurations, and the headers of the two test files no longer say that the
-SecurityDescriptor sets have no handler per descriptor.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
1 week ago
@ { Cmdlet = $cmdlet ; Parameter = 'AccessType' ; Value = 'Allow,Deny' }
@ { Cmdlet = $cmdlet ; Parameter = 'AccessType' ; Value = @ ( 'Allow' , 'Deny' ) }
fix(access): reject a list of any kind of collection for -AccessType and -AppliesTo
The rejection of a list for the enumerations that are not flags
enumerations looked only at a string with a comma and at an IList of more
than one element. A collection that is not an IList (a HashSet, a Queue, the
keys of a hashtable, a LINQ sequence), a collection of one element that
holds a comma list (@('Allow,Deny'), such as a JSON array of one string), and
a collection that holds a collection of several elements still reached
LanguagePrimitives.ConvertTo, which added up the numbers: -AppliesTo
$scopes.Keys created a ThisFolderAndFiles entry that nobody named.
ParameterConversion takes the one value out of the argument now: a wrapped
PSObject counts as what it wraps, a collection of one element converts as
its element (also when that element is a collection of one element), and a
collection of several elements or an element with a comma is the list that is
rejected. A dictionary, an empty collection, and a collection that cannot be
enumerated keep going to the conversion, which reports them as before. A
blank value inside a collection of one element is reported as blank.
The tests add the shapes to the bad-value tables of the four cmdlets (32
rows, red before the change and green after it in four configurations) and
pin that a list of one name and a hash set of one name stay valid. The
valid-value matrices of rc8 and this build still differ in the nine list rows
only, and a probe of 50 shapes shows every collection of several values
rejected and every single value accepted, in both editions.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
1 week ago
@ { Cmdlet = $cmdlet ; Parameter = 'AccessType' ; Value = @ ( 'Allow,Deny' ) }
@ { Cmdlet = $cmdlet ; Parameter = 'AccessType' ; Value = [System.Collections.Generic.HashSet[string]] :: new ( [string[]] @ ( 'Allow' , 'Deny' ) ) }
fix(access): reject a list for -AccessType and -AppliesTo, and complete in capitals in Turkish
-AccessType and -AppliesTo of Add-NTFSAccess, Remove-NTFSAccess,
Add-NTFSAudit (-AppliesTo), and Remove-NTFSAudit (-AppliesTo) take one value,
but PowerShell adds up the numbers of a comma list or an array for an
enumeration, so that -AppliesTo 'ThisFolderOnly,FilesOnly' created a
FilesOnly entry and -AccessType 'Allow,Deny' created a Deny entry, without a
message. The conversion in ParameterConversion now rejects a string with a
comma and a collection of more than one element for an enumeration that is
not a flags enumeration, with the same InvalidParameterValue error as for a
misspelled name (terminating for a named value, one error per object for a
piped one). A single name and a collection of one element convert as before,
and the flags parameters still take a list. The valid-value matrices of both
published rc8 and this build give the same entries in both editions, named,
piped by property name, and splatted, apart from the nine list rows.
The completers of the five parameters compared with WildcardOptions.IgnoreCase
only, so that a word typed in capitals did not complete while the culture of
the thread was Turkish (an I and an i are different letters there, and
THISFOLDERO did not match ThisFolderOnly). They compare culture invariant now,
like the filter of Get-ChildItem2.
The tests pin both, red before the change and green after it in four
configurations, and the headers of the two test files no longer say that the
-SecurityDescriptor sets have no handler per descriptor.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
1 week ago
@ { Cmdlet = $cmdlet ; Parameter = 'AppliesTo' ; Value = 'ThisFolderAndSubfolders,ThisFolderAndFiles' }
@ { Cmdlet = $cmdlet ; Parameter = 'AppliesTo' ; Value = @ ( 'ThisFolderAndSubfolders' , 'ThisFolderAndFiles' ) }
fix(access): reject a list of any kind of collection for -AccessType and -AppliesTo
The rejection of a list for the enumerations that are not flags
enumerations looked only at a string with a comma and at an IList of more
than one element. A collection that is not an IList (a HashSet, a Queue, the
keys of a hashtable, a LINQ sequence), a collection of one element that
holds a comma list (@('Allow,Deny'), such as a JSON array of one string), and
a collection that holds a collection of several elements still reached
LanguagePrimitives.ConvertTo, which added up the numbers: -AppliesTo
$scopes.Keys created a ThisFolderAndFiles entry that nobody named.
ParameterConversion takes the one value out of the argument now: a wrapped
PSObject counts as what it wraps, a collection of one element converts as
its element (also when that element is a collection of one element), and a
collection of several elements or an element with a comma is the list that is
rejected. A dictionary, an empty collection, and a collection that cannot be
enumerated keep going to the conversion, which reports them as before. A
blank value inside a collection of one element is reported as blank.
The tests add the shapes to the bad-value tables of the four cmdlets (32
rows, red before the change and green after it in four configurations) and
pin that a list of one name and a hash set of one name stay valid. The
valid-value matrices of rc8 and this build still differ in the nine list rows
only, and a probe of 50 shapes shows every collection of several values
rejected and every single value accepted, in both editions.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
1 week ago
@ { Cmdlet = $cmdlet ; Parameter = 'AppliesTo' ; Value = @ ( 'ThisFolderAndSubfolders,ThisFolderAndFiles' ) }
@ { Cmdlet = $cmdlet ; Parameter = 'AppliesTo' ; Value = [System.Collections.Generic.HashSet[string]] :: new ( [string[]] @ ( 'ThisFolderAndSubfolders' , 'ThisFolderAndFiles' ) ) }
fix(access): end a collection that holds itself and name the typo in the message of a list
The check that rejects a list for -AccessType and -AppliesTo opened every level
of a collection of one element, so that a collection that holds itself never
returned. It opens at most 32 levels now and counts a deeper nesting as a list,
and it stops reading a collection after the second element, so that a long or
endless sequence is not read to its end.
The message names what the caller wrote: the one element of a collection of one
element (also when that element is a collection itself), the first ten elements
of a longer collection followed by "...", and otherwise the text of the value.
Before, a collection of one misspelled name was named by its type.
Tests, written first: 28 rows failed on the previous build in all four
configurations and none fails now. They cover an array of one misspelled name,
a collection nested 40 levels deep for both parameters of both cmdlets, and an
array that holds an array of one name, which stays valid. The assertion of the
value text expects "The value '<text>'", because the old pattern could also
match the name of a type. The comments say that PowerShell combines the numbers
of a list bit by bit, which is what it does, and not that it adds them.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
1 week ago
@ { Cmdlet = $cmdlet ; Parameter = 'AccessType' ; Value = $accessDeepAccessType }
@ { Cmdlet = $cmdlet ; Parameter = 'AppliesTo' ; Value = $accessDeepScope }
fix(access): build the message of a bad parameter value even when the text of the value throws
The message of the error is built inside the catch block of the conversion, and
Describe formatted the one element of a collection of one element outside of
any guard. An element whose ToString throws (a script method, for example)
ended the command with a terminating ExtendedTypeSystemException instead of
the InvalidParameterValue error for that item, and a piped command stopped at
that object, where the pages say that the error is reported for just that
item. Describe now falls back to the type name whatever the text or the
enumeration of the value does, for the five parameters of the four cmdlets
alike.
Tests, written first: 24 rows failed on the previous build in all four
configurations and none fails now (an array of one object whose ToString
throws, piped for every parameter and named for one parameter of each kind).
New rows also pin what the previous commit claimed but did not test: the
message names the first ten elements of a list and ends with an ellipsis from
the eleventh, and a sequence of 100,000 elements is read only up to its first
elements (a counting sequence, named and piped). A build without the two limits
fails 4 of them and a build with a limit of nine fails 6, in all four
configurations. The comment of the nesting limit now says exactly what it
counts.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
1 week ago
@ { Cmdlet = $cmdlet ; Parameter = 'AccessType' ; Value = @ ( $accessThrowingElement ) }
@ { Cmdlet = $cmdlet ; Parameter = 'InheritanceFlags' ; Value = @ ( $accessThrowingElement ) }
fix(access): report a piped AccessType, AuditFlags, flags, or scope that isn't valid
Add-NTFSAccess, Remove-NTFSAccess, Add-NTFSAudit, and Remove-NTFSAudit
silently kept the default value of AccessType, AuditFlags,
InheritanceFlags, PropagationFlags, and AppliesTo when a piped object's
property could not be converted to the parameter's type: a misspelled
AccessType such as "Denyy", or a blank CSV cell, created an Allow entry
with no message. For a permissions tool, a typo for Deny that silently
grants access is a security-relevant defect (Decision 27).
The five parameters are now declared as object and converted inside
the cmdlet, the way the PowerShell binder converts a typed parameter
(names in any case, comma-separated flag lists, numbers, enum
instances). A value that cannot be converted, including a blank one,
is now an error named InvalidParameterValue: terminating and
catchable when it is named on the command line, exactly as the binder
reported it before; a non-terminating error targeting that path or
descriptor when it comes from a piped object, which leaves that item
unchanged and lets the pipeline continue with the next one. A $null
value is still not an error and keeps the parameter's default. Tab
completion of the five parameters keeps working through explicit
argument completers. Choosing between the Simple and Complex
parameter sets is unaffected and still rejects AppliesTo together
with InheritanceFlags or PropagationFlags as AmbiguousParameterSet,
for every mix of string and enum values.
The four cmdlet pages document the parameters as Type: Object, list
the valid values, and describe the new error; a sentence that said a
piped AppliesTo typo or blank cell was silently ignored is corrected.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
1 week ago
}
# A named $null (a literal $null, a variable, an alias, splatting, or $PSDefaultParameterValues) must
fix(access): stop for a named $null of the five parameters
A named $null of AccessType, AuditFlags, InheritanceFlags,
PropagationFlags, or AppliesTo (a literal $null, a variable that holds
$null such as a misspelled property, the alias -AccessControlType,
splatting, or a null entry of $PSDefaultParameterValues) silently kept
the parameter's default instead of stopping: Add-NTFSAccess -AccessType
$null created an Allow entry, Add-NTFSAudit -AuditFlags $null audited
Success and Failure, and Remove-NTFSAccess -AccessType $null removed
nothing. Before the five parameters became object (Decision 27, fix
B), the same named $null ended with a terminating ParameterBindingFailed.
Each cmdlet's BeginProcessing now checks, for a value that is null,
whether MyInvocation.BoundParameters has the parameter's name; a piped
property that is null is not bound yet at that point and still keeps
the default, but a value that is already bound and null is reported
the same way as a bad named value: a terminating InvalidParameterValue
error naming the parameter and its valid values. AppliesTo is Mandatory
and a named $null of it was already a terminating
ParameterArgumentValidationErrorNullNotAllowed from the binder itself,
unchanged by this fix.
ParameterConversion gains the message for this case next to the
existing blank-value message, so that the wording is built in one
place.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
1 week ago
# stay a terminating error, the same as a bad named value; only a $null that a piped object's property
# carries keeps the parameter's own default.
$accessNamedNullCases = foreach ( $cmdlet in 'Add-NTFSAccess' , 'Remove-NTFSAccess' ) {
@ { Cmdlet = $cmdlet ; Parameter = 'AccessType' }
@ { Cmdlet = $cmdlet ; Parameter = 'InheritanceFlags' }
@ { Cmdlet = $cmdlet ; Parameter = 'PropagationFlags' }
@ { Cmdlet = $cmdlet ; Parameter = 'AppliesTo' }
}
fix(access): report a piped AccessType, AuditFlags, flags, or scope that isn't valid
Add-NTFSAccess, Remove-NTFSAccess, Add-NTFSAudit, and Remove-NTFSAudit
silently kept the default value of AccessType, AuditFlags,
InheritanceFlags, PropagationFlags, and AppliesTo when a piped object's
property could not be converted to the parameter's type: a misspelled
AccessType such as "Denyy", or a blank CSV cell, created an Allow entry
with no message. For a permissions tool, a typo for Deny that silently
grants access is a security-relevant defect (Decision 27).
The five parameters are now declared as object and converted inside
the cmdlet, the way the PowerShell binder converts a typed parameter
(names in any case, comma-separated flag lists, numbers, enum
instances). A value that cannot be converted, including a blank one,
is now an error named InvalidParameterValue: terminating and
catchable when it is named on the command line, exactly as the binder
reported it before; a non-terminating error targeting that path or
descriptor when it comes from a piped object, which leaves that item
unchanged and lets the pipeline continue with the next one. A $null
value is still not an error and keeps the parameter's default. Tab
completion of the five parameters keeps working through explicit
argument completers. Choosing between the Simple and Complex
parameter sets is unaffected and still rejects AppliesTo together
with InheritanceFlags or PropagationFlags as AmbiguousParameterSet,
for every mix of string and enum values.
The four cmdlet pages document the parameters as Type: Object, list
the valid values, and describe the new error; a sentence that said a
piped AppliesTo typo or blank cell was silently ignored is corrected.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
1 week ago
$accessValidAccessTypeShapeCases = @ (
@ { Description = 'the exact name' ; Value = 'Deny' }
@ { Description = 'a different case' ; Value = 'deny' }
@ { Description = 'the enum value' ; Value = [System.Security.AccessControl.AccessControlType] :: Deny }
@ { Description = 'an integer' ; Value = 1 }
fix(access): reject a list for -AccessType and -AppliesTo, and complete in capitals in Turkish
-AccessType and -AppliesTo of Add-NTFSAccess, Remove-NTFSAccess,
Add-NTFSAudit (-AppliesTo), and Remove-NTFSAudit (-AppliesTo) take one value,
but PowerShell adds up the numbers of a comma list or an array for an
enumeration, so that -AppliesTo 'ThisFolderOnly,FilesOnly' created a
FilesOnly entry and -AccessType 'Allow,Deny' created a Deny entry, without a
message. The conversion in ParameterConversion now rejects a string with a
comma and a collection of more than one element for an enumeration that is
not a flags enumeration, with the same InvalidParameterValue error as for a
misspelled name (terminating for a named value, one error per object for a
piped one). A single name and a collection of one element convert as before,
and the flags parameters still take a list. The valid-value matrices of both
published rc8 and this build give the same entries in both editions, named,
piped by property name, and splatted, apart from the nine list rows.
The completers of the five parameters compared with WildcardOptions.IgnoreCase
only, so that a word typed in capitals did not complete while the culture of
the thread was Turkish (an I and an i are different letters there, and
THISFOLDERO did not match ThisFolderOnly). They compare culture invariant now,
like the filter of Get-ChildItem2.
The tests pin both, red before the change and green after it in four
configurations, and the headers of the two test files no longer say that the
-SecurityDescriptor sets have no handler per descriptor.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
1 week ago
@ { Description = 'an array of one name' ; Value = @ ( 'Deny' ) }
fix(access): reject a list of any kind of collection for -AccessType and -AppliesTo
The rejection of a list for the enumerations that are not flags
enumerations looked only at a string with a comma and at an IList of more
than one element. A collection that is not an IList (a HashSet, a Queue, the
keys of a hashtable, a LINQ sequence), a collection of one element that
holds a comma list (@('Allow,Deny'), such as a JSON array of one string), and
a collection that holds a collection of several elements still reached
LanguagePrimitives.ConvertTo, which added up the numbers: -AppliesTo
$scopes.Keys created a ThisFolderAndFiles entry that nobody named.
ParameterConversion takes the one value out of the argument now: a wrapped
PSObject counts as what it wraps, a collection of one element converts as
its element (also when that element is a collection of one element), and a
collection of several elements or an element with a comma is the list that is
rejected. A dictionary, an empty collection, and a collection that cannot be
enumerated keep going to the conversion, which reports them as before. A
blank value inside a collection of one element is reported as blank.
The tests add the shapes to the bad-value tables of the four cmdlets (32
rows, red before the change and green after it in four configurations) and
pin that a list of one name and a hash set of one name stay valid. The
valid-value matrices of rc8 and this build still differ in the nine list rows
only, and a probe of 50 shapes shows every collection of several values
rejected and every single value accepted, in both editions.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
1 week ago
@ { Description = 'a list of one name' ; Value = [System.Collections.Generic.List[string]] :: new ( [string[]] @ ( 'Deny' ) ) }
@ { Description = 'a hash set of one name' ; Value = [System.Collections.Generic.HashSet[string]] :: new ( [string[]] @ ( 'Deny' ) ) }
fix(access): end a collection that holds itself and name the typo in the message of a list
The check that rejects a list for -AccessType and -AppliesTo opened every level
of a collection of one element, so that a collection that holds itself never
returned. It opens at most 32 levels now and counts a deeper nesting as a list,
and it stops reading a collection after the second element, so that a long or
endless sequence is not read to its end.
The message names what the caller wrote: the one element of a collection of one
element (also when that element is a collection itself), the first ten elements
of a longer collection followed by "...", and otherwise the text of the value.
Before, a collection of one misspelled name was named by its type.
Tests, written first: 28 rows failed on the previous build in all four
configurations and none fails now. They cover an array of one misspelled name,
a collection nested 40 levels deep for both parameters of both cmdlets, and an
array that holds an array of one name, which stays valid. The assertion of the
value text expects "The value '<text>'", because the old pattern could also
match the name of a type. The comments say that PowerShell combines the numbers
of a list bit by bit, which is what it does, and not that it adds them.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
1 week ago
@ { Description = 'an array that holds an array of one name' ; Value = , @ ( 'Deny' ) }
fix(access): report a piped AccessType, AuditFlags, flags, or scope that isn't valid
Add-NTFSAccess, Remove-NTFSAccess, Add-NTFSAudit, and Remove-NTFSAudit
silently kept the default value of AccessType, AuditFlags,
InheritanceFlags, PropagationFlags, and AppliesTo when a piped object's
property could not be converted to the parameter's type: a misspelled
AccessType such as "Denyy", or a blank CSV cell, created an Allow entry
with no message. For a permissions tool, a typo for Deny that silently
grants access is a security-relevant defect (Decision 27).
The five parameters are now declared as object and converted inside
the cmdlet, the way the PowerShell binder converts a typed parameter
(names in any case, comma-separated flag lists, numbers, enum
instances). A value that cannot be converted, including a blank one,
is now an error named InvalidParameterValue: terminating and
catchable when it is named on the command line, exactly as the binder
reported it before; a non-terminating error targeting that path or
descriptor when it comes from a piped object, which leaves that item
unchanged and lets the pipeline continue with the next one. A $null
value is still not an error and keeps the parameter's default. Tab
completion of the five parameters keeps working through explicit
argument completers. Choosing between the Simple and Complex
parameter sets is unaffected and still rejects AppliesTo together
with InheritanceFlags or PropagationFlags as AmbiguousParameterSet,
for every mix of string and enum values.
The four cmdlet pages document the parameters as Type: Object, list
the valid values, and describe the new error; a sentence that said a
piped AppliesTo typo or blank cell was silently ignored is corrected.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
1 week ago
)
$accessValidFlagsShapeCases = @ (
@ { Description = 'a comma list without spaces' ; Value = 'ContainerInherit,ObjectInherit' }
@ { Description = 'a comma list with spaces' ; Value = 'ContainerInherit, ObjectInherit' }
@ { Description = 'the enum value' ; Value = [System.Security.AccessControl.InheritanceFlags] 'ContainerInherit, ObjectInherit' }
)
$accessAmbiguousValueCases = @ (
@ { AppliesToShape = 'a string' ; AppliesToIsEnum = $false ; FlagShape = 'a string' ; FlagIsEnum = $false }
@ { AppliesToShape = 'an enum value' ; AppliesToIsEnum = $true ; FlagShape = 'a string' ; FlagIsEnum = $false }
@ { AppliesToShape = 'a string' ; AppliesToIsEnum = $false ; FlagShape = 'an enum value' ; FlagIsEnum = $true }
@ { AppliesToShape = 'an enum value' ; AppliesToIsEnum = $true ; FlagShape = 'an enum value' ; FlagIsEnum = $true }
)
$accessCompleterCases = foreach ( $cmdlet in 'Add-NTFSAccess' , 'Remove-NTFSAccess' ) {
@ { Cmdlet = $cmdlet ; Parameter = 'AccessType' ; EnumTypeName = 'System.Security.AccessControl.AccessControlType' }
@ { Cmdlet = $cmdlet ; Parameter = 'InheritanceFlags' ; EnumTypeName = 'System.Security.AccessControl.InheritanceFlags' }
@ { Cmdlet = $cmdlet ; Parameter = 'PropagationFlags' ; EnumTypeName = 'System.Security.AccessControl.PropagationFlags' }
@ { Cmdlet = $cmdlet ; Parameter = 'AppliesTo' ; EnumTypeName = 'Security2.ApplyTo' }
}
fix(access): complete quoted and wildcard words of the five parameters
The completers for AccessType, AuditFlags, InheritanceFlags,
PropagationFlags, and AppliesTo returned nothing for a quoted word
(-AccessType "D) or a wildcard word (-AppliesTo *Only, -AccessType
D*), which rc8's built-in enum-parameter completion still offered.
PowerShell auto-closes an unterminated quote before it reaches the
completer, so the word a completer receives for -AccessType "D is
"D", not D.
EnumArgumentCompletion.Complete now strips a leading quote and its
matching trailing quote from the word before matching, matches the
enumeration's names with WildcardPattern(word + "*",
WildcardOptions.IgnoreCase) the way PowerShell's own enum completion
does, and, when the word was quoted, returns each match in the same
quote character (CompletionText 'FilesOnly' for a leading single
quote). An unquoted, non-wildcard word is unaffected: appending "*"
to it only turns an exact prefix match into a prefix-wildcard match.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
1 week ago
# A quoted or a wildcard word completed while these parameters were typed as enumerations, so the completers
# must offer the names for either shape too. Each ExpectedName has no sibling name that also starts with it
# (ThisFolderOnly has no ThisFolderOnlyOneLevel), so the unquoted and quoted forms match it alone; the
# wildcard only needs to contain it among the matches.
fix(access): complete quoted and wildcard words of the five parameters
The completers for AccessType, AuditFlags, InheritanceFlags,
PropagationFlags, and AppliesTo returned nothing for a quoted word
(-AccessType "D) or a wildcard word (-AppliesTo *Only, -AccessType
D*), which rc8's built-in enum-parameter completion still offered.
PowerShell auto-closes an unterminated quote before it reaches the
completer, so the word a completer receives for -AccessType "D is
"D", not D.
EnumArgumentCompletion.Complete now strips a leading quote and its
matching trailing quote from the word before matching, matches the
enumeration's names with WildcardPattern(word + "*",
WildcardOptions.IgnoreCase) the way PowerShell's own enum completion
does, and, when the word was quoted, returns each match in the same
quote character (CompletionText 'FilesOnly' for a leading single
quote). An unquoted, non-wildcard word is unaffected: appending "*"
to it only turns an exact prefix match into a prefix-wildcard match.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
1 week ago
$accessCompletionFormCases = @ (
@ { Parameter = 'AccessType' ; Word = 'D' ; ExpectedName = 'Deny' }
@ { Parameter = 'InheritanceFlags' ; Word = 'Container' ; ExpectedName = 'ContainerInherit' }
@ { Parameter = 'PropagationFlags' ; Word = 'Inh' ; ExpectedName = 'InheritOnly' }
@ { Parameter = 'AppliesTo' ; Word = 'ThisFolderO' ; ExpectedName = 'ThisFolderOnly' }
)
docs(access): say what a blank, null, or script block value of a property does, and pin it
R1-1 (major): the four pages claimed that a blank cell of AppliesTo, AccessType or
AuditFlags, InheritanceFlags, or PropagationFlags is an InvalidParameterValue error
for every shape. That is wrong when the blank, $null, or DBNull value belongs to
the group that -AppliesTo did not select: PowerShell counts a property that is
present, even blank, as offered for choosing the parameter set, the same as a
real value, so AppliesTo together with a blank InheritanceFlags or
PropagationFlags (for example a short CSV row, whose missing trailing columns
Import-Csv fills with $null) is AmbiguousParameterSet, and rc8 created the
entry. The pages now say that a CSV file should have the columns of only one
group, that mixing both groups fails with AmbiguousParameterSet, that a literal
$null named or piped for -AppliesTo itself fails with its own, unrelated,
pre-existing error because -AppliesTo is mandatory, and keep the sentence that a
bad value within the chosen group is InvalidParameterValue.
R1-3 (minor): the pages now also say that a script block is a plain value for
these parameters and is not evaluated once for each piped object the way rc8
evaluated it, so a named script block is a terminating InvalidParameterValue
error.
Neither finding needed a code change; both already read this way. Pinned with
new table-driven tests, over the four cmdlets and their Path and SD parameter
sets, for AppliesTo valid combined with a $null, an empty string, or DBNull
InheritanceFlags or PropagationFlags and the reverse, and for a named script
block of each of the five parameters: the error id the binder already gives,
nothing changed, and, where applicable, the next piped object still processed.
Confirmed unchanged against the pre-R1-2/R1-4 build.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
1 week ago
fix(access): reject a list for -AccessType and -AppliesTo, and complete in capitals in Turkish
-AccessType and -AppliesTo of Add-NTFSAccess, Remove-NTFSAccess,
Add-NTFSAudit (-AppliesTo), and Remove-NTFSAudit (-AppliesTo) take one value,
but PowerShell adds up the numbers of a comma list or an array for an
enumeration, so that -AppliesTo 'ThisFolderOnly,FilesOnly' created a
FilesOnly entry and -AccessType 'Allow,Deny' created a Deny entry, without a
message. The conversion in ParameterConversion now rejects a string with a
comma and a collection of more than one element for an enumeration that is
not a flags enumeration, with the same InvalidParameterValue error as for a
misspelled name (terminating for a named value, one error per object for a
piped one). A single name and a collection of one element convert as before,
and the flags parameters still take a list. The valid-value matrices of both
published rc8 and this build give the same entries in both editions, named,
piped by property name, and splatted, apart from the nine list rows.
The completers of the five parameters compared with WildcardOptions.IgnoreCase
only, so that a word typed in capitals did not complete while the culture of
the thread was Turkish (an I and an i are different letters there, and
THISFOLDERO did not match ThisFolderOnly). They compare culture invariant now,
like the filter of Get-ChildItem2.
The tests pin both, red before the change and green after it in four
configurations, and the headers of the two test files no longer say that the
-SecurityDescriptor sets have no handler per descriptor.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
1 week ago
# In Turkish, a case-insensitive comparison of the culture of the thread treats I and i as different letters, so
# a word typed in capitals completes only when the completers compare culture invariant.
$accessTurkishCompletionCases = @ (
@ { Parameter = 'AppliesTo' ; Word = 'THISFOLDERO' ; ExpectedName = 'ThisFolderOnly' }
@ { Parameter = 'InheritanceFlags' ; Word = 'CONTAINERINH' ; ExpectedName = 'ContainerInherit' }
@ { Parameter = 'PropagationFlags' ; Word = 'NOPROPAGATEINHERI' ; ExpectedName = 'NoPropagateInherit' }
)
# Because the five parameters are object, PowerShell counts a $null, an empty string, or DBNull value of a
# property as present when it chooses the parameter set, the same as a real value, so AppliesTo (the Simple
# group) together with a $null/''/DBNull InheritanceFlags or PropagationFlags (the Complex group) offers
# both groups and is AmbiguousParameterSet, not InvalidParameterValue. The reverse (AppliesTo blank, a
# Complex property valid) is AmbiguousParameterSet too, except that AppliesTo is Mandatory, so a literal
# $null of it (unlike '' or DBNull) is rejected earlier by the binder's own Mandatory check. Covers the
# Path and SD sets of the access cmdlets; the audit cmdlets are pinned in ParameterSets.Audit.Tests.ps1.
docs(access): say what a blank, null, or script block value of a property does, and pin it
R1-1 (major): the four pages claimed that a blank cell of AppliesTo, AccessType or
AuditFlags, InheritanceFlags, or PropagationFlags is an InvalidParameterValue error
for every shape. That is wrong when the blank, $null, or DBNull value belongs to
the group that -AppliesTo did not select: PowerShell counts a property that is
present, even blank, as offered for choosing the parameter set, the same as a
real value, so AppliesTo together with a blank InheritanceFlags or
PropagationFlags (for example a short CSV row, whose missing trailing columns
Import-Csv fills with $null) is AmbiguousParameterSet, and rc8 created the
entry. The pages now say that a CSV file should have the columns of only one
group, that mixing both groups fails with AmbiguousParameterSet, that a literal
$null named or piped for -AppliesTo itself fails with its own, unrelated,
pre-existing error because -AppliesTo is mandatory, and keep the sentence that a
bad value within the chosen group is InvalidParameterValue.
R1-3 (minor): the pages now also say that a script block is a plain value for
these parameters and is not evaluated once for each piped object the way rc8
evaluated it, so a named script block is a terminating InvalidParameterValue
error.
Neither finding needed a code change; both already read this way. Pinned with
new table-driven tests, over the four cmdlets and their Path and SD parameter
sets, for AppliesTo valid combined with a $null, an empty string, or DBNull
InheritanceFlags or PropagationFlags and the reverse, and for a named script
block of each of the five parameters: the error id the binder already gives,
nothing changed, and, where applicable, the next piped object still processed.
Confirmed unchanged against the pre-R1-2/R1-4 build.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
1 week ago
$accessAmbiguousOtherGroupCases = foreach ( $cmdlet in 'Add-NTFSAccess' , 'Remove-NTFSAccess' ) {
foreach ( $targetKind in 'Path' , 'SD' ) {
foreach ( $shape in 'a $null' , 'an empty string' , 'DBNull' ) {
$blankValue = switch ( $shape ) { 'a $null' { $null } ; 'an empty string' { '' } ; 'DBNull' { [System.DBNull] :: Value } }
@ {
Cmdlet = $cmdlet ; TargetKind = $targetKind ; Shape = $shape ; BlankValue = $blankValue
Direction = 'AppliesTo valid, InheritanceFlags blank' ; AppliesToIsBlank = $false
ExpectedErrorId = 'AmbiguousParameterSet'
}
@ {
Cmdlet = $cmdlet ; TargetKind = $targetKind ; Shape = $shape ; BlankValue = $blankValue
Direction = 'AppliesTo blank, PropagationFlags valid' ; AppliesToIsBlank = $true
ExpectedErrorId = $ ( if ( $shape -eq 'a $null' ) { 'ParameterArgumentValidationErrorNullNotAllowed' } else { 'AmbiguousParameterSet' } )
}
}
}
}
# With the object type, a script block is a plain value (a typed parameter evaluates it once per piped
# object), so it fails the same way any other value of the wrong shape does: a terminating
# InvalidParameterValue from ValidateNamedValue in BeginProcessing, before any piped object is processed.
docs(access): say what a blank, null, or script block value of a property does, and pin it
R1-1 (major): the four pages claimed that a blank cell of AppliesTo, AccessType or
AuditFlags, InheritanceFlags, or PropagationFlags is an InvalidParameterValue error
for every shape. That is wrong when the blank, $null, or DBNull value belongs to
the group that -AppliesTo did not select: PowerShell counts a property that is
present, even blank, as offered for choosing the parameter set, the same as a
real value, so AppliesTo together with a blank InheritanceFlags or
PropagationFlags (for example a short CSV row, whose missing trailing columns
Import-Csv fills with $null) is AmbiguousParameterSet, and rc8 created the
entry. The pages now say that a CSV file should have the columns of only one
group, that mixing both groups fails with AmbiguousParameterSet, that a literal
$null named or piped for -AppliesTo itself fails with its own, unrelated,
pre-existing error because -AppliesTo is mandatory, and keep the sentence that a
bad value within the chosen group is InvalidParameterValue.
R1-3 (minor): the pages now also say that a script block is a plain value for
these parameters and is not evaluated once for each piped object the way rc8
evaluated it, so a named script block is a terminating InvalidParameterValue
error.
Neither finding needed a code change; both already read this way. Pinned with
new table-driven tests, over the four cmdlets and their Path and SD parameter
sets, for AppliesTo valid combined with a $null, an empty string, or DBNull
InheritanceFlags or PropagationFlags and the reverse, and for a named script
block of each of the five parameters: the error id the binder already gives,
nothing changed, and, where applicable, the next piped object still processed.
Confirmed unchanged against the pre-R1-2/R1-4 build.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
1 week ago
$accessScriptBlockCases = foreach ( $cmdlet in 'Add-NTFSAccess' , 'Remove-NTFSAccess' ) {
@ { Cmdlet = $cmdlet ; Parameter = 'AccessType' }
@ { Cmdlet = $cmdlet ; Parameter = 'InheritanceFlags' }
@ { Cmdlet = $cmdlet ; Parameter = 'PropagationFlags' }
@ { Cmdlet = $cmdlet ; Parameter = 'AppliesTo' }
}
fix(access): report a piped AccessType, AuditFlags, flags, or scope that isn't valid
Add-NTFSAccess, Remove-NTFSAccess, Add-NTFSAudit, and Remove-NTFSAudit
silently kept the default value of AccessType, AuditFlags,
InheritanceFlags, PropagationFlags, and AppliesTo when a piped object's
property could not be converted to the parameter's type: a misspelled
AccessType such as "Denyy", or a blank CSV cell, created an Allow entry
with no message. For a permissions tool, a typo for Deny that silently
grants access is a security-relevant defect (Decision 27).
The five parameters are now declared as object and converted inside
the cmdlet, the way the PowerShell binder converts a typed parameter
(names in any case, comma-separated flag lists, numbers, enum
instances). A value that cannot be converted, including a blank one,
is now an error named InvalidParameterValue: terminating and
catchable when it is named on the command line, exactly as the binder
reported it before; a non-terminating error targeting that path or
descriptor when it comes from a piped object, which leaves that item
unchanged and lets the pipeline continue with the next one. A $null
value is still not an error and keeps the parameter's default. Tab
completion of the five parameters keeps working through explicit
argument completers. Choosing between the Simple and Complex
parameter sets is unaffected and still rejects AppliesTo together
with InheritanceFlags or PropagationFlags as AmbiguousParameterSet,
for every mix of string and enum values.
The four cmdlet pages document the parameters as Type: Object, list
the valid values, and describe the new error; a sentence that said a
piped AppliesTo typo or blank cell was silently ignored is corrected.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
1 week ago
}
Context 'A bad value bound from the pipeline' {
It 'Should write one InvalidParameterValue error for <Cmdlet> -<Parameter> (<Shape>), change nothing, and still process the next object' -ForEach $accessInvalidParameterCases {
$path = New-TestSandboxItem -Sandbox $sandbox -Name 'InvalidValue' -Directory
$next = New-TestSandboxItem -Sandbox $sandbox -Name 'InvalidValueNext' -Directory
$before = ( Get-Acl -LiteralPath $path ) . Sddl
$extra = @ { }
$extra [ $Parameter ] = $Value
$rows = @ (
[pscustomobject] ( @ { Path = $path ; Account = 'S-1-1-0' ; AccessRights = 'ReadData' } + $extra )
[pscustomobject] @ { Path = $next ; Account = 'S-1-1-0' ; AccessRights = 'ReadData' }
)
$result = @ ( $rows | & $Cmdlet -PassThru -ErrorVariable pipelineErrors -ErrorAction SilentlyContinue )
$pipelineErrors | Should -HaveCount 1
$pipelineErrors [ 0 ] . FullyQualifiedErrorId | Should -BeLike 'InvalidParameterValue,*'
$pipelineErrors [ 0 ] . CategoryInfo . Category | Should -Be 'InvalidArgument'
$pipelineErrors [ 0 ] . TargetObject | Should -Be $path
$pipelineErrors [ 0 ] . Exception . Message | Should -Match ( [regex] :: Escape ( $Parameter ) )
if ( $ValueText ) {
fix(access): end a collection that holds itself and name the typo in the message of a list
The check that rejects a list for -AccessType and -AppliesTo opened every level
of a collection of one element, so that a collection that holds itself never
returned. It opens at most 32 levels now and counts a deeper nesting as a list,
and it stops reading a collection after the second element, so that a long or
endless sequence is not read to its end.
The message names what the caller wrote: the one element of a collection of one
element (also when that element is a collection itself), the first ten elements
of a longer collection followed by "...", and otherwise the text of the value.
Before, a collection of one misspelled name was named by its type.
Tests, written first: 28 rows failed on the previous build in all four
configurations and none fails now. They cover an array of one misspelled name,
a collection nested 40 levels deep for both parameters of both cmdlets, and an
array that holds an array of one name, which stays valid. The assertion of the
value text expects "The value '<text>'", because the old pattern could also
match the name of a type. The comments say that PowerShell combines the numbers
of a list bit by bit, which is what it does, and not that it adds them.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
1 week ago
$pipelineErrors [ 0 ] . Exception . Message | Should -Match ( " The value ' " + [regex] :: Escape ( $ValueText ) + " ' " )
fix(access): report a piped AccessType, AuditFlags, flags, or scope that isn't valid
Add-NTFSAccess, Remove-NTFSAccess, Add-NTFSAudit, and Remove-NTFSAudit
silently kept the default value of AccessType, AuditFlags,
InheritanceFlags, PropagationFlags, and AppliesTo when a piped object's
property could not be converted to the parameter's type: a misspelled
AccessType such as "Denyy", or a blank CSV cell, created an Allow entry
with no message. For a permissions tool, a typo for Deny that silently
grants access is a security-relevant defect (Decision 27).
The five parameters are now declared as object and converted inside
the cmdlet, the way the PowerShell binder converts a typed parameter
(names in any case, comma-separated flag lists, numbers, enum
instances). A value that cannot be converted, including a blank one,
is now an error named InvalidParameterValue: terminating and
catchable when it is named on the command line, exactly as the binder
reported it before; a non-terminating error targeting that path or
descriptor when it comes from a piped object, which leaves that item
unchanged and lets the pipeline continue with the next one. A $null
value is still not an error and keeps the parameter's default. Tab
completion of the five parameters keeps working through explicit
argument completers. Choosing between the Simple and Complex
parameter sets is unaffected and still rejects AppliesTo together
with InheritanceFlags or PropagationFlags as AmbiguousParameterSet,
for every mix of string and enum values.
The four cmdlet pages document the parameters as Type: Object, list
the valid values, and describe the new error; a sentence that said a
piped AppliesTo typo or blank cell was silently ignored is corrected.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
1 week ago
}
( Get-Acl -LiteralPath $path ) . Sddl | Should -BeExactly $before
$result | Should -Not -BeNullOrEmpty
$result | ForEach-Object -Process { $_ . FullName | Should -Be $next }
}
It 'Should write one InvalidParameterValue error per descriptor for <Cmdlet> -<Parameter> bound to a piped descriptor, and leave the descriptors unchanged' -ForEach $accessSdInvalidParameterCases {
$first = New-TestSandboxItem -Sandbox $sandbox -Name 'InvalidSdFirst' -Directory
$second = New-TestSandboxItem -Sandbox $sandbox -Name 'InvalidSdSecond' -Directory
$descriptors = @ ( Get-NTFSSecurityDescriptor -Path $first , $second -ErrorAction Stop )
$before = $descriptors [ 0 ] . SecurityDescriptor . GetSecurityDescriptorSddlForm ( 'Access' )
$extra = @ { }
$extra [ $Parameter ] = $Value
$rows = @ (
[pscustomobject] ( @ { SecurityDescriptor = $descriptors [ 0 ] ; Account = 'S-1-1-0' ; AccessRights = 'ReadData' } + $extra )
[pscustomobject] @ { SecurityDescriptor = $descriptors [ 1 ] ; Account = 'S-1-1-0' ; AccessRights = 'ReadData' }
)
$result = @ ( $rows | & $Cmdlet -PassThru -ErrorVariable pipelineErrors -ErrorAction SilentlyContinue )
$pipelineErrors | Should -HaveCount 1
$pipelineErrors [ 0 ] . FullyQualifiedErrorId | Should -BeLike 'InvalidParameterValue,*'
$pipelineErrors [ 0 ] . CategoryInfo . Category | Should -Be 'InvalidArgument'
$pipelineErrors [ 0 ] . TargetObject | Should -Be $descriptors [ 0 ]
$descriptors [ 0 ] . SecurityDescriptor . GetSecurityDescriptorSddlForm ( 'Access' ) | Should -BeExactly $before
$result | Should -Not -BeNullOrEmpty
$result | ForEach-Object -Process { $_ . FullName | Should -Be $second }
}
}
Context 'Continuation and no carry-over across piped objects' {
It 'Should apply both good objects when a bad AccessType value is between them, in the order of the rows' {
$first = New-TestSandboxItem -Sandbox $sandbox -Name 'ContinueFirst' -Directory
$middle = New-TestSandboxItem -Sandbox $sandbox -Name 'ContinueMiddle' -Directory
$last = New-TestSandboxItem -Sandbox $sandbox -Name 'ContinueLast' -Directory
$rows = @ (
[pscustomobject] @ { Path = $first ; Account = 'S-1-1-0' ; AccessRights = 'ReadData' ; AccessType = 'Deny' }
[pscustomobject] @ { Path = $middle ; Account = 'S-1-1-0' ; AccessRights = 'ReadData' ; AccessType = 'Denyy' }
[pscustomobject] @ { Path = $last ; Account = 'S-1-1-0' ; AccessRights = 'ReadData' ; AccessType = 'Deny' }
)
$result = @ ( $rows | Add-NTFSAccess -PassThru -ErrorVariable pipelineErrors -ErrorAction SilentlyContinue )
$pipelineErrors | Should -HaveCount 1
$pipelineErrors [ 0 ] . TargetObject | Should -Be $middle
$firstRule = @ ( ( Get-Acl -LiteralPath $first ) . GetAccessRules ( $true , $false , $sidType ) | Where-Object -FilterScript { $_ . IdentityReference . Value -eq 'S-1-1-0' } )
$firstRule | Should -HaveCount 1
$firstRule [ 0 ] . AccessControlType | Should -Be ( [System.Security.AccessControl.AccessControlType] :: Deny )
$middleRule = @ ( ( Get-Acl -LiteralPath $middle ) . GetAccessRules ( $true , $false , $sidType ) | Where-Object -FilterScript { $_ . IdentityReference . Value -eq 'S-1-1-0' } )
$middleRule | Should -BeNullOrEmpty
$lastRule = @ ( ( Get-Acl -LiteralPath $last ) . GetAccessRules ( $true , $false , $sidType ) | Where-Object -FilterScript { $_ . IdentityReference . Value -eq 'S-1-1-0' } )
$lastRule | Should -HaveCount 1
$lastRule [ 0 ] . AccessControlType | Should -Be ( [System.Security.AccessControl.AccessControlType] :: Deny )
# The rows are applied in order: both surviving entries show up, $middle's does not, and nothing reports twice.
@ ( $result | Where-Object -FilterScript { $_ . FullName -eq $first -and $_ . Account . Sid -eq 'S-1-1-0' } ) | Should -HaveCount 1
@ ( $result | Where-Object -FilterScript { $_ . FullName -eq $last -and $_ . Account . Sid -eq 'S-1-1-0' } ) | Should -HaveCount 1
@ ( $result | Where-Object -FilterScript { $_ . FullName -eq $middle -and $_ . Account . Sid -eq 'S-1-1-0' } ) | Should -BeNullOrEmpty
}
It 'Should give the row without AccessType the default Allow after a preceding Deny row, with no carry-over' {
$denyFolder = New-TestSandboxItem -Sandbox $sandbox -Name 'NoCarryOverDeny' -Directory
$defaultFolder = New-TestSandboxItem -Sandbox $sandbox -Name 'NoCarryOverDefault' -Directory
@ (
[pscustomobject] @ { Path = $denyFolder ; Account = 'S-1-1-0' ; AccessRights = 'ReadData' ; AccessType = 'Deny' }
[pscustomobject] @ { Path = $defaultFolder ; Account = 'S-1-1-0' ; AccessRights = 'ReadData' }
) | Add-NTFSAccess -ErrorAction Stop
$denyRule = @ ( ( Get-Acl -LiteralPath $denyFolder ) . GetAccessRules ( $true , $false , $sidType ) | Where-Object -FilterScript { $_ . IdentityReference . Value -eq 'S-1-1-0' } )
$denyRule | Should -HaveCount 1
$denyRule [ 0 ] . AccessControlType | Should -Be ( [System.Security.AccessControl.AccessControlType] :: Deny )
$defaultRule = @ ( ( Get-Acl -LiteralPath $defaultFolder ) . GetAccessRules ( $true , $false , $sidType ) | Where-Object -FilterScript { $_ . IdentityReference . Value -eq 'S-1-1-0' } )
$defaultRule | Should -HaveCount 1
$defaultRule [ 0 ] . AccessControlType | Should -Be ( [System.Security.AccessControl.AccessControlType] :: Allow )
}
It 'Should give the row without <Parameter> its own default after a preceding row with an invalid value, with no carry-over' -ForEach $accessNoCarryOverCases {
$badFolder = New-TestSandboxItem -Sandbox $sandbox -Name 'NoCarryOverBad' -Directory
$defaultFolder = New-TestSandboxItem -Sandbox $sandbox -Name 'NoCarryOverAfterBad' -Directory
$extra = @ { }
$extra [ $Parameter ] = $Value
@ (
[pscustomobject] ( @ { Path = $badFolder ; Account = 'S-1-1-0' ; AccessRights = 'ReadData' } + $extra )
[pscustomobject] @ { Path = $defaultFolder ; Account = 'S-1-1-0' ; AccessRights = 'ReadData' }
) | & $Cmdlet -ErrorAction SilentlyContinue
$defaultRule = @ ( ( Get-Acl -LiteralPath $defaultFolder ) . GetAccessRules ( $true , $false , $sidType ) | Where-Object -FilterScript { $_ . IdentityReference . Value -eq 'S-1-1-0' } )
$defaultRule | Should -HaveCount 1
$defaultRule [ 0 ] . AccessControlType | Should -Be ( [System.Security.AccessControl.AccessControlType] :: Allow )
$defaultRule [ 0 ] . InheritanceFlags | Should -Be ( [System.Security.AccessControl.InheritanceFlags] 'ContainerInherit, ObjectInherit' )
$defaultRule [ 0 ] . PropagationFlags | Should -Be ( [System.Security.AccessControl.PropagationFlags] :: None )
}
}
Context 'Valid values are unchanged for every shape' {
It 'Should add a Deny entry for AccessType given as <Description>' -ForEach $accessValidAccessTypeShapeCases {
$folder = New-TestSandboxItem -Sandbox $sandbox -Name 'ValidAccessType' -Directory
[pscustomobject] @ { Path = $folder ; Account = 'S-1-1-0' ; AccessRights = 'ReadData' ; AccessType = $Value } | Add-NTFSAccess -ErrorAction Stop
$rule = @ ( ( Get-Acl -LiteralPath $folder ) . GetAccessRules ( $true , $false , $sidType ) | Where-Object -FilterScript { $_ . IdentityReference . Value -eq 'S-1-1-0' } )
$rule | Should -HaveCount 1
$rule [ 0 ] . AccessControlType | Should -Be ( [System.Security.AccessControl.AccessControlType] :: Deny )
}
It 'Should keep the default AccessType Allow for a $null value' {
$folder = New-TestSandboxItem -Sandbox $sandbox -Name 'ValidAccessTypeNull' -Directory
[pscustomobject] @ { Path = $folder ; Account = 'S-1-1-0' ; AccessRights = 'ReadData' ; AccessType = $null } | Add-NTFSAccess -ErrorAction Stop
$rule = @ ( ( Get-Acl -LiteralPath $folder ) . GetAccessRules ( $true , $false , $sidType ) | Where-Object -FilterScript { $_ . IdentityReference . Value -eq 'S-1-1-0' } )
$rule | Should -HaveCount 1
$rule [ 0 ] . AccessControlType | Should -Be ( [System.Security.AccessControl.AccessControlType] :: Allow )
}
It 'Should scope the entry with InheritanceFlags given as <Description>' -ForEach $accessValidFlagsShapeCases {
$folder = New-TestSandboxItem -Sandbox $sandbox -Name 'ValidInheritanceFlags' -Directory
[pscustomobject] @ { Path = $folder ; Account = 'S-1-1-0' ; AccessRights = 'ReadData' ; InheritanceFlags = $Value ; PropagationFlags = 'None' } | Add-NTFSAccess -ErrorAction Stop
$rule = @ ( ( Get-Acl -LiteralPath $folder ) . GetAccessRules ( $true , $false , $sidType ) | Where-Object -FilterScript { $_ . IdentityReference . Value -eq 'S-1-1-0' } )
$rule | Should -HaveCount 1
$rule [ 0 ] . InheritanceFlags | Should -Be ( [System.Security.AccessControl.InheritanceFlags] 'ContainerInherit, ObjectInherit' )
}
It 'Should keep the default flags for $null InheritanceFlags and PropagationFlags' {
$folder = New-TestSandboxItem -Sandbox $sandbox -Name 'ValidFlagsNull' -Directory
[pscustomobject] @ { Path = $folder ; Account = 'S-1-1-0' ; AccessRights = 'ReadData' ; InheritanceFlags = $null ; PropagationFlags = $null } | Add-NTFSAccess -ErrorAction Stop
$rule = @ ( ( Get-Acl -LiteralPath $folder ) . GetAccessRules ( $true , $false , $sidType ) | Where-Object -FilterScript { $_ . IdentityReference . Value -eq 'S-1-1-0' } )
$rule | Should -HaveCount 1
$rule [ 0 ] . InheritanceFlags | Should -Be ( [System.Security.AccessControl.InheritanceFlags] 'ContainerInherit, ObjectInherit' )
$rule [ 0 ] . PropagationFlags | Should -Be ( [System.Security.AccessControl.PropagationFlags] :: None )
}
}
Context 'A bad value named on the command line' {
It 'Should throw a terminating InvalidParameterValue error for <Cmdlet> -<Parameter>, catchable with try/catch, and change nothing' -ForEach $accessNamedInvalidParameterCases {
$folder = New-TestSandboxItem -Sandbox $sandbox -Name 'NamedInvalid' -Directory
$before = ( Get-Acl -LiteralPath $folder ) . Sddl
$namedArguments = @ { Path = $folder ; Account = 'S-1-1-0' ; AccessRights = 'ReadData' }
$namedArguments [ $Parameter ] = $Value
$caughtId = $null
try {
& $Cmdlet @namedArguments -ErrorAction Stop
}
catch {
$caughtId = $_ . FullyQualifiedErrorId
}
$caughtId | Should -BeLike 'InvalidParameterValue,*'
( Get-Acl -LiteralPath $folder ) . Sddl | Should -BeExactly $before
}
It 'Should stop before the first piped path when -AccessType is named and bad' {
$first = New-TestSandboxItem -Sandbox $sandbox -Name 'NamedBadFirst' -Directory
$second = New-TestSandboxItem -Sandbox $sandbox -Name 'NamedBadSecond' -Directory
$before = @ ( ( Get-Acl -LiteralPath $first ) . Sddl , ( Get-Acl -LiteralPath $second ) . Sddl )
$caughtId = $null
try {
$first , $second | Add-NTFSAccess -Account 'S-1-1-0' -AccessRights ReadData -AccessType Denyy -ErrorAction Stop
}
catch {
$caughtId = $_ . FullyQualifiedErrorId
}
$caughtId | Should -BeLike 'InvalidParameterValue,*'
( Get-Acl -LiteralPath $first ) . Sddl | Should -BeExactly $before [ 0 ]
( Get-Acl -LiteralPath $second ) . Sddl | Should -BeExactly $before [ 1 ]
}
}
fix(access): build the message of a bad parameter value even when the text of the value throws
The message of the error is built inside the catch block of the conversion, and
Describe formatted the one element of a collection of one element outside of
any guard. An element whose ToString throws (a script method, for example)
ended the command with a terminating ExtendedTypeSystemException instead of
the InvalidParameterValue error for that item, and a piped command stopped at
that object, where the pages say that the error is reported for just that
item. Describe now falls back to the type name whatever the text or the
enumeration of the value does, for the five parameters of the four cmdlets
alike.
Tests, written first: 24 rows failed on the previous build in all four
configurations and none fails now (an array of one object whose ToString
throws, piped for every parameter and named for one parameter of each kind).
New rows also pin what the previous commit claimed but did not test: the
message names the first ten elements of a list and ends with an ellipsis from
the eleventh, and a sequence of 100,000 elements is read only up to its first
elements (a counting sequence, named and piped). A build without the two limits
fails 4 of them and a build with a limit of nine fails 6, in all four
configurations. The comment of the nesting limit now says exactly what it
counts.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
1 week ago
# The conversion reads at most two elements of a sequence to know that it is a list, and the message names ten, so a
# sequence that is long (or has no end) is not read to its end.
Context 'A long sequence of values' {
It 'Should read only the first elements of a long sequence given as <Mode> -AccessType, and name its first ten in the error' -ForEach @ (
@ { Mode = 'named' }
@ { Mode = 'piped' }
) {
$folder = New-TestSandboxItem -Sandbox $sandbox -Name 'LongSequence' -Directory
$before = ( Get-Acl -LiteralPath $folder ) . Sddl
$counter = @ { Reads = 0 }
$count = { $counter . Reads + + ; 'Allow' } . GetNewClosure ( )
$sequence = [System.Linq.Enumerable] :: Select ( [int[]] ( 1 . . 100000 ) , [Func[int, string]] $count )
$message = $null
if ( $Mode -eq 'named' ) {
try {
Add-NTFSAccess -Path $folder -Account 'S-1-1-0' -AccessRights ReadData -AccessType $sequence -ErrorAction Stop
}
catch {
$message = $_ . Exception . Message
}
}
else {
[pscustomobject] @ { Path = $folder ; Account = 'S-1-1-0' ; AccessRights = 'ReadData' ; AccessType = $sequence } | Add-NTFSAccess -ErrorVariable pipelineErrors -ErrorAction SilentlyContinue
$message = @ ( $pipelineErrors ) [ 0 ] . Exception . Message
}
$message | Should -Match ( 'The value ''' + ( 'Allow, ' * 10 ) + '\.\.\.''' )
$counter . Reads | Should -BeLessThan 50
( Get-Acl -LiteralPath $folder ) . Sddl | Should -BeExactly $before
}
}
# A list is rejected when the conversion has read its second element. The loop that reads it then releases the
# enumerator, and an enumerator that fails there must not turn the list into a value that PowerShell converts.
Context 'A sequence whose enumerator fails when it is released' {
BeforeAll {
if ( -not ( 'NtfsSecurityTests.SequenceWhoseEnumeratorFailsOnDispose' -as [type] ) ) {
Add-Type -TypeDefinition @ '
namespace NtfsSecurityTests
{
public sealed class SequenceWhoseEnumeratorFailsOnDispose : System . Collections . IEnumerable
{
private readonly object [ ] items ;
public SequenceWhoseEnumeratorFailsOnDispose ( params object [ ] items )
{
this . items = items ;
}
public System . Collections . IEnumerator GetEnumerator ( )
{
return new Enumerator ( this . items ) ;
}
private sealed class Enumerator : System . Collections . IEnumerator , System . IDisposable
{
private readonly object [ ] items ;
private int index = - 1 ;
public Enumerator ( object [ ] items )
{
this . items = items ;
}
public object Current
{
get { return this . items [this.index] ; }
}
public bool MoveNext ( )
{
this . index + + ;
return this . index < this . items . Length ;
}
public void Reset ( )
{
this . index = - 1 ;
}
public void Dispose ( )
{
throw new System . InvalidOperationException ( " The enumerator fails when it is released. " ) ;
}
}
}
}
' @
}
}
It 'Should reject <Shape> for <Parameter> given as <Mode>, and change nothing' -ForEach @ (
foreach ( $mode in 'named' , 'piped' ) {
@ { Parameter = 'AccessType' ; Mode = $mode ; Shape = 'a sequence of two values' ; Values = @ ( 'Allow' , 'Deny' ) }
@ { Parameter = 'AccessType' ; Mode = $mode ; Shape = 'a sequence of one comma list' ; Values = @ ( 'Allow,Deny' ) }
@ { Parameter = 'AccessType' ; Mode = $mode ; Shape = 'a sequence of one array of two values' ; Values = , @ ( 'Allow' , 'Deny' ) }
@ { Parameter = 'AppliesTo' ; Mode = $mode ; Shape = 'a sequence of two values' ; Values = @ ( 'SubfoldersOnly' , 'FilesOnly' ) }
@ { Parameter = 'AppliesTo' ; Mode = $mode ; Shape = 'a sequence of one comma list' ; Values = @ ( 'ThisFolderOnly,FilesOnly' ) }
@ { Parameter = 'AppliesTo' ; Mode = $mode ; Shape = 'a sequence of one array of two values' ; Values = , @ ( 'SubfoldersOnly' , 'FilesOnly' ) }
}
) {
$folder = New-TestSandboxItem -Sandbox $sandbox -Name 'ReleaseFails' -Directory
$before = ( Get-Acl -LiteralPath $folder ) . Sddl
$sequence = [NtfsSecurityTests.SequenceWhoseEnumeratorFailsOnDispose] :: new ( [object[]] $Values )
$caughtId = $null
if ( $Mode -eq 'named' ) {
$namedArguments = @ { Path = $folder ; Account = 'S-1-1-0' ; AccessRights = 'ReadData' }
$namedArguments [ $Parameter ] = $sequence
try {
Add-NTFSAccess @namedArguments -ErrorAction Stop
}
catch {
$caughtId = $_ . FullyQualifiedErrorId
}
}
else {
$properties = @ { Path = $folder ; Account = 'S-1-1-0' ; AccessRights = 'ReadData' }
$properties [ $Parameter ] = $sequence
[pscustomobject] $properties | Add-NTFSAccess -ErrorVariable pipelineErrors -ErrorAction SilentlyContinue
$caughtId = @ ( $pipelineErrors ) [ 0 ] . FullyQualifiedErrorId
}
$caughtId | Should -BeLike 'InvalidParameterValue,*'
( Get-Acl -LiteralPath $folder ) . Sddl | Should -BeExactly $before
}
# What was read is what counts: a failing release does not turn one valid value into an error.
It 'Should still accept a sequence of one valid value for -AccessType given as <Mode>' -ForEach @ (
@ { Mode = 'named' }
@ { Mode = 'piped' }
) {
$folder = New-TestSandboxItem -Sandbox $sandbox -Name 'ReleaseFailsValid' -Directory
$sequence = [NtfsSecurityTests.SequenceWhoseEnumeratorFailsOnDispose] :: new ( [object[]] @ ( 'Deny' ) )
if ( $Mode -eq 'named' ) {
Add-NTFSAccess -Path $folder -Account 'S-1-1-0' -AccessRights ReadData -AccessType $sequence -ErrorAction Stop
}
else {
[pscustomobject] @ { Path = $folder ; Account = 'S-1-1-0' ; AccessRights = 'ReadData' ; AccessType = $sequence } | Add-NTFSAccess -ErrorAction Stop
}
$rule = @ ( ( Get-Acl -LiteralPath $folder ) . GetAccessRules ( $true , $false , $sidType ) | Where-Object -FilterScript { $_ . IdentityReference . Value -eq 'S-1-1-0' } )
$rule | Should -HaveCount 1
$rule [ 0 ] . AccessControlType | Should -Be ( [System.Security.AccessControl.AccessControlType] :: Deny )
}
}
# A typed parameter ends a named $null (for example a variable that misspells a property, such as -AccessType
# $rule.AccessTpye) with a terminating ParameterBindingFailed; the object-typed parameter accepts it, so the
# cmdlet must not keep the default silently. A named $null is reported the same way as a bad named value.
fix(access): stop for a named $null of the five parameters
A named $null of AccessType, AuditFlags, InheritanceFlags,
PropagationFlags, or AppliesTo (a literal $null, a variable that holds
$null such as a misspelled property, the alias -AccessControlType,
splatting, or a null entry of $PSDefaultParameterValues) silently kept
the parameter's default instead of stopping: Add-NTFSAccess -AccessType
$null created an Allow entry, Add-NTFSAudit -AuditFlags $null audited
Success and Failure, and Remove-NTFSAccess -AccessType $null removed
nothing. Before the five parameters became object (Decision 27, fix
B), the same named $null ended with a terminating ParameterBindingFailed.
Each cmdlet's BeginProcessing now checks, for a value that is null,
whether MyInvocation.BoundParameters has the parameter's name; a piped
property that is null is not bound yet at that point and still keeps
the default, but a value that is already bound and null is reported
the same way as a bad named value: a terminating InvalidParameterValue
error naming the parameter and its valid values. AppliesTo is Mandatory
and a named $null of it was already a terminating
ParameterArgumentValidationErrorNullNotAllowed from the binder itself,
unchanged by this fix.
ParameterConversion gains the message for this case next to the
existing blank-value message, so that the wording is built in one
place.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
1 week ago
Context 'A named $null is a terminating error' {
It 'Should throw a terminating error for a named $null of <Cmdlet> -<Parameter>, and change nothing' -ForEach $accessNamedNullCases {
$folder = New-TestSandboxItem -Sandbox $sandbox -Name 'NamedNull' -Directory
$before = ( Get-Acl -LiteralPath $folder ) . Sddl
$namedArguments = @ { Path = $folder ; Account = 'S-1-1-0' ; AccessRights = 'ReadData' }
$namedArguments [ $Parameter ] = $null
$caughtId = $null
$caughtMessage = $null
try {
& $Cmdlet @namedArguments -ErrorAction Stop
}
catch {
$caughtId = $_ . FullyQualifiedErrorId
$caughtMessage = $_ . Exception . Message
}
if ( $Parameter -eq 'AppliesTo' ) {
# AppliesTo is Mandatory: PowerShell's own binder already rejects a named $null before the cmdlet
# runs, with its own error id, unrelated to this fix; characterized here so that the row is not
# silently missing from the table.
$caughtId | Should -BeLike 'ParameterArgumentValidationErrorNullNotAllowed,*'
}
else {
$caughtId | Should -BeLike 'InvalidParameterValue,*'
$caughtMessage | Should -Match ( [regex] :: Escape ( $Parameter ) )
$caughtMessage | Should -Match 'is null'
}
( Get-Acl -LiteralPath $folder ) . Sddl | Should -BeExactly $before
}
It 'Should throw a terminating InvalidParameterValue error for a named $null of <Cmdlet> -AccessControlType (the alias), and change nothing' -ForEach @ (
@ { Cmdlet = 'Add-NTFSAccess' }
@ { Cmdlet = 'Remove-NTFSAccess' }
) {
$folder = New-TestSandboxItem -Sandbox $sandbox -Name 'NamedNullAlias' -Directory
$before = ( Get-Acl -LiteralPath $folder ) . Sddl
$caughtId = $null
try {
& $Cmdlet -Path $folder -Account 'S-1-1-0' -AccessRights ReadData -AccessControlType $null -ErrorAction Stop
}
catch {
$caughtId = $_ . FullyQualifiedErrorId
}
$caughtId | Should -BeLike 'InvalidParameterValue,*'
( Get-Acl -LiteralPath $folder ) . Sddl | Should -BeExactly $before
}
It 'Should throw a terminating InvalidParameterValue error for a $null of -AccessType from $PSDefaultParameterValues, and change nothing' {
$folder = New-TestSandboxItem -Sandbox $sandbox -Name 'NamedNullDefault' -Directory
$before = ( Get-Acl -LiteralPath $folder ) . Sddl
$PSDefaultParameterValues [ 'Add-NTFSAccess:AccessType' ] = $null
$caughtId = $null
try {
Add-NTFSAccess -Path $folder -Account 'S-1-1-0' -AccessRights ReadData -ErrorAction Stop
}
catch {
$caughtId = $_ . FullyQualifiedErrorId
}
finally {
$PSDefaultParameterValues . Remove ( 'Add-NTFSAccess:AccessType' )
}
$caughtId | Should -BeLike 'InvalidParameterValue,*'
( Get-Acl -LiteralPath $folder ) . Sddl | Should -BeExactly $before
}
It 'Should still give <Cmdlet> -<Parameter> its own default for a piped $null, unaffected by the named $null fix' -ForEach $accessNamedNullCases {
$folder = New-TestSandboxItem -Sandbox $sandbox -Name 'PipedNullUnaffected' -Directory
$extra = @ { }
$extra [ $Parameter ] = $null
[pscustomobject] ( @ { Path = $folder ; Account = 'S-1-1-0' ; AccessRights = 'ReadData' } + $extra ) |
& $Cmdlet -ErrorVariable pipedErrors -ErrorAction SilentlyContinue
if ( $Parameter -eq 'AppliesTo' ) {
# AppliesTo is Mandatory: a piped $null is the pre-existing binder validation error, unrelated to this fix.
$pipedErrors | Should -HaveCount 1
$pipedErrors [ 0 ] . FullyQualifiedErrorId | Should -BeLike 'ParameterArgumentValidationErrorNullNotAllowed,*'
}
else {
$pipedErrors | Should -BeNullOrEmpty
}
}
}
fix(access): report a piped AccessType, AuditFlags, flags, or scope that isn't valid
Add-NTFSAccess, Remove-NTFSAccess, Add-NTFSAudit, and Remove-NTFSAudit
silently kept the default value of AccessType, AuditFlags,
InheritanceFlags, PropagationFlags, and AppliesTo when a piped object's
property could not be converted to the parameter's type: a misspelled
AccessType such as "Denyy", or a blank CSV cell, created an Allow entry
with no message. For a permissions tool, a typo for Deny that silently
grants access is a security-relevant defect (Decision 27).
The five parameters are now declared as object and converted inside
the cmdlet, the way the PowerShell binder converts a typed parameter
(names in any case, comma-separated flag lists, numbers, enum
instances). A value that cannot be converted, including a blank one,
is now an error named InvalidParameterValue: terminating and
catchable when it is named on the command line, exactly as the binder
reported it before; a non-terminating error targeting that path or
descriptor when it comes from a piped object, which leaves that item
unchanged and lets the pipeline continue with the next one. A $null
value is still not an error and keeps the parameter's default. Tab
completion of the five parameters keeps working through explicit
argument completers. Choosing between the Simple and Complex
parameter sets is unaffected and still rejects AppliesTo together
with InheritanceFlags or PropagationFlags as AmbiguousParameterSet,
for every mix of string and enum values.
The four cmdlet pages document the parameters as Type: Object, list
the valid values, and describe the new error; a sentence that said a
piped AppliesTo typo or blank cell was silently ignored is corrected.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
1 week ago
Context 'Choosing the parameter set with a mix of string and enum values' {
It 'Should reject a piped object whose AppliesTo is <AppliesToShape> combined with InheritanceFlags as <FlagShape> with an AmbiguousParameterSet error' -ForEach $accessAmbiguousValueCases {
$folder = New-TestSandboxItem -Sandbox $sandbox -Name 'AmbiguousPipedMix' -Directory
$before = ( Get-Acl -LiteralPath $folder ) . Sddl
$appliesToValue = if ( $AppliesToIsEnum ) { [Security2.ApplyTo] :: ThisFolderOnly } else { 'ThisFolderOnly' }
$flagValue = if ( $FlagIsEnum ) { [System.Security.AccessControl.InheritanceFlags] :: None } else { 'None' }
$object = [pscustomobject] @ { Path = $folder ; Account = 'S-1-1-0' ; AccessRights = 'ReadData' ; AppliesTo = $appliesToValue ; InheritanceFlags = $flagValue }
{ $object | Add-NTFSAccess -ErrorAction Stop } | Should -Throw -ErrorId 'AmbiguousParameterSet*'
( Get-Acl -LiteralPath $folder ) . Sddl | Should -BeExactly $before
}
It 'Should reject a named call whose -AppliesTo is <AppliesToShape> combined with -PropagationFlags as <FlagShape> with an AmbiguousParameterSet error' -ForEach $accessAmbiguousValueCases {
$folder = New-TestSandboxItem -Sandbox $sandbox -Name 'AmbiguousNamedMix' -Directory
$before = ( Get-Acl -LiteralPath $folder ) . Sddl
$appliesToValue = if ( $AppliesToIsEnum ) { [Security2.ApplyTo] :: ThisFolderOnly } else { 'ThisFolderOnly' }
$flagValue = if ( $FlagIsEnum ) { [System.Security.AccessControl.InheritanceFlags] :: None } else { 'None' }
{ Add-NTFSAccess -Path $folder -Account 'S-1-1-0' -AccessRights ReadData -AppliesTo $appliesToValue -PropagationFlags $flagValue -ErrorAction Stop } |
Should -Throw -ErrorId 'AmbiguousParameterSet*'
( Get-Acl -LiteralPath $folder ) . Sddl | Should -BeExactly $before
docs(access): say what a blank, null, or script block value of a property does, and pin it
R1-1 (major): the four pages claimed that a blank cell of AppliesTo, AccessType or
AuditFlags, InheritanceFlags, or PropagationFlags is an InvalidParameterValue error
for every shape. That is wrong when the blank, $null, or DBNull value belongs to
the group that -AppliesTo did not select: PowerShell counts a property that is
present, even blank, as offered for choosing the parameter set, the same as a
real value, so AppliesTo together with a blank InheritanceFlags or
PropagationFlags (for example a short CSV row, whose missing trailing columns
Import-Csv fills with $null) is AmbiguousParameterSet, and rc8 created the
entry. The pages now say that a CSV file should have the columns of only one
group, that mixing both groups fails with AmbiguousParameterSet, that a literal
$null named or piped for -AppliesTo itself fails with its own, unrelated,
pre-existing error because -AppliesTo is mandatory, and keep the sentence that a
bad value within the chosen group is InvalidParameterValue.
R1-3 (minor): the pages now also say that a script block is a plain value for
these parameters and is not evaluated once for each piped object the way rc8
evaluated it, so a named script block is a terminating InvalidParameterValue
error.
Neither finding needed a code change; both already read this way. Pinned with
new table-driven tests, over the four cmdlets and their Path and SD parameter
sets, for AppliesTo valid combined with a $null, an empty string, or DBNull
InheritanceFlags or PropagationFlags and the reverse, and for a named script
block of each of the five parameters: the error id the binder already gives,
nothing changed, and, where applicable, the next piped object still processed.
Confirmed unchanged against the pre-R1-2/R1-4 build.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
1 week ago
}
}
# A typed parameter skips a $null, an empty string, or DBNull value of a property, and the entry was created for
# every shape below; the object-typed parameters count such a value of a property of the other group as present
# too, so PowerShell cannot resolve the parameter set and nothing is changed.
Context 'A blank value of a property of the other group' {
docs(access): say what a blank, null, or script block value of a property does, and pin it
R1-1 (major): the four pages claimed that a blank cell of AppliesTo, AccessType or
AuditFlags, InheritanceFlags, or PropagationFlags is an InvalidParameterValue error
for every shape. That is wrong when the blank, $null, or DBNull value belongs to
the group that -AppliesTo did not select: PowerShell counts a property that is
present, even blank, as offered for choosing the parameter set, the same as a
real value, so AppliesTo together with a blank InheritanceFlags or
PropagationFlags (for example a short CSV row, whose missing trailing columns
Import-Csv fills with $null) is AmbiguousParameterSet, and rc8 created the
entry. The pages now say that a CSV file should have the columns of only one
group, that mixing both groups fails with AmbiguousParameterSet, that a literal
$null named or piped for -AppliesTo itself fails with its own, unrelated,
pre-existing error because -AppliesTo is mandatory, and keep the sentence that a
bad value within the chosen group is InvalidParameterValue.
R1-3 (minor): the pages now also say that a script block is a plain value for
these parameters and is not evaluated once for each piped object the way rc8
evaluated it, so a named script block is a terminating InvalidParameterValue
error.
Neither finding needed a code change; both already read this way. Pinned with
new table-driven tests, over the four cmdlets and their Path and SD parameter
sets, for AppliesTo valid combined with a $null, an empty string, or DBNull
InheritanceFlags or PropagationFlags and the reverse, and for a named script
block of each of the five parameters: the error id the binder already gives,
nothing changed, and, where applicable, the next piped object still processed.
Confirmed unchanged against the pre-R1-2/R1-4 build.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
1 week ago
It 'Should reject <Cmdlet> (<TargetKind>) for <Direction> (<Shape>) with the error the binder gives, change nothing, and still process the next object' -ForEach $accessAmbiguousOtherGroupCases {
$bad = New-TestSandboxItem -Sandbox $sandbox -Name 'AmbiguousOtherGroupBad' -Directory
$next = New-TestSandboxItem -Sandbox $sandbox -Name 'AmbiguousOtherGroupNext' -Directory
$before = ( Get-Acl -LiteralPath $bad ) . Sddl
$badTarget = if ( $TargetKind -eq 'Path' ) { @ { Path = $bad } } else { @ { SecurityDescriptor = ( Get-NTFSSecurityDescriptor -Path $bad -ErrorAction Stop ) } }
$nextTarget = if ( $TargetKind -eq 'Path' ) { @ { Path = $next } } else { @ { SecurityDescriptor = ( Get-NTFSSecurityDescriptor -Path $next -ErrorAction Stop ) } }
$badExtra = @ { Account = 'S-1-1-0' ; AccessRights = 'ReadData' }
if ( $AppliesToIsBlank ) {
$badExtra [ 'AppliesTo' ] = $BlankValue
$badExtra [ 'PropagationFlags' ] = 'None'
}
else {
$badExtra [ 'AppliesTo' ] = 'ThisFolderOnly'
$badExtra [ 'InheritanceFlags' ] = $BlankValue
}
$rows = @ (
[pscustomobject] ( $badTarget + $badExtra )
[pscustomobject] ( $nextTarget + @ { Account = 'S-1-1-0' ; AccessRights = 'ReadData' } )
)
$result = @ ( $rows | & $Cmdlet -PassThru -ErrorVariable pipelineErrors -ErrorAction SilentlyContinue )
$pipelineErrors | Should -HaveCount 1
$pipelineErrors [ 0 ] . FullyQualifiedErrorId | Should -BeLike " $ ExpectedErrorId,* "
( Get-Acl -LiteralPath $bad ) . Sddl | Should -BeExactly $before
$result | Should -Not -BeNullOrEmpty
$result | ForEach-Object -Process { $_ . FullName | Should -Be $next }
}
}
# With the object type, a script block is a plain value (a typed parameter evaluates it once per piped object),
# so ValidateNamedValue in BeginProcessing rejects it the same as any other bad value, before any piped object
# is processed.
docs(access): say what a blank, null, or script block value of a property does, and pin it
R1-1 (major): the four pages claimed that a blank cell of AppliesTo, AccessType or
AuditFlags, InheritanceFlags, or PropagationFlags is an InvalidParameterValue error
for every shape. That is wrong when the blank, $null, or DBNull value belongs to
the group that -AppliesTo did not select: PowerShell counts a property that is
present, even blank, as offered for choosing the parameter set, the same as a
real value, so AppliesTo together with a blank InheritanceFlags or
PropagationFlags (for example a short CSV row, whose missing trailing columns
Import-Csv fills with $null) is AmbiguousParameterSet, and rc8 created the
entry. The pages now say that a CSV file should have the columns of only one
group, that mixing both groups fails with AmbiguousParameterSet, that a literal
$null named or piped for -AppliesTo itself fails with its own, unrelated,
pre-existing error because -AppliesTo is mandatory, and keep the sentence that a
bad value within the chosen group is InvalidParameterValue.
R1-3 (minor): the pages now also say that a script block is a plain value for
these parameters and is not evaluated once for each piped object the way rc8
evaluated it, so a named script block is a terminating InvalidParameterValue
error.
Neither finding needed a code change; both already read this way. Pinned with
new table-driven tests, over the four cmdlets and their Path and SD parameter
sets, for AppliesTo valid combined with a $null, an empty string, or DBNull
InheritanceFlags or PropagationFlags and the reverse, and for a named script
block of each of the five parameters: the error id the binder already gives,
nothing changed, and, where applicable, the next piped object still processed.
Confirmed unchanged against the pre-R1-2/R1-4 build.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
1 week ago
Context 'A named script block is a terminating error' {
It 'Should throw a terminating InvalidParameterValue error for a named script block value of <Cmdlet> -<Parameter>, and change nothing' -ForEach $accessScriptBlockCases {
$folder = New-TestSandboxItem -Sandbox $sandbox -Name 'NamedScriptBlock' -Directory
$before = ( Get-Acl -LiteralPath $folder ) . Sddl
$namedArguments = @ { Path = $folder ; Account = 'S-1-1-0' ; AccessRights = 'ReadData' }
$namedArguments [ $Parameter ] = { 'Deny' }
$caughtId = $null
try {
& $Cmdlet @namedArguments -ErrorAction Stop
}
catch {
$caughtId = $_ . FullyQualifiedErrorId
}
$caughtId | Should -BeLike 'InvalidParameterValue,*'
( Get-Acl -LiteralPath $folder ) . Sddl | Should -BeExactly $before
fix(access): report a piped AccessType, AuditFlags, flags, or scope that isn't valid
Add-NTFSAccess, Remove-NTFSAccess, Add-NTFSAudit, and Remove-NTFSAudit
silently kept the default value of AccessType, AuditFlags,
InheritanceFlags, PropagationFlags, and AppliesTo when a piped object's
property could not be converted to the parameter's type: a misspelled
AccessType such as "Denyy", or a blank CSV cell, created an Allow entry
with no message. For a permissions tool, a typo for Deny that silently
grants access is a security-relevant defect (Decision 27).
The five parameters are now declared as object and converted inside
the cmdlet, the way the PowerShell binder converts a typed parameter
(names in any case, comma-separated flag lists, numbers, enum
instances). A value that cannot be converted, including a blank one,
is now an error named InvalidParameterValue: terminating and
catchable when it is named on the command line, exactly as the binder
reported it before; a non-terminating error targeting that path or
descriptor when it comes from a piped object, which leaves that item
unchanged and lets the pipeline continue with the next one. A $null
value is still not an error and keeps the parameter's default. Tab
completion of the five parameters keeps working through explicit
argument completers. Choosing between the Simple and Complex
parameter sets is unaffected and still rejects AppliesTo together
with InheritanceFlags or PropagationFlags as AmbiguousParameterSet,
for every mix of string and enum values.
The four cmdlet pages document the parameters as Type: Object, list
the valid values, and describe the new error; a sentence that said a
piped AppliesTo typo or blank cell was silently ignored is corrected.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
1 week ago
}
}
Context 'Tab completion and the parameter type' {
It '<Cmdlet> -<Parameter> should complete exactly the names of its enumeration' -ForEach $accessCompleterCases {
$inputScript = '{0} -{1} ' -f $Cmdlet , $Parameter
$enumType = [type] $EnumTypeName
$completion = [System.Management.Automation.CommandCompletion] :: CompleteInput ( $inputScript , $inputScript . Length , $null )
@ ( $completion . CompletionMatches | ForEach-Object -Process { $_ . CompletionText } ) | Should -BeExactly ( [Enum] :: GetNames ( $enumType ) )
}
It '<Cmdlet> should declare -<Parameter> as [object]' -ForEach $accessCompleterCases {
( Get-Command -Name $Cmdlet ) . Parameters [ $Parameter ] . ParameterType | Should -Be ( [object] )
}
fix(access): complete quoted and wildcard words of the five parameters
The completers for AccessType, AuditFlags, InheritanceFlags,
PropagationFlags, and AppliesTo returned nothing for a quoted word
(-AccessType "D) or a wildcard word (-AppliesTo *Only, -AccessType
D*), which rc8's built-in enum-parameter completion still offered.
PowerShell auto-closes an unterminated quote before it reaches the
completer, so the word a completer receives for -AccessType "D is
"D", not D.
EnumArgumentCompletion.Complete now strips a leading quote and its
matching trailing quote from the word before matching, matches the
enumeration's names with WildcardPattern(word + "*",
WildcardOptions.IgnoreCase) the way PowerShell's own enum completion
does, and, when the word was quoted, returns each match in the same
quote character (CompletionText 'FilesOnly' for a leading single
quote). An unquoted, non-wildcard word is unaffected: appending "*"
to it only turns an exact prefix match into a prefix-wildcard match.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
1 week ago
It 'Should complete Add-NTFSAccess -<Parameter> for the unquoted, single-quoted, double-quoted, and wildcard forms of <ExpectedName>' -ForEach $accessCompletionFormCases {
$base = 'Add-NTFSAccess -Path C:\x -Account a -AccessRights Read -{0} ' -f $Parameter
$unquoted = $base + $Word
$r1 = [System.Management.Automation.CommandCompletion] :: CompleteInput ( $unquoted , $unquoted . Length , $null )
@ ( $r1 . CompletionMatches . CompletionText ) | Should -Contain $ExpectedName
$singleQuoted = $base + " ' " + $Word
$r2 = [System.Management.Automation.CommandCompletion] :: CompleteInput ( $singleQuoted , $singleQuoted . Length , $null )
@ ( $r2 . CompletionMatches . CompletionText ) | Should -Contain ( " ' " + $ExpectedName + " ' " )
$doubleQuoted = $base + '"' + $Word
$r3 = [System.Management.Automation.CommandCompletion] :: CompleteInput ( $doubleQuoted , $doubleQuoted . Length , $null )
@ ( $r3 . CompletionMatches . CompletionText ) | Should -Contain ( '"' + $ExpectedName + '"' )
$wildcard = $base + '*' + $ExpectedName . Substring ( $ExpectedName . Length - 3 )
$r4 = [System.Management.Automation.CommandCompletion] :: CompleteInput ( $wildcard , $wildcard . Length , $null )
@ ( $r4 . CompletionMatches . CompletionText ) | Should -Contain $ExpectedName
}
fix(access): reject a list for -AccessType and -AppliesTo, and complete in capitals in Turkish
-AccessType and -AppliesTo of Add-NTFSAccess, Remove-NTFSAccess,
Add-NTFSAudit (-AppliesTo), and Remove-NTFSAudit (-AppliesTo) take one value,
but PowerShell adds up the numbers of a comma list or an array for an
enumeration, so that -AppliesTo 'ThisFolderOnly,FilesOnly' created a
FilesOnly entry and -AccessType 'Allow,Deny' created a Deny entry, without a
message. The conversion in ParameterConversion now rejects a string with a
comma and a collection of more than one element for an enumeration that is
not a flags enumeration, with the same InvalidParameterValue error as for a
misspelled name (terminating for a named value, one error per object for a
piped one). A single name and a collection of one element convert as before,
and the flags parameters still take a list. The valid-value matrices of both
published rc8 and this build give the same entries in both editions, named,
piped by property name, and splatted, apart from the nine list rows.
The completers of the five parameters compared with WildcardOptions.IgnoreCase
only, so that a word typed in capitals did not complete while the culture of
the thread was Turkish (an I and an i are different letters there, and
THISFOLDERO did not match ThisFolderOnly). They compare culture invariant now,
like the filter of Get-ChildItem2.
The tests pin both, red before the change and green after it in four
configurations, and the headers of the two test files no longer say that the
-SecurityDescriptor sets have no handler per descriptor.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
1 week ago
It 'Should complete Add-NTFSAccess -<Parameter> for the word <Word> while the culture of the thread is Turkish' -ForEach $accessTurkishCompletionCases {
$inputScript = 'Add-NTFSAccess -Path C:\x -Account a -AccessRights Read -{0} {1}' -f $Parameter , $Word
$original = [System.Threading.Thread] :: CurrentThread . CurrentCulture
try {
[System.Threading.Thread] :: CurrentThread . CurrentCulture = [System.Globalization.CultureInfo] :: GetCultureInfo ( 'tr-TR' )
'I' . ToLower ( ) | Should -Be ( [string][char] 0x131 ) -Because 'the culture of the thread has to be Turkish for the test to mean anything'
$completion = [System.Management.Automation.CommandCompletion] :: CompleteInput ( $inputScript , $inputScript . Length , $null )
$names = @ ( $completion . CompletionMatches | ForEach-Object -Process { $_ . CompletionText } )
}
finally {
[System.Threading.Thread] :: CurrentThread . CurrentCulture = $original
}
$names | Should -Contain $ExpectedName
}
fix(access): report a piped AccessType, AuditFlags, flags, or scope that isn't valid
Add-NTFSAccess, Remove-NTFSAccess, Add-NTFSAudit, and Remove-NTFSAudit
silently kept the default value of AccessType, AuditFlags,
InheritanceFlags, PropagationFlags, and AppliesTo when a piped object's
property could not be converted to the parameter's type: a misspelled
AccessType such as "Denyy", or a blank CSV cell, created an Allow entry
with no message. For a permissions tool, a typo for Deny that silently
grants access is a security-relevant defect (Decision 27).
The five parameters are now declared as object and converted inside
the cmdlet, the way the PowerShell binder converts a typed parameter
(names in any case, comma-separated flag lists, numbers, enum
instances). A value that cannot be converted, including a blank one,
is now an error named InvalidParameterValue: terminating and
catchable when it is named on the command line, exactly as the binder
reported it before; a non-terminating error targeting that path or
descriptor when it comes from a piped object, which leaves that item
unchanged and lets the pipeline continue with the next one. A $null
value is still not an error and keeps the parameter's default. Tab
completion of the five parameters keeps working through explicit
argument completers. Choosing between the Simple and Complex
parameter sets is unaffected and still rejects AppliesTo together
with InheritanceFlags or PropagationFlags as AmbiguousParameterSet,
for every mix of string and enum values.
The four cmdlet pages document the parameters as Type: Object, list
the valid values, and describe the new error; a sentence that said a
piped AppliesTo typo or blank cell was silently ignored is corrected.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
1 week ago
}
}
test(access): assert the parameter sets that the judged matrix found untested
Adds Tests/ParameterSets.Access.Tests.ps1, closing the TestNeeded rows of
paramsets/judged-access.csv for Add-NTFSAccess, Remove-NTFSAccess,
Clear-NTFSAccess, Get-NTFSAccess, Get-NTFSOrphanedAccess,
Get-NTFSSimpleAccess, and Get-NTFSEffectiveAccess: 20 new It blocks (26
executed cases) covering pipeline binding (ValueFromPipeline and
ValueFromPipelineByPropertyName, including positional file/folder objects),
-PassThru combined with one denied and one succeeding path, error
ErrorId/category/target for read and write failures, continuation after a
per-item error, state read back with .NET (not -PassThru/Get-NTFSAccess),
SecurityDescriptor-set pipelines and memory-vs-disk behavior, and the
declared output types and PathSimple/SDSimple vs PathComplex/SDComplex
parameter-set boundaries. No row with GapKind=MaintainerDecision is pinned:
the -SecurityDescriptor sets of Add/Remove keep their open per-item error
behavior untested either way.
Finding: a plain piped object with an AppliesTo property does not steer
Add-NTFSAccess/Remove-NTFSAccess to the PathSimple/SDSimple parameter set by
itself; PowerShell resolves the ambiguity to the default set (PathComplex/
SDComplex) and silently drops the unmatched property. The proposed test for
that exact shape (notes section (b), item 2c) could not be written as
specified; the pipeline test for PathSimple instead binds -Path through the
pipeline while -AppliesTo is named explicitly, which does select the set.
Validated in all four configurations (elevated/basic user, Windows
PowerShell 5.1/PowerShell 7): 26 passed, 0 failed, 0 skipped in each. Eight
one-statement production mutations across all seven cmdlets were applied in
an isolated worktree, built, and run against this file; each was detected by
at least one new test, then the source was restored exactly and rebuilt.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
3 weeks ago
Describe 'Declared output types and parameter set boundaries' {
It '<Name> should declare <Types> as its output type' -ForEach $declaredOutputTypes {
@ ( ( Get-Command -Name $Name ) . OutputType . Name ) | Should -Be $Types
}
It '<_> should require -AppliesTo only in the Simple parameter sets, -InheritanceFlags/-PropagationFlags only in the Complex parameter sets, and reject combining -AppliesTo with -InheritanceFlags without changing the item' -ForEach @ (
'Add-NTFSAccess' , 'Remove-NTFSAccess'
) {
$parameterSets = ( Get-Command -Name $_ ) . ParameterSets
foreach ( $setName in 'PathSimple' , 'SDSimple' ) {
$parameterSet = $parameterSets | Where-Object -Property Name -EQ -Value $setName
( $parameterSet . Parameters | Where-Object -Property Name -EQ -Value 'AppliesTo' ) . IsMandatory | Should -BeTrue
$parameterSet . Parameters . Name | Should -Not -Contain 'InheritanceFlags'
$parameterSet . Parameters . Name | Should -Not -Contain 'PropagationFlags'
}
foreach ( $setName in 'PathComplex' , 'SDComplex' ) {
$parameterSet = $parameterSets | Where-Object -Property Name -EQ -Value $setName
$parameterSet . Parameters . Name | Should -Not -Contain 'AppliesTo'
$parameterSet . Parameters . Name | Should -Contain 'InheritanceFlags'
$parameterSet . Parameters . Name | Should -Contain 'PropagationFlags'
}
$file = New-TestSandboxItem -Sandbox $sandbox -Name ( 'Boundary{0}' -f $_ . Replace ( '-' , '' ) )
$before = ( Get-Acl -LiteralPath $file ) . Sddl
{ & $_ -Path $file -Account 'S-1-1-0' -AccessRights ReadData -AppliesTo ThisFolderOnly -InheritanceFlags None -ErrorAction Stop } |
Should -Throw -ErrorId 'AmbiguousParameterSet*'
( Get-Acl -LiteralPath $file ) . Sddl | Should -BeExactly $before
}
}