<#
Tests the error handling that the cmdlets with -Path share , with the module built in NTFSSecurity \ bin \ Release on
files in a sandbox folder : a path that doesn ' t exist , an item whose owner may not read its permissions , and an item
whose owner may not change its permissions , which the cmdlets that write the DACL handle by taking ownership . Each
error belongs to its path only , and the cmdlet continues with the next one . The tests turn the module setting
EnablePrivileges off , so that an elevated session meets the same denials as a basic user , and restore it .
#>
[ Diagnostics . CodeAnalysis . SuppressMessageAttribute (
'PSUseDeclaredVarsMoreThanAssignments' , '' , Justification = 'Pester shares variables between blocks.'
) ]
param ( )
BeforeDiscovery {
Import-Module -Name ( Join-Path -Path $PSScriptRoot -ChildPath 'TestHelpers.psm1' ) -Force
$holdsSecurityPrivilege = Test-PrivilegeHeld -Name 'SeSecurityPrivilege'
$holdsRestorePrivilege = Test-PrivilegeHeld -Name 'SeRestorePrivilege'
$currentUser = [System.Security.Principal.WindowsIdentity] :: GetCurrent ( ) . User . Value
$readEntry = @ { Account = 'S-1-1-0' ; AccessRights = 'ReadData' }
# An audit entry on a file has no inheritance flags.
$auditEntry = @ { Account = 'S-1-1-0' ; AccessRights = 'ReadData' ; InheritanceFlags = 'None' ; PropagationFlags = 'None' }
# Output: whether the cmdlet writes an object for the next path, an existing file.
$missingPathCases = @ (
@ { Command = 'Get-NTFSAccess' ; Parameters = @ { } ; ErrorId = 'ReadFileError' ; Output = $true }
@ { Command = 'Add-NTFSAccess' ; Parameters = $readEntry ; ErrorId = 'ReadFileError' ; Output = $false }
@ { Command = 'Remove-NTFSAccess' ; Parameters = $readEntry ; ErrorId = 'ReadFileError' ; Output = $false }
@ { Command = 'Clear-NTFSAccess' ; Parameters = @ { } ; ErrorId = 'ReadFileError' ; Output = $false }
@ { Command = 'Get-NTFSEffectiveAccess' ; Parameters = @ { Account = 'S-1-1-0' } ; ErrorId = 'ReadFileError' ; Output = $true }
@ { Command = 'Get-NTFSOrphanedAccess' ; Parameters = @ { } ; ErrorId = 'ReadFileError' ; Output = $false }
@ { Command = 'Get-NTFSInheritance' ; Parameters = @ { } ; ErrorId = 'ReadFileError' ; Output = $true }
@ { Command = 'Enable-NTFSAccessInheritance' ; Parameters = @ { } ; ErrorId = 'ReadFileError' ; Output = $false }
@ { Command = 'Disable-NTFSAccessInheritance' ; Parameters = @ { } ; ErrorId = 'ReadFileError' ; Output = $false }
@ { Command = 'Set-NTFSInheritance' ; Parameters = @ { AccessInheritanceEnabled = $true } ; ErrorId = 'ReadFileError' ; Output = $false }
@ { Command = 'Get-NTFSOwner' ; Parameters = @ { } ; ErrorId = 'ReadFileError' ; Output = $true }
@ { Command = 'Set-NTFSOwner' ; Parameters = @ { Account = $currentUser } ; ErrorId = 'ReadFileError' ; Output = $false }
@ { Command = 'Get-NTFSSecurityDescriptor' ; Parameters = @ { } ; ErrorId = 'ReadFileError' ; Output = $true }
@ { Command = 'Get-Item2' ; Parameters = @ { } ; ErrorId = 'FileNotFound' ; Output = $true }
@ { Command = 'Get-ChildItem2' ; Parameters = @ { } ; ErrorId = 'FileNotFound' ; Output = $true }
@ { Command = 'Get-FileHash2' ; Parameters = @ { } ; ErrorId = 'ReadFileError' ; Output = $true }
@ { Command = 'Get-NTFSHardLink' ; Parameters = @ { } ; ErrorId = 'FileNotFound' ; Output = $true }
)
# The audit cmdlets need the Security privilege for the next path.
$missingPathAuditCases = @ (
@ { Command = 'Get-NTFSAudit' ; Parameters = @ { } }
@ { Command = 'Add-NTFSAudit' ; Parameters = $auditEntry }
@ { Command = 'Remove-NTFSAudit' ; Parameters = $auditEntry }
@ { Command = 'Clear-NTFSAudit' ; Parameters = @ { } }
@ { Command = 'Enable-NTFSAuditInheritance' ; Parameters = @ { } }
@ { Command = 'Disable-NTFSAuditInheritance' ; Parameters = @ { } }
)
$deniedReadCases = @ (
@ { Command = 'Get-NTFSAccess' ; Parameters = @ { } ; Output = $true }
@ { Command = 'Get-NTFSEffectiveAccess' ; Parameters = @ { Account = 'S-1-1-0' } ; Output = $true }
@ { Command = 'Get-NTFSOrphanedAccess' ; Parameters = @ { } ; Output = $false }
@ { Command = 'Get-NTFSInheritance' ; Parameters = @ { } ; Output = $true }
@ { Command = 'Get-NTFSOwner' ; Parameters = @ { } ; Output = $true }
@ { Command = 'Get-NTFSSecurityDescriptor' ; Parameters = @ { } ; Output = $true }
)
}
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 'PathErrors'
Push-Location -LiteralPath $sandbox
$privateData = ( Get-Module -Name NTFSSecurity ) . PrivateData
$enablePrivileges = $privateData [ 'EnablePrivileges' ]
$privateData [ 'EnablePrivileges' ] = $false
$sidType = [System.Security.Principal.SecurityIdentifier]
function Get-TestMissingPath {
$path = Join-Path -Path $sandbox -ChildPath ( 'Missing-{0}' -f [guid] :: NewGuid ( ) . ToString ( 'N' ) . Substring ( 0 , 8 ) )
Assert-TestSandboxPath -Sandbox $sandbox -Path $path
$path
}
function Get-TestAcl {
# .NET, because Get-Acl of an elevated Windows PowerShell reads also items that deny reading their permissions.
# .NET Core has the method as an extension method.
param ( [string] $Path )
$info = New-Object -TypeName 'System.IO.FileInfo' -ArgumentList $Path
if ( $PSVersionTable . PSEdition -eq 'Desktop' ) {
$info . GetAccessControl ( )
}
else {
[System.IO.FileSystemAclExtensions] :: GetAccessControl ( $info )
}
}
}
AfterAll {
$privateData [ 'EnablePrivileges' ] = $enablePrivileges
Pop-Location
Remove-TestSandbox -Sandbox $sandbox
Remove-Module -Name NTFSSecurity -Force -ErrorAction SilentlyContinue
}
Describe 'A path that does not exist' {
It '<Command> should write a <ErrorId> for it and continue with the next path' -ForEach $missingPathCases {
$missing = Get-TestMissingPath
$file = New-TestSandboxItem -Sandbox $sandbox -Name 'Next'
$output = @ ( & $Command -Path $missing , $file @Parameters -ErrorVariable pathErrors -ErrorAction SilentlyContinue -WarningAction SilentlyContinue )
$pathErrors | Should -HaveCount 1
$pathErrors [ 0 ] . FullyQualifiedErrorId | Should -BeLike " $ ErrorId,* "
$pathErrors [ 0 ] . TargetObject | Should -Be $missing
if ( $Output ) {
$output | Should -Not -BeNullOrEmpty
}
}
It '<Command> should write a ReadFileError for it and continue with the next path' -ForEach $missingPathAuditCases -Skip : ( -not $holdsSecurityPrivilege ) {
$privateData [ 'EnablePrivileges' ] = $true
try {
$missing = Get-TestMissingPath
$file = New-TestSandboxItem -Sandbox $sandbox -Name 'NextAudit'
& $Command -Path $missing , $file @Parameters -ErrorVariable pathErrors -ErrorAction SilentlyContinue | Out-Null
}
finally {
$privateData [ 'EnablePrivileges' ] = $false
}
$pathErrors | Should -HaveCount 1
$pathErrors [ 0 ] . FullyQualifiedErrorId | Should -BeLike 'ReadFileError,*'
$pathErrors [ 0 ] . TargetObject | Should -Be $missing
}
}
Describe 'An item whose owner may not read its permissions' {
# A deny entry for OWNER RIGHTS replaces the right of the owner to read the security descriptor. Taking ownership
# can't help: the cmdlet must read the owner first, which needs the same right. Before 5.0.0-rc6,
# Get-NTFSOrphanedAccess reported this as an AddAceError.
It '<Command> should write a ReadSecurityError for it and continue with the next path' -ForEach $deniedReadCases {
$blocked = New-TestSandboxItem -Sandbox $sandbox -Name 'Unreadable'
Block-TestReadPermission -Sandbox $sandbox -Path $blocked
$file = New-TestSandboxItem -Sandbox $sandbox -Name 'NextReadable'
$output = @ ( & $Command -Path $blocked , $file @Parameters -ErrorVariable pathErrors -ErrorAction SilentlyContinue -WarningAction SilentlyContinue )
$pathErrors | Should -HaveCount 1
$pathErrors [ 0 ] . FullyQualifiedErrorId | Should -BeLike 'ReadSecurityError,*'
$pathErrors [ 0 ] . TargetObject | Should -Be $blocked
if ( $Output ) {
$output | Should -Not -BeNullOrEmpty
}
}
}
Describe 'A denied write and a denied ownership retry' {
It '<Command> should keep the denied item unchanged, report <ErrorId>, and process the next item' -ForEach @ (
@ { Command = 'Add-NTFSAccess' ; Parameters = @ { Account = 'S-1-1-0' ; AccessRights = 'ReadData' } ; ErrorId = 'AddAceError' ; Operation = 'Add' }
@ { Command = 'Remove-NTFSAccess' ; Parameters = @ { Account = 'S-1-1-0' ; AccessRights = 'ReadData' } ; ErrorId = 'RemoveAceError' ; Operation = 'Remove' }
@ { Command = 'Clear-NTFSAccess' ; Parameters = @ { } ; ErrorId = 'ClearAclError' ; Operation = 'Clear' }
@ { Command = 'Disable-NTFSAccessInheritance' ; Parameters = @ { } ; ErrorId = 'ModifySdError' ; Operation = 'Disable' }
@ { Command = 'Enable-NTFSAccessInheritance' ; Parameters = @ { } ; ErrorId = 'ModifySdError' ; Operation = 'Enable' }
@ { Command = 'Set-NTFSInheritance' ; Parameters = @ { AccessInheritanceEnabled = $false } ; ErrorId = 'ModifySdError' ; Operation = 'Disable' }
) {
$blocked = New-TestSandboxItem -Sandbox $sandbox -Name 'RetryBlocked'
$next = New-TestSandboxItem -Sandbox $sandbox -Name 'RetryNext'
foreach ( $path in $blocked , $next ) {
if ( $Operation -ne 'Add' ) {
Add-NTFSAccess -Path $path -Account 'S-1-1-0' -AccessRights ReadData -ErrorAction Stop
}
if ( $Operation -eq 'Enable' ) {
Disable-NTFSAccessInheritance -Path $path -ErrorAction Stop
}
}
Block-TestWritePermission -Sandbox $sandbox -Path $blocked
$before = ( Get-TestAcl -Path $blocked ) . Sddl
& $Command -Path $blocked , $next @Parameters -ErrorVariable changeErrors -ErrorAction SilentlyContinue
$changeErrors | Should -HaveCount 1
$changeErrors [ 0 ] . FullyQualifiedErrorId | Should -BeLike " $ ErrorId,* "
$changeErrors [ 0 ] . TargetObject | Should -Be $blocked
( Get-TestAcl -Path $blocked ) . Sddl | Should -BeExactly $before
$acl = Get-TestAcl -Path $next
$everyone = @ ( $acl . GetAccessRules ( $true , $false , $sidType ) | Where-Object -FilterScript { $_ . IdentityReference . Value -eq 'S-1-1-0' } )
switch ( $Operation ) {
'Add' { $everyone | Should -HaveCount 1 }
'Remove' { $everyone | Should -BeNullOrEmpty }
'Clear' { @ ( $acl . GetAccessRules ( $true , $false , $sidType ) ) | Should -BeNullOrEmpty }
'Disable' { $acl . AreAccessRulesProtected | Should -BeTrue }
'Enable' { $acl . AreAccessRulesProtected | Should -BeFalse }
}
}
}
Describe 'An owner that the process cannot restore without the Restore privilege' {
It 'Should report RestoreOwnerError after a successful ownership retry and continue with the next path' -Skip : ( -not $holdsRestorePrivilege ) {
$blocked = New-TestSandboxItem -Sandbox $sandbox -Name 'UnassignableOwner'
$next = New-TestSandboxItem -Sandbox $sandbox -Name 'NextOwner'
$user = [Security.Principal.WindowsIdentity] :: GetCurrent ( ) . User . Value
Add-TestDenyRule -Sandbox $sandbox -Path $blocked -Rights @ { $user = 'ChangePermissions' }
$originalOwner = 'S-1-5-80-956008885-3418522649-1831038044-1853292631-2271478464'
Set-TestOwner -Sandbox $sandbox -Path $blocked -Sid $originalOwner
( Get-Privileges | Where-Object -Property Privilege -EQ -Value 'Restore' ) . PrivilegeState | Should -Be 'Disabled'
Add-NTFSAccess -Path $blocked , $next -Account 'S-1-1-0' -AccessRights ReadData -ErrorVariable changeErrors -ErrorAction SilentlyContinue
$changeErrors | Should -HaveCount 1
$changeErrors [ 0 ] . FullyQualifiedErrorId | Should -BeLike 'RestoreOwnerError,*'
$changeErrors [ 0 ] . CategoryInfo . Category | Should -Be 'WriteError'
$changeErrors [ 0 ] . TargetObject | Should -Be $blocked
( Get-TestAcl -Path $blocked ) . GetOwner ( $sidType ) . Value | Should -Be $user
foreach ( $path in $blocked , $next ) {
@ ( ( Get-TestAcl -Path $path ) . GetAccessRules ( $true , $false , $sidType ) | Where-Object -FilterScript { $_ . IdentityReference . Value -eq 'S-1-1-0' } ) | Should -HaveCount 1
}
}
}
Describe 'An item whose owner may not change its permissions' {
# A deny entry for OWNER RIGHTS replaces the right of the owner to change the DACL. The cmdlets take ownership,
# which Windows answers by removing the OWNER RIGHTS entries, write the DACL, and set the previous owner back.
BeforeEach {
$file = New-TestSandboxItem -Sandbox $sandbox -Name 'Unchangeable'
$acl = Get-TestAcl -Path $file
$owner = $acl . GetOwner ( $sidType ) . Value
}
It 'Add-NTFSAccess should take ownership, add the entry, and set the owner back' {
Add-TestDenyRule -Sandbox $sandbox -Path $file -Rights @ { 'S-1-3-4' = 'ChangePermissions' }
Add-NTFSAccess -Path $file -Account 'S-1-1-0' -AccessRights ReadData -ErrorVariable changeErrors -ErrorAction SilentlyContinue
$changeErrors | Should -BeNullOrEmpty
$acl = Get-TestAcl -Path $file
$acl . GetOwner ( $sidType ) . Value | Should -Be $owner
@ ( $acl . GetAccessRules ( $true , $false , $sidType ) | Where-Object -FilterScript { $_ . IdentityReference . Value -eq 'S-1-1-0' } ) | Should -HaveCount 1
@ ( $acl . GetAccessRules ( $true , $false , $sidType ) | Where-Object -FilterScript { $_ . IdentityReference . Value -eq 'S-1-3-4' } ) | Should -BeNullOrEmpty
}
It 'Remove-NTFSAccess should take ownership, remove the entry, and set the owner back' {
Add-NTFSAccess -Path $file -Account 'S-1-1-0' -AccessRights ReadData
Add-TestDenyRule -Sandbox $sandbox -Path $file -Rights @ { 'S-1-3-4' = 'ChangePermissions' }
Remove-NTFSAccess -Path $file -Account 'S-1-1-0' -AccessRights ReadData -ErrorVariable changeErrors -ErrorAction SilentlyContinue
$changeErrors | Should -BeNullOrEmpty
$acl = Get-TestAcl -Path $file
$acl . GetOwner ( $sidType ) . Value | Should -Be $owner
@ ( $acl . GetAccessRules ( $true , $false , $sidType ) | Where-Object -FilterScript { $_ . IdentityReference . Value -eq 'S-1-1-0' } ) | Should -BeNullOrEmpty
}
It 'Clear-NTFSAccess should take ownership, remove the explicit entries, and set the owner back' {
Add-NTFSAccess -Path $file -Account 'S-1-1-0' -AccessRights ReadData
Add-TestDenyRule -Sandbox $sandbox -Path $file -Rights @ { 'S-1-3-4' = 'ChangePermissions' }
Clear-NTFSAccess -Path $file -ErrorVariable changeErrors -ErrorAction SilentlyContinue
$changeErrors | Should -BeNullOrEmpty
$acl = Get-TestAcl -Path $file
$acl . GetOwner ( $sidType ) . Value | Should -Be $owner
@ ( $acl . GetAccessRules ( $true , $false , $sidType ) ) | Should -BeNullOrEmpty
}
# With the DACL cleared and protected, nobody keeps the right to set an owner, so setting the previous owner back
# fails. The user owned the item already, so there is no owner to set back.
It 'Clear-NTFSAccess -DisableInheritance should take ownership, clear and protect the DACL, and not set an unchanged owner back' {
$user = [Security.Principal.WindowsIdentity] :: GetCurrent ( ) . User . Value
Set-TestOwner -Sandbox $sandbox -Path $file -Sid $user
Add-NTFSAccess -Path $file -Account 'S-1-1-0' -AccessRights ReadData
Add-TestDenyRule -Sandbox $sandbox -Path $file -Rights @ { 'S-1-3-4' = 'ChangePermissions' }
Clear-NTFSAccess -Path $file -DisableInheritance -ErrorVariable changeErrors -ErrorAction SilentlyContinue
$changeErrors | Should -BeNullOrEmpty
$acl = Get-TestAcl -Path $file
$acl . GetOwner ( $sidType ) . Value | Should -Be $user
$acl . AreAccessRulesProtected | Should -BeTrue
@ ( $acl . GetAccessRules ( $true , $true , $sidType ) ) | Should -BeNullOrEmpty
}
# Windows lets the owner of an item set another owner only with the Restore privilege, which the tests turn off, or
# with the right in the DACL, which the cleared DACL no longer holds. The cmdlet reports the owner it cannot set back.
It 'Clear-NTFSAccess -DisableInheritance should report RestoreOwnerError for a previous owner that it cannot set back' -Skip : ( -not $holdsRestorePrivilege ) {
$user = [Security.Principal.WindowsIdentity] :: GetCurrent ( ) . User . Value
test: cover token handles, drive-root writes, name filters, and descriptor APIs
New guards cover the default AllAccess token handle, token handles that lack the right to query or to adjust privileges, and the finalizer of an abandoned PrivilegeEnabler, which closes the handle that it owns; writes to the root of a drive that subst maps to a sandbox folder, which skip for the restricted token of the basic-user runner that cannot define a drive letter; Get-ChildItem2 -Filter against a match by short name; retargeting a descriptor with Item; the path overloads of the inheritance helpers for a missing path and of the audit rule reader for folders; removing a generic right while another account holds an exact entry; and an empty -ServerName.
Follow-ups of the independent review: dispose the enabler in the finally block before its handle, don't pin that the iterator overloads write only when enumerated, arrange the previous owner of the RestoreOwnerError test, and describe what ObjectApis.Tests.ps1 does.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
3 weeks ago
Set-TestOwner -Sandbox $sandbox -Path $file -Sid 'S-1-5-32-544'
( Get-Privileges | Where-Object -Property Privilege -EQ -Value 'Restore' ) . PrivilegeState | Should -Be 'Disabled'
Add-NTFSAccess -Path $file -Account 'S-1-1-0' -AccessRights ReadData
Add-TestDenyRule -Sandbox $sandbox -Path $file -Rights @ { 'S-1-3-4' = 'ChangePermissions' }
Clear-NTFSAccess -Path $file -DisableInheritance -ErrorVariable changeErrors -ErrorAction SilentlyContinue
$changeErrors | Should -HaveCount 1
$changeErrors [ 0 ] . FullyQualifiedErrorId | Should -BeLike 'RestoreOwnerError,*'
$changeErrors [ 0 ] . CategoryInfo . Category | Should -Be 'WriteError'
$changeErrors [ 0 ] . TargetObject | Should -Be $file
$acl = Get-TestAcl -Path $file
$acl . GetOwner ( $sidType ) . Value | Should -Be $user
$acl . AreAccessRulesProtected | Should -BeTrue
@ ( $acl . GetAccessRules ( $true , $true , $sidType ) ) | Should -BeNullOrEmpty
}
It 'Disable-NTFSAccessInheritance should take ownership, protect the DACL, and set the owner back' {
Add-TestDenyRule -Sandbox $sandbox -Path $file -Rights @ { 'S-1-3-4' = 'ChangePermissions' }
Disable-NTFSAccessInheritance -Path $file -ErrorVariable changeErrors -ErrorAction SilentlyContinue
$changeErrors | Should -BeNullOrEmpty
$acl = Get-TestAcl -Path $file
$acl . GetOwner ( $sidType ) . Value | Should -Be $owner
$acl . AreAccessRulesProtected | Should -BeTrue
}
It 'Enable-NTFSAccessInheritance should take ownership, let the DACL inherit, and set the owner back' {
Disable-NTFSAccessInheritance -Path $file
Add-TestDenyRule -Sandbox $sandbox -Path $file -Rights @ { 'S-1-3-4' = 'ChangePermissions' }
Enable-NTFSAccessInheritance -Path $file -ErrorVariable changeErrors -ErrorAction SilentlyContinue
$changeErrors | Should -BeNullOrEmpty
$acl = Get-TestAcl -Path $file
$acl . GetOwner ( $sidType ) . Value | Should -Be $owner
$acl . AreAccessRulesProtected | Should -BeFalse
}
It 'Set-NTFSInheritance should take ownership, protect the DACL, and set the owner back' {
Add-TestDenyRule -Sandbox $sandbox -Path $file -Rights @ { 'S-1-3-4' = 'ChangePermissions' }
Set-NTFSInheritance -Path $file -AccessInheritanceEnabled $false -ErrorVariable changeErrors -ErrorAction SilentlyContinue
$changeErrors | Should -BeNullOrEmpty
$acl = Get-TestAcl -Path $file
$acl . GetOwner ( $sidType ) . Value | Should -Be $owner
$acl . AreAccessRulesProtected | Should -BeTrue
}
}