Browse Source

test: cover permission scopes and folder inheritance transitions

Exercise all 13 scopes using named scopes and explicit Windows flags,
through disk paths and in-memory descriptors, for access and audit
entries. Verify removal keeps another account, and check actual
inheritance through children and grandchildren, including OneLevel.
Cover keep/remove switches and successful PassThru for both files and
folders, including Set-NTFSInheritance enabling protection states.

Controlled mutations lose OneLevel and keep folders protected: 12 tests
fail as expected. Restored implementations pass the 170-test focused
suite in all four configurations where privileges permit. No cmdlet
contract changes.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
pull/117/head
Raimund Andree 3 days ago
parent
commit
e7ee203ea6
  1. 126
      Tests/Inheritance.Tests.ps1
  2. 165
      Tests/PermissionScopes.Tests.ps1

126
Tests/Inheritance.Tests.ps1

@ -12,6 +12,13 @@ BeforeDiscovery {
$canChangeAudit = Test-PrivilegeHeld -Name 'SeSecurityPrivilege' $canChangeAudit = Test-PrivilegeHeld -Name 'SeSecurityPrivilege'
# Assigning an owner other than the user or one of its groups needs the Restore privilege. # Assigning an owner other than the user or one of its groups needs the Restore privilege.
$canAssignAnyOwner = Test-PrivilegeHeld -Name 'SeRestorePrivilege' $canAssignAnyOwner = Test-PrivilegeHeld -Name 'SeRestorePrivilege'
$inheritanceCases = @(foreach ($type in 'file', 'folder') {
foreach ($enable in $false, $true) {
foreach ($remove in $false, $true) {
@{ Type = $type; Enable = $enable; Remove = $remove }
}
}
})
} }
BeforeAll { BeforeAll {
@ -276,6 +283,125 @@ Describe 'Audit inheritance switches' {
} }
} }
Describe 'Access inheritance transitions on files and folders' {
It 'Should set enabled=<Enable> on a <Type>, remove requested entries=<Remove>, and report the written state' -ForEach $inheritanceCases {
$parent = New-TestSandboxItem -Sandbox $sandbox -Name 'AccessParent' -Directory
$path = Join-Path -Path $parent -ChildPath 'Child'
$parentAccount = 'S-1-5-21-1-2-3-4901'
$childAccount = 'S-1-5-21-1-2-3-4902'
Add-NTFSAccess -Path $parent -Account $parentAccount -AccessRights ReadData -ErrorAction Stop
Assert-TestSandboxPath -Sandbox $sandbox -Path $path
if ($Type -eq 'folder') { New-Item -ItemType Directory -Path $path | Out-Null } else { Set-Content -LiteralPath $path -Value 'Child' }
Add-NTFSAccess -Path $path -Account $childAccount -AccessRights Delete -AppliesTo ThisFolderOnly -ErrorAction Stop
@(Get-NTFSAccess -Path $path -Account $parentAccount -ExcludeExplicit -ErrorAction Stop) | Should -HaveCount 1
$owner = (Get-NTFSOwner -Path $path -ErrorAction Stop).Owner.Sid
if ($Enable) {
Disable-NTFSAccessInheritance -Path $path -RemoveInheritedAccessRules -ErrorAction Stop
$parameters = @{ RemoveExplicitAccessRules = $Remove }
$command = 'Enable-NTFSAccessInheritance'
}
else {
$parameters = @{ RemoveInheritedAccessRules = $Remove }
$command = 'Disable-NTFSAccessInheritance'
}
$result = @(& $command -Path $path @parameters -PassThru -ErrorAction Stop)
$result | Should -HaveCount 1
$result[0] | Should -BeOfType [Security2.FileSystemInheritanceInfo]
$result[0].FullName | Should -Be $path
$result[0].AccessInheritanceEnabled | Should -Be $Enable
(Get-NTFSInheritance -Path $path).AccessInheritanceEnabled | Should -Be $Enable
(Get-NTFSOwner -Path $path).Owner.Sid | Should -Be $owner
$parentRules = @(Get-NTFSAccess -Path $path -Account $parentAccount)
if ($Enable) {
$parentRules | Should -HaveCount 1
$parentRules[0].IsInherited | Should -BeTrue
}
elseif ($Remove) {
$parentRules | Should -BeNullOrEmpty
}
else {
$parentRules | Should -HaveCount 1
$parentRules[0].IsInherited | Should -BeFalse
}
$childRules = @(Get-NTFSAccess -Path $path -Account $childAccount)
if ($Enable -and $Remove) { $childRules | Should -BeNullOrEmpty } else { $childRules | Should -HaveCount 1 }
}
It 'Set-NTFSInheritance should re-enable access inheritance on a <_> and keep its explicit entry' -ForEach @('file', 'folder') {
$path = New-TestSandboxItem -Sandbox $sandbox -Name 'AccessEnable' -Directory:($_ -eq 'folder')
Add-NTFSAccess -Path $path -Account 'S-1-5-21-1-2-3-4902' -AccessRights Delete -AppliesTo ThisFolderOnly -ErrorAction Stop
Disable-NTFSAccessInheritance -Path $path -RemoveInheritedAccessRules -ErrorAction Stop
$result = @(Set-NTFSInheritance -Path $path -AccessInheritanceEnabled $true -PassThru -ErrorAction Stop)
$result | Should -HaveCount 1
$result[0].AccessInheritanceEnabled | Should -BeTrue
@(Get-NTFSAccess -Path $path -ExcludeExplicit) | Should -Not -BeNullOrEmpty
@(Get-NTFSAccess -Path $path -Account 'S-1-5-21-1-2-3-4902' -ExcludeInherited) | Should -HaveCount 1
}
}
Describe 'Audit inheritance transitions on files and folders' -Skip:(-not $canChangeAudit) {
It 'Should set enabled=<Enable> on a <Type>, remove requested audit entries=<Remove>, and leave the DACL unchanged' -ForEach $inheritanceCases {
$parent = New-TestSandboxItem -Sandbox $sandbox -Name 'AuditParent' -Directory
$path = Join-Path -Path $parent -ChildPath 'Child'
$parentAccount = 'S-1-5-21-1-2-3-4911'
$childAccount = 'S-1-5-21-1-2-3-4912'
Add-NTFSAudit -Path $parent -Account $parentAccount -AccessRights ReadData -AuditFlags Success -ErrorAction Stop
Assert-TestSandboxPath -Sandbox $sandbox -Path $path
if ($Type -eq 'folder') { New-Item -ItemType Directory -Path $path | Out-Null } else { Set-Content -LiteralPath $path -Value 'Child' }
Add-NTFSAudit -Path $path -Account $childAccount -AccessRights Delete -AuditFlags Failure -AppliesTo ThisFolderOnly -ErrorAction Stop
@(Get-NTFSAudit -Path $path -Account $parentAccount -ExcludeExplicit -ErrorAction Stop) | Should -HaveCount 1
$before = (Get-Acl -LiteralPath $path).GetSecurityDescriptorSddlForm('Access')
if ($Enable) {
Disable-NTFSAuditInheritance -Path $path -RemoveInheritedAuditRules -ErrorAction Stop
$parameters = @{ RemoveExplicitAuditRules = $Remove }
$command = 'Enable-NTFSAuditInheritance'
}
else {
$parameters = @{ RemoveInheritedAuditRules = $Remove }
$command = 'Disable-NTFSAuditInheritance'
}
$result = @(& $command -Path $path @parameters -PassThru -ErrorAction Stop)
$result | Should -HaveCount 1
$result[0].FullName | Should -Be $path
$result[0].AuditInheritanceEnabled | Should -Be $Enable
(Get-NTFSInheritance -Path $path).AuditInheritanceEnabled | Should -Be $Enable
(Get-Acl -LiteralPath $path).GetSecurityDescriptorSddlForm('Access') | Should -BeExactly $before
$parentRules = @(Get-NTFSAudit -Path $path -Account $parentAccount)
if ($Enable) {
$parentRules | Should -HaveCount 1
$parentRules[0].IsInherited | Should -BeTrue
}
elseif ($Remove) {
$parentRules | Should -BeNullOrEmpty
}
else {
$parentRules | Should -HaveCount 1
$parentRules[0].IsInherited | Should -BeFalse
}
$childRules = @(Get-NTFSAudit -Path $path -Account $childAccount)
if ($Enable -and $Remove) { $childRules | Should -BeNullOrEmpty } else { $childRules | Should -HaveCount 1 }
}
It 'Set-NTFSInheritance should re-enable audit inheritance on a <_> and keep its explicit audit entry' -ForEach @('file', 'folder') {
$path = New-TestSandboxItem -Sandbox $sandbox -Name 'AuditEnable' -Directory:($_ -eq 'folder')
Add-NTFSAudit -Path $path -Account 'S-1-5-21-1-2-3-4912' -AccessRights Delete -AuditFlags Failure -AppliesTo ThisFolderOnly -ErrorAction Stop
Disable-NTFSAuditInheritance -Path $path -RemoveInheritedAuditRules -ErrorAction Stop
$before = (Get-Acl -LiteralPath $path).GetSecurityDescriptorSddlForm('Access')
$result = @(Set-NTFSInheritance -Path $path -AuditInheritanceEnabled $true -PassThru -ErrorAction Stop)
$result | Should -HaveCount 1
$result[0].AuditInheritanceEnabled | Should -BeTrue
@(Get-NTFSAudit -Path $path -Account 'S-1-5-21-1-2-3-4912' -ExcludeInherited) | Should -HaveCount 1
(Get-Acl -LiteralPath $path).GetSecurityDescriptorSddlForm('Access') | Should -BeExactly $before
}
}
Describe 'Access inheritance cmdlets' { Describe 'Access inheritance cmdlets' {
Context 'When the item has an owner that the user cannot assign' { Context 'When the item has an owner that the user cannot assign' {
BeforeAll { BeforeAll {

165
Tests/PermissionScopes.Tests.ps1

@ -0,0 +1,165 @@
<#
Tests all permission scopes through the access and audit cmdlets, both parameter forms and both storage modes.
Expected flags are the Windows ACE flags, independent of the module's scope converter.
#>
[Diagnostics.CodeAnalysis.SuppressMessageAttribute(
'PSUseDeclaredVarsMoreThanAssignments', '', Justification = 'Pester shares variables between blocks.'
)]
param ()
BeforeDiscovery {
Import-Module -Name (Join-Path -Path $PSScriptRoot -ChildPath 'TestHelpers.psm1') -Force
$canReadAudit = Test-PrivilegeHeld -Name 'SeSecurityPrivilege'
$scopes = @(
@{ 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' }
)
$activeTargets = @{
ThisFolderOnly = @('Root')
ThisFolderSubfoldersAndFiles = @('Root', 'File', 'Child', 'ChildFile', 'Grandchild', 'GrandchildFile')
ThisFolderAndSubfolders = @('Root', 'Child', 'Grandchild')
ThisFolderAndFiles = @('Root', 'File', 'ChildFile', 'GrandchildFile')
SubfoldersAndFilesOnly = @('File', 'Child', 'ChildFile', 'Grandchild', 'GrandchildFile')
SubfoldersOnly = @('Child', 'Grandchild')
FilesOnly = @('File', 'ChildFile', 'GrandchildFile')
ThisFolderSubfoldersAndFilesOneLevel = @('Root', 'File', 'Child')
ThisFolderAndSubfoldersOneLevel = @('Root', 'Child')
ThisFolderAndFilesOneLevel = @('Root', 'File')
SubfoldersAndFilesOnlyOneLevel = @('File', 'Child')
SubfoldersOnlyOneLevel = @('Child')
FilesOnlyOneLevel = @('File')
}
$propagationCases = @($scopes | ForEach-Object { @{ Name = $_.Name; ActiveTargets = $activeTargets[$_.Name] } })
$scopeNames = @($scopes.Name)
$scopeCases = @(foreach ($scope in $scopes) {
foreach ($source in 'Path', 'SecurityDescriptor') {
foreach ($form in 'AppliesTo', 'Flags') {
@{ Name = $scope.Name; Inheritance = $scope.Inheritance; Propagation = $scope.Propagation; Source = $source; Form = $form }
}
}
})
}
BeforeAll {
Import-Module -Name (Join-Path -Path $PSScriptRoot -ChildPath 'TestHelpers.psm1') -Force
Import-Module -Name (Join-Path -Path $PSScriptRoot -ChildPath '..\NTFSSecurity\bin\Release\NTFSSecurity.psd1') -Force -ErrorAction Stop
$sandbox = New-TestSandbox -Name 'PermissionScopes'
Push-Location -LiteralPath $sandbox
$account = 'S-1-5-21-1-2-3-4801'
$keeper = 'S-1-5-21-1-2-3-4802'
}
AfterAll {
Pop-Location
Remove-TestSandbox -Sandbox $sandbox
Remove-Module -Name NTFSSecurity -Force -ErrorAction SilentlyContinue
}
Describe 'Permission scope inventory' {
It 'Should cover every named -AppliesTo value' -ForEach @(@{ ScopeNames = $scopeNames }) {
(@([Enum]::GetNames([Security2.ApplyTo])) | Sort-Object) -join ',' |
Should -Be (($scopeNames | Sort-Object) -join ',')
}
}
Describe 'Access rule scopes' {
It 'Should add and remove <Name> using <Form> on <Source>, preserving the other account' -ForEach $scopeCases {
$folder = New-TestSandboxItem -Sandbox $sandbox -Name 'AccessScope' -Directory
$before = (Get-Acl -LiteralPath $folder).GetSecurityDescriptorSddlForm('Access')
$location = if ($Source -eq 'Path') { @{ Path = $folder } } else { @{ SecurityDescriptor = Get-NTFSSecurityDescriptor -Path $folder -ErrorAction Stop } }
$flags = @{ InheritanceFlags = $Inheritance; PropagationFlags = $Propagation }
$addScope = if ($Form -eq 'AppliesTo') { @{ AppliesTo = $Name } } else { $flags }
$removeScope = if ($Form -eq 'AppliesTo') { $flags } else { @{ AppliesTo = $Name } }
Add-NTFSAccess @location -Account $keeper -AccessRights Delete -AppliesTo ThisFolderOnly -ErrorAction Stop
$added = @(Add-NTFSAccess @location @addScope -Account $account -AccessRights ReadData -PassThru -ErrorAction Stop |
Where-Object -FilterScript { $_.Account.Sid -eq $account })
$added | Should -HaveCount 1
$added[0] | Should -BeOfType [Security2.FileSystemAccessRule2]
$added[0].InheritanceFlags | Should -Be ([Security.AccessControl.InheritanceFlags] $Inheritance)
$added[0].PropagationFlags | Should -Be ([Security.AccessControl.PropagationFlags] $Propagation)
$added[0].AccessRights.HasFlag([Security2.FileSystemRights2]::ReadData) | Should -BeTrue
[Security2.FileSystemSecurity2]::ConvertToApplyTo($added[0].InheritanceFlags, $added[0].PropagationFlags).ToString() | Should -Be $Name
if ($Source -eq 'SecurityDescriptor') {
(Get-Acl -LiteralPath $folder).GetSecurityDescriptorSddlForm('Access') | Should -BeExactly $before
}
$remaining = @(Remove-NTFSAccess @location @removeScope -Account $account -AccessRights ReadData -RemoveSpecific -PassThru -ErrorAction Stop)
@($remaining | Where-Object -FilterScript { $_.Account.Sid -eq $account }) | Should -BeNullOrEmpty
@($remaining | Where-Object -FilterScript { $_.Account.Sid -eq $keeper }) | Should -HaveCount 1
@(Get-NTFSAccess @location -Account $account -ErrorAction Stop) | Should -BeNullOrEmpty
}
}
Describe 'Access scopes on descendants' {
It 'Should apply <Name> only to its intended descendants, including the OneLevel boundary' -ForEach $propagationCases {
$folder = New-TestSandboxItem -Sandbox $sandbox -Name 'Propagation' -Directory
Add-NTFSAccess -Path $folder -Account $account -AccessRights ReadData -AppliesTo $Name -ErrorAction Stop
$paths = [ordered]@{
Root = $folder
File = Join-Path -Path $folder -ChildPath 'File.txt'
Child = Join-Path -Path $folder -ChildPath 'Child'
ChildFile = Join-Path -Path $folder -ChildPath 'Child\File.txt'
Grandchild = Join-Path -Path $folder -ChildPath 'Child\Grandchild'
GrandchildFile = Join-Path -Path $folder -ChildPath 'Child\Grandchild\File.txt'
}
Assert-TestSandboxPath -Sandbox $sandbox -Path @($paths.Values)
New-Item -ItemType Directory -Path $paths.Grandchild -Force | Out-Null
foreach ($key in 'File', 'ChildFile', 'GrandchildFile') {
Set-Content -LiteralPath $paths[$key] -Value $key
}
$actual = @(foreach ($key in $paths.Keys) {
$active = @(Get-NTFSAccess -Path $paths[$key] -Account $account -ErrorAction Stop | Where-Object -FilterScript {
-not $_.PropagationFlags.HasFlag([Security.AccessControl.PropagationFlags]::InheritOnly) -and
$_.AccessRights.HasFlag([Security2.FileSystemRights2]::ReadData)
})
if ($active.Count -gt 0) { $key }
})
($actual | Sort-Object) -join ',' | Should -Be (($ActiveTargets | Sort-Object) -join ',')
}
}
Describe 'Audit rule scopes' -Skip:(-not $canReadAudit) {
It 'Should add and remove <Name> using <Form> on <Source>, preserving the other account' -ForEach $scopeCases {
$folder = New-TestSandboxItem -Sandbox $sandbox -Name 'AuditScope' -Directory
$before = (Get-Acl -LiteralPath $folder -Audit).GetSecurityDescriptorSddlForm('Audit')
$location = if ($Source -eq 'Path') { @{ Path = $folder } } else { @{ SecurityDescriptor = Get-NTFSSecurityDescriptor -Path $folder -ErrorAction Stop } }
$flags = @{ InheritanceFlags = $Inheritance; PropagationFlags = $Propagation }
$addScope = if ($Form -eq 'AppliesTo') { @{ AppliesTo = $Name } } else { $flags }
$removeScope = if ($Form -eq 'AppliesTo') { $flags } else { @{ AppliesTo = $Name } }
Add-NTFSAudit @location -Account $keeper -AccessRights Delete -AuditFlags Failure -AppliesTo ThisFolderOnly -ErrorAction Stop
$added = @(Add-NTFSAudit @location @addScope -Account $account -AccessRights ReadData -AuditFlags 'Success, Failure' -PassThru -ErrorAction Stop |
Where-Object -FilterScript { $_.Account.Sid -eq $account })
$added | Should -HaveCount 1
$added[0] | Should -BeOfType [Security2.FileSystemAuditRule2]
$added[0].InheritanceFlags | Should -Be ([Security.AccessControl.InheritanceFlags] $Inheritance)
$added[0].PropagationFlags | Should -Be ([Security.AccessControl.PropagationFlags] $Propagation)
$added[0].AuditFlags | Should -Be ([Security.AccessControl.AuditFlags] 'Success, Failure')
[Security2.FileSystemSecurity2]::ConvertToApplyTo($added[0].InheritanceFlags, $added[0].PropagationFlags).ToString() | Should -Be $Name
if ($Source -eq 'SecurityDescriptor') {
(Get-Acl -LiteralPath $folder -Audit).GetSecurityDescriptorSddlForm('Audit') | Should -BeExactly $before
}
$remaining = @(Remove-NTFSAudit @location @removeScope -Account $account -AccessRights ReadData -AuditFlags 'Success, Failure' -RemoveSpecific -PassThru -ErrorAction Stop)
@($remaining | Where-Object -FilterScript { $_.Account.Sid -eq $account }) | Should -BeNullOrEmpty
@($remaining | Where-Object -FilterScript { $_.Account.Sid -eq $keeper }) | Should -HaveCount 1
@(Get-NTFSAudit @location -Account $account -ErrorAction Stop) | Should -BeNullOrEmpty
}
}
Loading…
Cancel
Save