mirror of https://github.com/raandree/NTFSSecurity
Browse Source
Add tests for the error handling that the cmdlets with -Path share: a path that doesn't exist (16 cmdlets, and 6 audit cmdlets with the Security privilege), an item whose owner may not read its permissions (6 cmdlets), and an item whose owner may not change them, which the 6 cmdlets that write the DACL handle by taking ownership. Each error belongs to its path only, and the cmdlet goes on with the next one. The tests found one defect: Get-NTFSOrphanedAccess reported an item that it couldn't read as an AddAceError with the category WriteError. It now writes a ReadSecurityError, like Get-NTFSAccess. They also show that the take-ownership retry works without privileges when the account holds the Take Ownership right and may assign the previous owner, and that it can't help a denied read, because reading the owner needs the same right. Concepts and five cmdlet pages said that the retry needs the privileges; they now describe both, and that Windows removes the OWNER RIGHTS entries when the owner changes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: AI Assistant <ai@example.com>ai/release-5.0.0-rc6
10 changed files with 254 additions and 12 deletions
@ -0,0 +1,232 @@ |
|||
<# |
|||
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' |
|||
$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-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 '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 |
|||
} |
|||
|
|||
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 |
|||
} |
|||
} |
|||
Loading…
Reference in new issue