|
|
|
|
<#
|
|
|
|
|
Tests the access cmdlets of the module built in NTFSSecurity\bin\Release on files in a sandbox folder.
|
|
|
|
|
#>
|
|
|
|
|
[Diagnostics.CodeAnalysis.SuppressMessageAttribute(
|
|
|
|
|
'PSUseDeclaredVarsMoreThanAssignments', '', Justification = 'Pester shares variables between blocks.'
|
|
|
|
|
)]
|
|
|
|
|
param ()
|
|
|
|
|
|
|
|
|
|
BeforeDiscovery {
|
|
|
|
|
Import-Module -Name (Join-Path -Path $PSScriptRoot -ChildPath 'TestHelpers.psm1') -Force
|
|
|
|
|
# With the Restore privilege, Windows may grant writing the DACL despite a deny entry.
|
|
|
|
|
$canBypassWriteDeny = Test-PrivilegeHeld -Name 'SeRestorePrivilege'
|
fix: write only the sections of the security descriptor that a cmdlet changes
The access and audit cmdlets wrote the owner of an item back with the
entries they changed. For a DACL without the auto-inherit flag, Windows
returns the owner and the group even when only the DACL is read, and the
cmdlets wrote every section that the descriptor held. Without the Restore
privilege, or on a file server that refuses the owner, the write failed
with error 1307 (#34).
- Add-NTFSAccess, Clear-NTFSAccess, Add-NTFSAudit, and Clear-NTFSAudit
read only the DACL or the SACL. FileSystemSecurity2.Write(),
Remove-NTFSAccess, and Remove-NTFSAudit write only the sections they
read, which also fixes the access inheritance cmdlets.
- Read together with the SACL, the inherited entries of such a DACL lose
their inherited flag when the parent folder has no SACL, and the
cmdlets stored them as explicit copies. Get-NTFSSecurityDescriptor now
reads the DACL in a separate call.
- Set-NTFSSecurityDescriptor writes only the sections that changed since
they were read; an unchanged descriptor writes nothing (maintainer
decision of 2026-10-06).
- Clear-NTFSAudit writes nothing for an item without a SACL, and reports
an error without the Security privilege (maintainer decision).
The regression tests failed before and pass after the fix in Windows
PowerShell 5.1 and PowerShell 7, elevated and as a basic user.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
4 weeks ago
|
|
|
# Assigning an owner other than the user or one of its groups needs the Restore privilege.
|
|
|
|
|
$canAssignAnyOwner = Test-PrivilegeHeld -Name 'SeRestorePrivilege'
|
|
|
|
|
$holdsSecurityPrivilege = Test-PrivilegeHeld -Name 'SeSecurityPrivilege'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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 'Access'
|
|
|
|
|
Push-Location -LiteralPath $sandbox
|
fix: write only the sections of the security descriptor that a cmdlet changes
The access and audit cmdlets wrote the owner of an item back with the
entries they changed. For a DACL without the auto-inherit flag, Windows
returns the owner and the group even when only the DACL is read, and the
cmdlets wrote every section that the descriptor held. Without the Restore
privilege, or on a file server that refuses the owner, the write failed
with error 1307 (#34).
- Add-NTFSAccess, Clear-NTFSAccess, Add-NTFSAudit, and Clear-NTFSAudit
read only the DACL or the SACL. FileSystemSecurity2.Write(),
Remove-NTFSAccess, and Remove-NTFSAudit write only the sections they
read, which also fixes the access inheritance cmdlets.
- Read together with the SACL, the inherited entries of such a DACL lose
their inherited flag when the parent folder has no SACL, and the
cmdlets stored them as explicit copies. Get-NTFSSecurityDescriptor now
reads the DACL in a separate call.
- Set-NTFSSecurityDescriptor writes only the sections that changed since
they were read; an unchanged descriptor writes nothing (maintainer
decision of 2026-10-06).
- Clear-NTFSAudit writes nothing for an item without a SACL, and reports
an error without the Security privilege (maintainer decision).
The regression tests failed before and pass after the fix in Windows
PowerShell 5.1 and PowerShell 7, elevated and as a basic user.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
4 weeks ago
|
|
|
|
|
|
|
|
$privateData = (Get-Module -Name NTFSSecurity).PrivateData
|
|
|
|
|
$enablePrivileges = $privateData['EnablePrivileges']
|
|
|
|
|
$sidType = [System.Security.Principal.SecurityIdentifier]
|
|
|
|
|
# An owner that the user can assign only with the Restore privilege
|
|
|
|
|
$trustedInstaller = 'S-1-5-80-956008885-3418522649-1831038044-1853292631-2271478464'
|
|
|
|
|
|
|
|
|
|
function Get-RestorePrivilegeState {
|
|
|
|
|
(Get-Privileges | Where-Object -Property Privilege -EQ -Value 'Restore').PrivilegeState
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
AfterAll {
|
fix: write only the sections of the security descriptor that a cmdlet changes
The access and audit cmdlets wrote the owner of an item back with the
entries they changed. For a DACL without the auto-inherit flag, Windows
returns the owner and the group even when only the DACL is read, and the
cmdlets wrote every section that the descriptor held. Without the Restore
privilege, or on a file server that refuses the owner, the write failed
with error 1307 (#34).
- Add-NTFSAccess, Clear-NTFSAccess, Add-NTFSAudit, and Clear-NTFSAudit
read only the DACL or the SACL. FileSystemSecurity2.Write(),
Remove-NTFSAccess, and Remove-NTFSAudit write only the sections they
read, which also fixes the access inheritance cmdlets.
- Read together with the SACL, the inherited entries of such a DACL lose
their inherited flag when the parent folder has no SACL, and the
cmdlets stored them as explicit copies. Get-NTFSSecurityDescriptor now
reads the DACL in a separate call.
- Set-NTFSSecurityDescriptor writes only the sections that changed since
they were read; an unchanged descriptor writes nothing (maintainer
decision of 2026-10-06).
- Clear-NTFSAudit writes nothing for an item without a SACL, and reports
an error without the Security privilege (maintainer decision).
The regression tests failed before and pass after the fix in Windows
PowerShell 5.1 and PowerShell 7, elevated and as a basic user.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
4 weeks ago
|
|
|
$privateData['EnablePrivileges'] = $enablePrivileges
|
|
|
|
|
Pop-Location
|
|
|
|
|
Remove-TestSandbox -Sandbox $sandbox
|
|
|
|
|
Remove-Module -Name NTFSSecurity -Force -ErrorAction SilentlyContinue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Describe 'Get-NTFSAccess' {
|
|
|
|
|
Context 'When a path fails after a readable path' {
|
|
|
|
|
# Before 5.0.0, the cmdlet wrote the entries of the previous item again for the failing path.
|
|
|
|
|
It 'Should return the entries of the first item once' {
|
|
|
|
|
$folder = New-TestSandboxItem -Sandbox $sandbox -Name 'Readable' -Directory
|
|
|
|
|
$denied = New-TestSandboxItem -Sandbox $sandbox -Name 'Denied'
|
|
|
|
|
Block-TestReadPermission -Sandbox $sandbox -Path $denied
|
|
|
|
|
$expected = @(Get-NTFSAccess -Path $folder).Count
|
|
|
|
|
|
|
|
|
|
$entries = @(Get-NTFSAccess -Path $folder, $denied -ErrorAction SilentlyContinue)
|
|
|
|
|
|
|
|
|
|
@($entries | Where-Object -Property FullName -EQ -Value $folder) | Should -HaveCount $expected
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
fix: resolve the security descriptor sets without -AppliesTo
Defect 14. Add-NTFSAccess, Remove-NTFSAccess, Add-NTFSAudit, and
Remove-NTFSAudit have the sets PathSimple, PathComplex, SDSimple, and
SDComplex; the default PathComplex needs -Path. A command with
-SecurityDescriptor and without -AppliesTo, -InheritanceFlags, or
-PropagationFlags matched both SD sets and failed with "Parameter set
cannot be resolved". -AppliesTo is now mandatory in the Simple sets, so
such a command resolves to the Complex set and its default flags
(ContainerInherit, ObjectInherit / None, which is what AppliesTo
ThisFolderSubfoldersAndFiles means), as a command with -Path already did.
With -AppliesTo nothing changes.
The pages say "Required: True" for -AppliesTo and drop its never
reachable defaults; platyPS takes that metadata from the shipped help
file, so the help file was regenerated before the build.
Tests/Access.Tests.ps1: 6 tests on in-memory security descriptors.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
4 weeks ago
|
|
|
|
|
|
|
|
Describe 'Get-NTFSEffectiveAccess' {
|
|
|
|
|
BeforeAll {
|
|
|
|
|
$effectiveFile = New-TestSandboxItem -Sandbox $sandbox -Name 'Effective'
|
|
|
|
|
Assert-TestSandboxPath -Sandbox $sandbox -Path $effectiveFile
|
|
|
|
|
$acl = Get-Acl -LiteralPath $effectiveFile
|
|
|
|
|
$guests = New-Object -TypeName 'System.Security.Principal.SecurityIdentifier' -ArgumentList 'S-1-5-32-546'
|
|
|
|
|
$acl.AddAccessRule((New-Object -TypeName 'System.Security.AccessControl.FileSystemAccessRule' -ArgumentList (
|
|
|
|
|
$guests, [System.Security.AccessControl.FileSystemRights]::FullControl,
|
|
|
|
|
[System.Security.AccessControl.AccessControlType]::Deny
|
|
|
|
|
)))
|
|
|
|
|
Set-Acl -LiteralPath $effectiveFile -AclObject $acl
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Before 5.0.0, the warning misspelled the privilege as "Privliege".
|
|
|
|
|
It 'Should warn once that the Security privilege is missing' -Skip:$holdsSecurityPrivilege {
|
|
|
|
|
Get-NTFSEffectiveAccess -Path $effectiveFile -WarningVariable accessWarnings -WarningAction SilentlyContinue | Out-Null
|
|
|
|
|
|
|
|
|
|
$accessWarnings | Should -HaveCount 1
|
|
|
|
|
$accessWarnings[0].Message | Should -BeExactly 'The user does not hold the Security privilege and might not be able to read the effective permissions.'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
It 'Should leave out an account without access when -ExcludeNoneAccessEntries is used' {
|
|
|
|
|
$result = @(Get-NTFSEffectiveAccess -Path $effectiveFile -Account 'S-1-5-32-546' -ExcludeNoneAccessEntries -WarningAction SilentlyContinue -ErrorAction Stop)
|
|
|
|
|
|
|
|
|
|
$result | Should -BeNullOrEmpty
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
It 'Should return an account with access when -ExcludeNoneAccessEntries is used' {
|
|
|
|
|
$result = @(Get-NTFSEffectiveAccess -Path $effectiveFile -ExcludeNoneAccessEntries -WarningAction SilentlyContinue -ErrorAction Stop)
|
|
|
|
|
|
|
|
|
|
$result | Should -HaveCount 1
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
It 'Should use the current location when -Path is omitted' {
|
|
|
|
|
$result = @(Get-NTFSEffectiveAccess -WarningAction SilentlyContinue -ErrorAction Stop)
|
|
|
|
|
|
|
|
|
|
$result | Should -HaveCount 1
|
|
|
|
|
$result[0].FullName | Should -BeLike ('*\{0}' -f (Split-Path -Path $sandbox -Leaf))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
It 'Should compute the effective access of a security descriptor' {
|
|
|
|
|
$sd = Get-NTFSSecurityDescriptor -Path $effectiveFile
|
|
|
|
|
|
|
|
|
|
$result = @(Get-NTFSEffectiveAccess -SecurityDescriptor $sd -WarningAction SilentlyContinue -ErrorAction Stop)
|
|
|
|
|
|
|
|
|
|
$result | Should -HaveCount 1
|
|
|
|
|
$result[0].FullName | Should -Be $effectiveFile
|
|
|
|
|
}
|
fix: drive roots, audit descriptors, -WhatIf conflicts, and small items
- #41: for the root of a drive, the cmdlets read and changed the security
descriptor of the drive, a device object. FileSystemSecurity2 now routes
drive roots through the path-based AlphaFS methods, which keep the
trailing backslash; the removal and inheritance helpers use it too.
- #109: Add-, Remove-, and Clear-NTFSAudit report a security descriptor
without the audit entries like Get-NTFSAudit, through one helper, and
Get-NTFSEffectiveAccess names the cause that Windows reported instead
of a missing Security privilege.
- #108: Copy-Item2 and Move-Item2 check the destination only for an
operation that runs; with -WhatIf, a verbose message names the conflict.
- #111: Disable-Privileges skips the privileges that the token doesn't
hold, the privilege messages are spelled right, and Get-FileHash2
declares the type name of its objects; 05-Releasing.md documents the
release metadata tests.
- rc3 review leftovers: Remove-NTFSAudit writes nothing for an item
without a SACL, the owner retry of Set-NTFSSecurityDescriptor restores
the previous owner in a finally block and keeps an owner that the
descriptor sets, and FileSystemSecurity2.Write with another item writes
only the sections that were read.
Each fix has a test that failed first, in Windows PowerShell 5.1 and
PowerShell 7; writing a drive root was checked once on a temporary VHD.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
4 weeks ago
|
|
|
|
|
|
|
|
Context 'When the effective access cannot be calculated' {
|
|
|
|
|
BeforeAll {
|
|
|
|
|
$privateData['EnablePrivileges'] = $false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
AfterAll {
|
|
|
|
|
$privateData['EnablePrivileges'] = $enablePrivileges
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Before 5.0.0-rc4, the cmdlet blamed a missing Security privilege for every failure while the privilege
|
|
|
|
|
# wasn't enabled (#109). A security descriptor without an owner is such a failure: the DACL of the file has the
|
|
|
|
|
# auto-inherit flag since Set-Acl wrote it, so Windows returns no owner when only the DACL is read.
|
|
|
|
|
It 'Should name the cause in the error, not the Security privilege' {
|
|
|
|
|
(Get-Privileges | Where-Object -Property Privilege -EQ -Value 'Security').PrivilegeState | Should -Not -Be 'Enabled'
|
|
|
|
|
$sd = New-Object -TypeName 'Security2.FileSystemSecurity2' -ArgumentList (
|
|
|
|
|
(Get-Item2 -Path $effectiveFile), [System.Security.AccessControl.AccessControlSections]::Access
|
|
|
|
|
)
|
|
|
|
|
$sd.SecurityDescriptor.GetOwner($sidType) | Should -BeNullOrEmpty
|
|
|
|
|
|
|
|
|
|
Get-NTFSEffectiveAccess -SecurityDescriptor $sd -ErrorVariable accessErrors -ErrorAction SilentlyContinue -WarningAction SilentlyContinue | Out-Null
|
|
|
|
|
|
|
|
|
|
$accessErrors | Should -HaveCount 1
|
|
|
|
|
$accessErrors[0].FullyQualifiedErrorId | Should -BeLike 'GetEffectiveAccessError,*'
|
|
|
|
|
$accessErrors[0].Exception.Message | Should -Not -BeLike '*Enable-Privileges*'
|
fix: drive roots, audit descriptors, -WhatIf conflicts, and small items
- #41: for the root of a drive, the cmdlets read and changed the security
descriptor of the drive, a device object. FileSystemSecurity2 now routes
drive roots through the path-based AlphaFS methods, which keep the
trailing backslash; the removal and inheritance helpers use it too.
- #109: Add-, Remove-, and Clear-NTFSAudit report a security descriptor
without the audit entries like Get-NTFSAudit, through one helper, and
Get-NTFSEffectiveAccess names the cause that Windows reported instead
of a missing Security privilege.
- #108: Copy-Item2 and Move-Item2 check the destination only for an
operation that runs; with -WhatIf, a verbose message names the conflict.
- #111: Disable-Privileges skips the privileges that the token doesn't
hold, the privilege messages are spelled right, and Get-FileHash2
declares the type name of its objects; 05-Releasing.md documents the
release metadata tests.
- rc3 review leftovers: Remove-NTFSAudit writes nothing for an item
without a SACL, the owner retry of Set-NTFSSecurityDescriptor restores
the previous owner in a finally block and keeps an owner that the
descriptor sets, and FileSystemSecurity2.Write with another item writes
only the sections that were read.
Each fix has a test that failed first, in Windows PowerShell 5.1 and
PowerShell 7; writing a drive root was checked once on a temporary VHD.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
4 weeks ago
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Context 'When the computer of -ServerName cannot be reached' {
|
|
|
|
|
# Before 5.0.0-rc5, the cmdlet returned no access when the computer couldn't be reached, although it warned that
|
|
|
|
|
# it had calculated the result on this computer. Windows reports a computer that it can't resolve or reach with
|
|
|
|
|
# the error RPC server unavailable; the name ends in .invalid, which no DNS server resolves (RFC 2606).
|
|
|
|
|
It 'Should return the result of this computer and warn' {
|
|
|
|
|
$expected = Get-NTFSEffectiveAccess -Path $effectiveFile -WarningAction SilentlyContinue -ErrorAction Stop
|
|
|
|
|
[long] $expected.AccessRights | Should -BeGreaterThan ([long] [Security2.FileSystemRights2]::Synchronize)
|
|
|
|
|
|
|
|
|
|
$result = @(Get-NTFSEffectiveAccess -Path $effectiveFile -ServerName 'ntfssecurity-test.invalid' -WarningVariable accessWarnings -WarningAction SilentlyContinue -ErrorVariable accessErrors -ErrorAction SilentlyContinue)
|
|
|
|
|
|
|
|
|
|
$accessErrors | Should -BeNullOrEmpty
|
|
|
|
|
$result | Should -HaveCount 1
|
|
|
|
|
$result[0].AccessRights | Should -Be $expected.AccessRights
|
|
|
|
|
$accessWarnings.Message | Should -Contain ('The effective rights can only be computed based on group membership on this computer. ' +
|
|
|
|
|
'For more accurate results, calculate effective access rights on the target computer')
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Describe 'Get-NTFSOrphanedAccess' {
|
|
|
|
|
# Before 5.0.0, a path with braces stopped the cmdlet with a FormatException (#3).
|
|
|
|
|
It 'Should read a folder whose name contains braces' {
|
|
|
|
|
$braces = Join-Path -Path $sandbox -ChildPath ('{{Braces}}-{0}' -f [guid]::NewGuid().ToString('N').Substring(0, 8))
|
|
|
|
|
Assert-TestSandboxPath -Sandbox $sandbox -Path $braces
|
|
|
|
|
[IO.Directory]::CreateDirectory($braces) | Out-Null
|
|
|
|
|
|
|
|
|
|
{ Get-NTFSOrphanedAccess -Path $braces -ErrorAction Stop } | Should -Not -Throw
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
BeforeAll {
|
|
|
|
|
$orphanedFile = New-TestSandboxItem -Sandbox $sandbox -Name 'Orphaned'
|
|
|
|
|
Assert-TestSandboxPath -Sandbox $sandbox -Path $orphanedFile
|
|
|
|
|
$acl = Get-Acl -LiteralPath $orphanedFile
|
|
|
|
|
foreach ($sid in 'S-1-5-21-1-2-3-1001', 'S-1-5-21-1-2-3-1002') {
|
|
|
|
|
$acl.AddAccessRule((New-Object -TypeName 'System.Security.AccessControl.FileSystemAccessRule' -ArgumentList (
|
|
|
|
|
(New-Object -TypeName 'System.Security.Principal.SecurityIdentifier' -ArgumentList $sid),
|
|
|
|
|
[System.Security.AccessControl.FileSystemRights]::ReadData, [System.Security.AccessControl.AccessControlType]::Allow
|
|
|
|
|
)))
|
|
|
|
|
}
|
|
|
|
|
Set-Acl -LiteralPath $orphanedFile -AclObject $acl
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
It 'Should return the entries whose account cannot be resolved' {
|
|
|
|
|
@(Get-NTFSOrphanedAccess -Path $orphanedFile) | Should -HaveCount 2
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
It 'Should return only the entries of -Account' {
|
|
|
|
|
$result = @(Get-NTFSOrphanedAccess -Path $orphanedFile -Account 'S-1-5-21-1-2-3-1002')
|
|
|
|
|
|
|
|
|
|
$result | Should -HaveCount 1
|
|
|
|
|
$result[0].Account.Sid | Should -Be 'S-1-5-21-1-2-3-1002'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
It 'Should read the entries of a security descriptor' {
|
|
|
|
|
$sd = Get-NTFSSecurityDescriptor -Path $orphanedFile
|
|
|
|
|
|
|
|
|
|
$result = @(Get-NTFSOrphanedAccess -SecurityDescriptor $sd)
|
|
|
|
|
|
|
|
|
|
$result | Should -HaveCount 2
|
|
|
|
|
$result | ForEach-Object -Process { $_.FullName | Should -Be $orphanedFile }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Describe 'Get-NTFSSimpleAccess' {
|
|
|
|
|
BeforeAll {
|
|
|
|
|
$simpleFolder = New-TestSandboxItem -Sandbox $sandbox -Name 'Simple' -Directory
|
|
|
|
|
Assert-TestSandboxPath -Sandbox $sandbox -Path $simpleFolder
|
|
|
|
|
$acl = Get-Acl -LiteralPath $simpleFolder
|
|
|
|
|
$acl.AddAccessRule((New-Object -TypeName 'System.Security.AccessControl.FileSystemAccessRule' -ArgumentList (
|
|
|
|
|
(New-Object -TypeName 'System.Security.Principal.SecurityIdentifier' -ArgumentList 'S-1-1-0'),
|
|
|
|
|
[System.Security.AccessControl.FileSystemRights]::ReadData, [System.Security.AccessControl.AccessControlType]::Allow
|
|
|
|
|
)))
|
|
|
|
|
Set-Acl -LiteralPath $simpleFolder -AclObject $acl
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
It 'Should return only the entries of -Account' {
|
|
|
|
|
$result = @(Get-NTFSSimpleAccess -Path $simpleFolder -Account 'S-1-1-0' -IncludeRootFolder:$false)
|
|
|
|
|
|
|
|
|
|
$result | Should -Not -BeNullOrEmpty
|
|
|
|
|
$result | ForEach-Object -Process { $_.Identity.Sid | Should -Be 'S-1-1-0' }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
It 'Should read the entries of a security descriptor' {
|
|
|
|
|
$sd = Get-NTFSSecurityDescriptor -Path $simpleFolder
|
|
|
|
|
|
|
|
|
|
$result = @(Get-NTFSSimpleAccess -SecurityDescriptor $sd)
|
|
|
|
|
|
|
|
|
|
$result | Should -Not -BeNullOrEmpty
|
|
|
|
|
$result | ForEach-Object -Process { $_.FullName | Should -Be $simpleFolder }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
It 'Should show the entries as a table with the account, the rights, and the type' {
|
|
|
|
|
$text = Get-NTFSSimpleAccess -Path $simpleFolder -IncludeRootFolder:$false | Out-String -Width 200
|
|
|
|
|
|
|
|
|
|
$text | Should -Match 'Account\s+Access Rights\s+Type'
|
|
|
|
|
}
|
fix: report ReadData and the parent of a relative path in Get-NTFSSimpleAccess
Two deviations from the cmdlet page, found by new tests from its
contract:
- An entry that grants only ReadData became None: the reduction tested
the combined Read mask, and nothing mapped ReadData alone. .NET adds
Synchronize to every allow entry, which hid it; other tools write such
entries.
- With -IncludeRootFolder, on by default, a relative path with a single
folder name had no parent folder in the result, because the parent was
taken from the path before it was resolved.
The tests also cover the rights reduction, the comparison of folders
with their parent, the pipeline, the current location, -ExcludeExplicit,
files, and missing paths, elevated and as a basic user in both editions.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
3 weeks ago
|
|
|
|
|
|
|
|
Context 'When it reduces the rights of an entry' {
|
|
|
|
|
BeforeAll {
|
|
|
|
|
# One explicit entry per case, for a SID of its own and with exactly these rights. .NET would add
|
|
|
|
|
# Synchronize to an allow entry, so the DACL is set in SDDL.
|
|
|
|
|
$rightsFolder = New-TestSandboxItem -Sandbox $sandbox -Name 'SimpleRights' -Directory
|
|
|
|
|
$rightsCases = @(
|
|
|
|
|
@{ Name = 'ReadData'; Mask = 0x1; Expected = 'Read' }
|
|
|
|
|
@{ Name = 'ReadAttributes'; Mask = 0x80; Expected = 'Read' }
|
|
|
|
|
@{ Name = 'Traverse'; Mask = 0x20; Expected = 'Read' }
|
|
|
|
|
@{ Name = 'ReadPermissions'; Mask = 0x20000; Expected = 'Read' }
|
|
|
|
|
@{ Name = 'CreateFiles'; Mask = 0x2; Expected = 'Write' }
|
|
|
|
|
@{ Name = 'AppendData'; Mask = 0x4; Expected = 'Write' }
|
|
|
|
|
@{ Name = 'WriteAttributes'; Mask = 0x100; Expected = 'Write' }
|
|
|
|
|
@{ Name = 'ChangePermissions'; Mask = 0x40000; Expected = 'Write' }
|
|
|
|
|
@{ Name = 'TakeOwnership'; Mask = 0x80000; Expected = 'Write' }
|
|
|
|
|
@{ Name = 'Delete'; Mask = 0x10000; Expected = 'Delete' }
|
|
|
|
|
@{ Name = 'DeleteSubdirectoriesAndFiles'; Mask = 0x40; Expected = 'Delete' }
|
|
|
|
|
@{ Name = 'Modify'; Mask = 0x1301BF; Expected = 'Read, Write, Delete' }
|
|
|
|
|
@{ Name = 'FullControl'; Mask = 0x1F01FF; Expected = 'Read, Write, Delete' }
|
|
|
|
|
)
|
|
|
|
|
$rightsSid = @{}
|
|
|
|
|
$entries = for ($i = 0; $i -lt $rightsCases.Count; $i++) {
|
|
|
|
|
$sid = 'S-1-5-21-1-2-3-{0}' -f (3001 + $i)
|
|
|
|
|
$rightsSid[$rightsCases[$i].Name] = $sid
|
|
|
|
|
'(A;;0x{0:X};;;{1})' -f $rightsCases[$i].Mask, $sid
|
|
|
|
|
}
|
|
|
|
|
$acl = Get-Acl -LiteralPath $rightsFolder
|
|
|
|
|
$acl.SetSecurityDescriptorSddlForm(('D:{0}' -f ($entries -join '')), 'Access')
|
|
|
|
|
Set-Acl -LiteralPath $rightsFolder -AclObject $acl
|
|
|
|
|
|
|
|
|
|
$rightsResult = @(Get-NTFSSimpleAccess -Path $rightsFolder -ExcludeInherited -IncludeRootFolder:$false -ErrorAction Stop)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Before 5.0.0-rc6, an entry with ReadData alone, which .NET never creates but other tools do, became None.
|
|
|
|
|
It 'Should reduce <Name> to <Expected>' -ForEach @(
|
|
|
|
|
@{ Name = 'ReadData'; Expected = 'Read' }
|
|
|
|
|
@{ Name = 'ReadAttributes'; Expected = 'Read' }
|
|
|
|
|
@{ Name = 'Traverse'; Expected = 'Read' }
|
|
|
|
|
@{ Name = 'ReadPermissions'; Expected = 'Read' }
|
|
|
|
|
@{ Name = 'CreateFiles'; Expected = 'Write' }
|
|
|
|
|
@{ Name = 'AppendData'; Expected = 'Write' }
|
|
|
|
|
@{ Name = 'WriteAttributes'; Expected = 'Write' }
|
|
|
|
|
@{ Name = 'ChangePermissions'; Expected = 'Write' }
|
|
|
|
|
@{ Name = 'TakeOwnership'; Expected = 'Write' }
|
|
|
|
|
@{ Name = 'Delete'; Expected = 'Delete' }
|
|
|
|
|
@{ Name = 'DeleteSubdirectoriesAndFiles'; Expected = 'Delete' }
|
|
|
|
|
@{ Name = 'Modify'; Expected = 'Read, Write, Delete' }
|
|
|
|
|
@{ Name = 'FullControl'; Expected = 'Read, Write, Delete' }
|
|
|
|
|
) {
|
|
|
|
|
$entry = @($rightsResult | Where-Object -FilterScript { $_.Identity.Sid -eq $rightsSid[$Name] })
|
|
|
|
|
|
|
|
|
|
$entry | Should -HaveCount 1
|
|
|
|
|
$entry[0].AccessRights.ToString() | Should -Be $Expected
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Context 'When it compares folders with their parent folder' {
|
|
|
|
|
BeforeAll {
|
|
|
|
|
# The parent grants Everyone ReadData to its subfolders and the user Full Control, so that a basic user
|
|
|
|
|
# can create the items; the child adds an entry of its own. The child is created after the DACL of the
|
|
|
|
|
# parent, so that it inherits only these entries.
|
|
|
|
|
$parent = New-TestSandboxItem -Sandbox $sandbox -Name 'SimpleParent' -Directory
|
|
|
|
|
$user = [System.Security.Principal.WindowsIdentity]::GetCurrent().User.Value
|
|
|
|
|
$acl = Get-Acl -LiteralPath $parent
|
|
|
|
|
$acl.SetSecurityDescriptorSddlForm("D:P(A;OICI;0x120089;;;WD)(A;OICI;FA;;;BA)(A;OICI;FA;;;SY)(A;OICI;FA;;;$user)", 'Access')
|
|
|
|
|
Set-Acl -LiteralPath $parent -AclObject $acl
|
|
|
|
|
$child = Join-Path -Path $parent -ChildPath 'Child'
|
|
|
|
|
Assert-TestSandboxPath -Sandbox $sandbox -Path $child
|
|
|
|
|
New-Item -ItemType Directory -Path $child | Out-Null
|
|
|
|
|
$acl = Get-Acl -LiteralPath $child
|
|
|
|
|
$acl.AddAccessRule((New-Object -TypeName 'System.Security.AccessControl.FileSystemAccessRule' -ArgumentList (
|
|
|
|
|
(New-Object -TypeName 'System.Security.Principal.SecurityIdentifier' -ArgumentList 'S-1-5-21-1-2-3-3101'),
|
|
|
|
|
[System.Security.AccessControl.FileSystemRights]::Modify, [System.Security.AccessControl.AccessControlType]::Allow
|
|
|
|
|
)))
|
|
|
|
|
Set-Acl -LiteralPath $child -AclObject $acl
|
|
|
|
|
$childFile = Join-Path -Path $child -ChildPath 'File.txt'
|
|
|
|
|
Assert-TestSandboxPath -Sandbox $sandbox -Path $childFile
|
|
|
|
|
Set-Content -LiteralPath $childFile -Value 'File' -ErrorAction Stop
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
It 'Should report all entries of the first folder and of the following folder only what the parent does not cover' {
|
|
|
|
|
$result = @(Get-NTFSSimpleAccess -Path $parent, $child -IncludeRootFolder:$false -ErrorAction Stop)
|
|
|
|
|
|
|
|
|
|
@($result | Where-Object -Property FullName -EQ -Value $parent) | Should -HaveCount 4
|
|
|
|
|
$childEntries = @($result | Where-Object -Property FullName -EQ -Value $child)
|
|
|
|
|
$childEntries | Should -HaveCount 1
|
|
|
|
|
$childEntries[0].Identity.Sid | Should -Be 'S-1-5-21-1-2-3-3101'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
It 'Should compare the folders from the pipeline in the same way' {
|
|
|
|
|
$result = @(Get-Item2 -Path $parent, $child | Get-NTFSSimpleAccess -IncludeRootFolder:$false -ErrorAction Stop)
|
|
|
|
|
|
|
|
|
|
@($result | Where-Object -Property FullName -EQ -Value $child).Identity.Sid | Should -Be 'S-1-5-21-1-2-3-3101'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
It 'Should report the parent folder of the first path first by default' {
|
|
|
|
|
$result = @(Get-NTFSSimpleAccess -Path $child -ErrorAction Stop)
|
|
|
|
|
|
|
|
|
|
$result[0].FullName | Should -Be $parent
|
|
|
|
|
@($result | Where-Object -Property FullName -EQ -Value $child).Identity.Sid | Should -Be 'S-1-5-21-1-2-3-3101'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Before 5.0.0-rc6, a relative path with a single folder name had no parent folder in the result.
|
|
|
|
|
It 'Should report the parent folder of a relative path first by default' {
|
|
|
|
|
Push-Location -LiteralPath $parent
|
|
|
|
|
try {
|
|
|
|
|
$result = @(Get-NTFSSimpleAccess -Path 'Child' -ErrorAction Stop)
|
|
|
|
|
}
|
|
|
|
|
finally {
|
|
|
|
|
Pop-Location
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$result[0].FullName | Should -Be $parent
|
|
|
|
|
@($result | Where-Object -Property FullName -EQ -Value $child).Identity.Sid | Should -Be 'S-1-5-21-1-2-3-3101'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
It 'Should use the current location without -Path' {
|
|
|
|
|
Push-Location -LiteralPath $child
|
|
|
|
|
try {
|
|
|
|
|
$result = @(Get-NTFSSimpleAccess -IncludeRootFolder:$false -ErrorAction Stop)
|
|
|
|
|
}
|
|
|
|
|
finally {
|
|
|
|
|
Pop-Location
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$result | ForEach-Object -Process { $_.FullName | Should -Be $child }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
It 'Should return only the inherited entries with -ExcludeExplicit' {
|
|
|
|
|
$result = @(Get-NTFSSimpleAccess -Path $child -ExcludeExplicit -IncludeRootFolder:$false -ErrorAction Stop)
|
|
|
|
|
|
|
|
|
|
$result | Should -HaveCount 4
|
|
|
|
|
$result.Identity.Sid | Should -Not -Contain 'S-1-5-21-1-2-3-3101'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
It 'Should skip a file without an error' {
|
|
|
|
|
$result = @(Get-NTFSSimpleAccess -Path $childFile -IncludeRootFolder:$false -ErrorVariable simpleErrors -ErrorAction SilentlyContinue)
|
|
|
|
|
|
|
|
|
|
$simpleErrors | Should -BeNullOrEmpty
|
|
|
|
|
$result | Should -BeNullOrEmpty
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
It 'Should report the security descriptor of a file' {
|
|
|
|
|
$result = @(Get-NTFSSecurityDescriptor -Path $childFile | Get-NTFSSimpleAccess -ErrorAction Stop)
|
|
|
|
|
|
|
|
|
|
$result | Should -Not -BeNullOrEmpty
|
|
|
|
|
$result | ForEach-Object -Process { $_.FullName | Should -Be $childFile }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
It 'Should write an error for a path that does not exist and continue with the next path' {
|
|
|
|
|
$missing = Join-Path -Path $parent -ChildPath 'Missing'
|
|
|
|
|
|
|
|
|
|
$result = @(Get-NTFSSimpleAccess -Path $missing, $child -IncludeRootFolder:$false -ErrorVariable simpleErrors -ErrorAction SilentlyContinue)
|
|
|
|
|
|
|
|
|
|
$simpleErrors | Should -HaveCount 1
|
|
|
|
|
$simpleErrors[0].FullyQualifiedErrorId | Should -BeLike 'ReadError,*'
|
|
|
|
|
$result | Should -Not -BeNullOrEmpty
|
|
|
|
|
$result | ForEach-Object -Process { $_.FullName | Should -Be $child }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Describe 'Remove-NTFSAccess' {
|
|
|
|
|
Context 'When a path does not exist' {
|
|
|
|
|
BeforeAll {
|
|
|
|
|
$missing = Join-Path -Path $sandbox -ChildPath 'Missing.txt'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Before 5.0.0, the cmdlet went on with the missing item and wrote a second, misleading RemoveAceError.
|
|
|
|
|
It 'Should write only the read error' {
|
|
|
|
|
Remove-NTFSAccess -Path $missing -Account 'Everyone' -AccessRights ReadData -ErrorVariable removeErrors -ErrorAction SilentlyContinue
|
|
|
|
|
|
|
|
|
|
$removeErrors | Should -HaveCount 1
|
|
|
|
|
$removeErrors[0].FullyQualifiedErrorId | Should -BeLike 'ReadFileError,*'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
It 'Should not stop with -PassThru' {
|
|
|
|
|
{ Remove-NTFSAccess -Path $missing -Account 'Everyone' -AccessRights ReadData -PassThru -ErrorAction SilentlyContinue } |
|
|
|
|
|
Should -Not -Throw
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Context 'When the entries cannot be changed' {
|
|
|
|
|
# Before 5.0.0, -PassThru returned the unchanged entries of the item after the error.
|
|
|
|
|
It 'Should write an error and return nothing with -PassThru' -Skip:$canBypassWriteDeny {
|
|
|
|
|
$protected = New-TestSandboxItem -Sandbox $sandbox -Name 'RemoveProtected'
|
|
|
|
|
Assert-TestSandboxPath -Sandbox $sandbox -Path $protected
|
|
|
|
|
Add-NTFSAccess -Path $protected -Account 'Everyone' -AccessRights ReadData
|
|
|
|
|
Block-TestWritePermission -Sandbox $sandbox -Path $protected
|
|
|
|
|
|
|
|
|
|
$result = @(Remove-NTFSAccess -Path $protected -Account 'Everyone' -AccessRights ReadData -PassThru -ErrorVariable removeErrors -ErrorAction SilentlyContinue)
|
|
|
|
|
|
|
|
|
|
$removeErrors | Should -HaveCount 1
|
|
|
|
|
$removeErrors[0].FullyQualifiedErrorId | Should -BeLike 'RemoveAceError,*'
|
|
|
|
|
$result | Should -BeNullOrEmpty
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
fix: write only the sections of the security descriptor that a cmdlet changes
The access and audit cmdlets wrote the owner of an item back with the
entries they changed. For a DACL without the auto-inherit flag, Windows
returns the owner and the group even when only the DACL is read, and the
cmdlets wrote every section that the descriptor held. Without the Restore
privilege, or on a file server that refuses the owner, the write failed
with error 1307 (#34).
- Add-NTFSAccess, Clear-NTFSAccess, Add-NTFSAudit, and Clear-NTFSAudit
read only the DACL or the SACL. FileSystemSecurity2.Write(),
Remove-NTFSAccess, and Remove-NTFSAudit write only the sections they
read, which also fixes the access inheritance cmdlets.
- Read together with the SACL, the inherited entries of such a DACL lose
their inherited flag when the parent folder has no SACL, and the
cmdlets stored them as explicit copies. Get-NTFSSecurityDescriptor now
reads the DACL in a separate call.
- Set-NTFSSecurityDescriptor writes only the sections that changed since
they were read; an unchanged descriptor writes nothing (maintainer
decision of 2026-10-06).
- Clear-NTFSAudit writes nothing for an item without a SACL, and reports
an error without the Security privilege (maintainer decision).
The regression tests failed before and pass after the fix in Windows
PowerShell 5.1 and PowerShell 7, elevated and as a basic user.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
4 weeks ago
|
|
|
Context 'When the item has an owner that the user cannot assign' {
|
|
|
|
|
BeforeAll {
|
|
|
|
|
$privateData['EnablePrivileges'] = $false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
AfterAll {
|
|
|
|
|
$privateData['EnablePrivileges'] = $enablePrivileges
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Before 5.0.0-rc3, the cmdlet read only the DACL, but wrote the owner that Windows returns with a DACL without
|
|
|
|
|
# the auto-inherit flag, which Windows refuses without the Restore privilege (#34). Any write of the DACL adds
|
|
|
|
|
# the flag, so the item keeps its DACL as created and has no explicit entry to remove.
|
|
|
|
|
It 'Should write no error and keep the owner' -Skip:(-not $canAssignAnyOwner) {
|
|
|
|
|
$file = New-TestSandboxItem -Sandbox $sandbox -Name 'RemoveOtherOwner'
|
|
|
|
|
Set-TestOwner -Sandbox $sandbox -Path $file -Sid $trustedInstaller
|
|
|
|
|
Get-RestorePrivilegeState | Should -Be 'Disabled'
|
|
|
|
|
|
|
|
|
|
Remove-NTFSAccess -Path $file -Account 'Everyone' -AccessRights ReadData -ErrorVariable removeErrors -ErrorAction SilentlyContinue
|
|
|
|
|
|
|
|
|
|
$removeErrors | Should -BeNullOrEmpty
|
|
|
|
|
(Get-Acl -LiteralPath $file).GetOwner($sidType).Value | Should -Be $trustedInstaller
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Context 'With a generic right' {
|
|
|
|
|
# Before 5.0.0, removing an entry with a generic right such as GENERIC_ALL failed with "The value '269484032' is
|
|
|
|
|
# not valid", because .NET rebuilds the rule and rejects generic rights (#17). Windows keeps generic rights in
|
|
|
|
|
# inherit-only entries of folders.
|
|
|
|
|
BeforeAll {
|
|
|
|
|
$guests = [System.Security.Principal.SecurityIdentifier]'S-1-5-32-546'
|
|
|
|
|
|
|
|
|
|
function New-GenericRightFolder {
|
|
|
|
|
[Diagnostics.CodeAnalysis.SuppressMessageAttribute(
|
|
|
|
|
'PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Test helper that only writes to the sandbox.'
|
|
|
|
|
)]
|
|
|
|
|
param ([string] $Entry)
|
|
|
|
|
|
|
|
|
|
$folder = New-TestSandboxItem -Sandbox $sandbox -Name 'Generic' -Directory
|
|
|
|
|
Assert-TestSandboxPath -Sandbox $sandbox -Path $folder
|
|
|
|
|
$acl = Get-Acl -LiteralPath $folder
|
|
|
|
|
$acl.SetSecurityDescriptorSddlForm(($acl.Sddl -replace 'D:(?<flags>[A-Z]*)', ('D:${flags}' + $Entry)))
|
|
|
|
|
Set-Acl -LiteralPath $folder -AclObject $acl
|
|
|
|
|
$folder
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function Get-GuestsRule {
|
|
|
|
|
param ([string] $Path)
|
|
|
|
|
|
|
|
|
|
(Get-Acl -LiteralPath $Path).GetAccessRules($true, $false, [System.Security.Principal.SecurityIdentifier]) |
|
|
|
|
|
Where-Object -Property IdentityReference -EQ -Value $guests
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
It 'Should remove an inherit-only entry that Get-NTFSAccess returned, and nothing else' -ForEach @(
|
|
|
|
|
@{ Case = 'Allow'; Entry = '(A;OICIIO;GA;;;BG)'; Specific = $false }
|
|
|
|
|
@{ Case = 'Allow with -RemoveSpecific'; Entry = '(A;OICIIO;GA;;;BG)'; Specific = $true }
|
|
|
|
|
@{ Case = 'Deny'; Entry = '(D;OICIIO;GA;;;BG)'; Specific = $false }
|
|
|
|
|
) {
|
|
|
|
|
$folder = New-GenericRightFolder -Entry $Entry
|
|
|
|
|
$before = (Get-Acl -LiteralPath $folder).Sddl
|
|
|
|
|
$before | Should -Match ([regex]::Escape($Entry))
|
|
|
|
|
|
|
|
|
|
Get-NTFSAccess -Path $folder -Account 'S-1-5-32-546' -ExcludeInherited |
|
|
|
|
|
Remove-NTFSAccess -RemoveSpecific:$Specific -ErrorVariable removeErrors -ErrorAction SilentlyContinue
|
|
|
|
|
|
|
|
|
|
$removeErrors | Should -BeNullOrEmpty
|
|
|
|
|
(Get-Acl -LiteralPath $folder).Sddl | Should -BeExactly $before.Replace($Entry, '')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
It 'Should remove only the requested generic right from an entry with two' {
|
|
|
|
|
$folder = New-GenericRightFolder -Entry '(A;OICIIO;0x90000000;;;BG)'
|
|
|
|
|
|
|
|
|
|
Remove-NTFSAccess -Path $folder -Account 'S-1-5-32-546' -AccessRights GenericRead -InheritanceFlags ContainerInherit, ObjectInherit -PropagationFlags InheritOnly -ErrorVariable removeErrors -ErrorAction SilentlyContinue
|
|
|
|
|
|
|
|
|
|
$removeErrors | Should -BeNullOrEmpty
|
|
|
|
|
$rule = Get-GuestsRule -Path $folder
|
|
|
|
|
$rule | Should -HaveCount 1
|
|
|
|
|
[int] $rule.FileSystemRights | Should -Be 0x10000000
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# A rule that matches the entry exactly is removed as it is, with the Synchronize right that the module adds
|
|
|
|
|
# to an Allow rule; otherwise an entry with only Synchronize would be left behind.
|
|
|
|
|
It 'Should remove an entry with GenericAll and Synchronize when -AccessRights names GenericAll' {
|
|
|
|
|
$folder = New-GenericRightFolder -Entry '(A;OICIIO;0x10100000;;;BG)'
|
|
|
|
|
|
|
|
|
|
Remove-NTFSAccess -Path $folder -Account 'S-1-5-32-546' -AccessRights GenericAll -InheritanceFlags ContainerInherit, ObjectInherit -PropagationFlags InheritOnly -ErrorVariable removeErrors -ErrorAction SilentlyContinue
|
|
|
|
|
|
|
|
|
|
$removeErrors | Should -BeNullOrEmpty
|
|
|
|
|
Get-GuestsRule -Path $folder | Should -BeNullOrEmpty
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Context 'With -RemoveSpecific' {
|
|
|
|
|
BeforeEach {
|
|
|
|
|
$removeFolder = New-TestSandboxItem -Sandbox $sandbox -Name 'RemoveSpecific' -Directory
|
|
|
|
|
$sd = Get-NTFSSecurityDescriptor -Path $removeFolder
|
|
|
|
|
Add-NTFSAccess -SecurityDescriptor $sd -Account 'Everyone' -AccessRights Modify
|
|
|
|
|
|
|
|
|
|
function Get-EveryoneRule {
|
|
|
|
|
$sd.SecurityDescriptor.GetAccessRules($true, $false, [System.Security.Principal.SecurityIdentifier]) |
|
|
|
|
|
Where-Object -FilterScript { $_.IdentityReference.Value -eq 'S-1-1-0' }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
It 'Should keep an entry that does not match exactly' {
|
|
|
|
|
Remove-NTFSAccess -SecurityDescriptor $sd -Account 'Everyone' -AccessRights ReadData -RemoveSpecific
|
|
|
|
|
|
|
|
|
|
(Get-EveryoneRule).FileSystemRights.HasFlag([System.Security.AccessControl.FileSystemRights]::Modify) | Should -BeTrue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
It 'Should remove an entry that matches exactly' {
|
|
|
|
|
Remove-NTFSAccess -SecurityDescriptor $sd -Account 'Everyone' -AccessRights Modify -RemoveSpecific
|
|
|
|
|
|
|
|
|
|
Get-EveryoneRule | Should -BeNullOrEmpty
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
It 'Should take the rights away from a matching entry without -RemoveSpecific' {
|
|
|
|
|
Remove-NTFSAccess -SecurityDescriptor $sd -Account 'Everyone' -AccessRights ReadData
|
|
|
|
|
|
|
|
|
|
$rule = Get-EveryoneRule
|
|
|
|
|
$rule | Should -Not -BeNullOrEmpty
|
|
|
|
|
$rule.FileSystemRights.HasFlag([System.Security.AccessControl.FileSystemRights]::ReadData) | Should -BeFalse
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Describe 'Add-NTFSAccess' {
|
|
|
|
|
Context 'When the entries cannot be changed' {
|
|
|
|
|
# Before 5.0.0, -PassThru returned the unchanged entries of the item after the error.
|
|
|
|
|
It 'Should write an error and return nothing with -PassThru' -Skip:$canBypassWriteDeny {
|
|
|
|
|
$protected = New-TestSandboxItem -Sandbox $sandbox -Name 'AddProtected'
|
|
|
|
|
Assert-TestSandboxPath -Sandbox $sandbox -Path $protected
|
|
|
|
|
Block-TestWritePermission -Sandbox $sandbox -Path $protected
|
|
|
|
|
|
|
|
|
|
$result = @(Add-NTFSAccess -Path $protected -Account 'Everyone' -AccessRights ReadData -PassThru -ErrorVariable addErrors -ErrorAction SilentlyContinue)
|
|
|
|
|
|
|
|
|
|
$addErrors | Should -HaveCount 1
|
|
|
|
|
$addErrors[0].FullyQualifiedErrorId | Should -BeLike 'AddAceError,*'
|
|
|
|
|
$result | Should -BeNullOrEmpty
|
|
|
|
|
}
|
|
|
|
|
}
|
fix: write only the sections of the security descriptor that a cmdlet changes
The access and audit cmdlets wrote the owner of an item back with the
entries they changed. For a DACL without the auto-inherit flag, Windows
returns the owner and the group even when only the DACL is read, and the
cmdlets wrote every section that the descriptor held. Without the Restore
privilege, or on a file server that refuses the owner, the write failed
with error 1307 (#34).
- Add-NTFSAccess, Clear-NTFSAccess, Add-NTFSAudit, and Clear-NTFSAudit
read only the DACL or the SACL. FileSystemSecurity2.Write(),
Remove-NTFSAccess, and Remove-NTFSAudit write only the sections they
read, which also fixes the access inheritance cmdlets.
- Read together with the SACL, the inherited entries of such a DACL lose
their inherited flag when the parent folder has no SACL, and the
cmdlets stored them as explicit copies. Get-NTFSSecurityDescriptor now
reads the DACL in a separate call.
- Set-NTFSSecurityDescriptor writes only the sections that changed since
they were read; an unchanged descriptor writes nothing (maintainer
decision of 2026-10-06).
- Clear-NTFSAudit writes nothing for an item without a SACL, and reports
an error without the Security privilege (maintainer decision).
The regression tests failed before and pass after the fix in Windows
PowerShell 5.1 and PowerShell 7, elevated and as a basic user.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
4 weeks ago
|
|
|
|
|
|
|
|
Context 'When the item has an owner that the user cannot assign' {
|
|
|
|
|
BeforeAll {
|
|
|
|
|
$privateData['EnablePrivileges'] = $false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
AfterAll {
|
|
|
|
|
$privateData['EnablePrivileges'] = $enablePrivileges
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Before 5.0.0-rc3, the cmdlet wrote the unchanged owner back. Without the Restore privilege, Windows refuses
|
|
|
|
|
# an owner that the user cannot assign, like a file server that refuses the owner (#34): (1307) This security
|
|
|
|
|
# ID may not be assigned as the owner of this object.
|
|
|
|
|
It 'Should add the entry and keep the owner' -Skip:(-not $canAssignAnyOwner) {
|
|
|
|
|
$file = New-TestSandboxItem -Sandbox $sandbox -Name 'OtherOwner'
|
|
|
|
|
Set-TestOwner -Sandbox $sandbox -Path $file -Sid $trustedInstaller
|
|
|
|
|
Get-RestorePrivilegeState | Should -Be 'Disabled'
|
|
|
|
|
|
|
|
|
|
Add-NTFSAccess -Path $file -Account 'Everyone' -AccessRights ReadData -ErrorVariable addErrors -ErrorAction SilentlyContinue
|
|
|
|
|
|
|
|
|
|
$addErrors | Should -BeNullOrEmpty
|
|
|
|
|
$acl = Get-Acl -LiteralPath $file
|
|
|
|
|
$acl.GetOwner($sidType).Value | Should -Be $trustedInstaller
|
|
|
|
|
@($acl.GetAccessRules($true, $false, $sidType) | Where-Object -FilterScript { $_.IdentityReference.Value -eq 'S-1-1-0' }) |
|
|
|
|
|
Should -HaveCount 1
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Context 'With inherited entries' {
|
|
|
|
|
# Before 5.0.0-rc3, the cmdlet read the DACL together with the SACL when the process held the Security
|
|
|
|
|
# privilege. When the folder has no SACL, Windows then returns the inherited entries of a DACL without the
|
|
|
|
|
# auto-inherit flag, such as that of a file in the temp folder of the user, without their inherited flag, and
|
|
|
|
|
# the cmdlet wrote them back as explicit copies.
|
|
|
|
|
It 'Should add one explicit entry and keep the inherited entries inherited' -Skip:(-not $holdsSecurityPrivilege) {
|
|
|
|
|
$file = New-TestSandboxItem -Sandbox $sandbox -Name 'Inherited'
|
|
|
|
|
Assert-TestSandboxPath -Sandbox $sandbox -Path $file
|
|
|
|
|
$inheritedCount = @((Get-Acl -LiteralPath $file).GetAccessRules($false, $true, $sidType)).Count
|
|
|
|
|
$inheritedCount | Should -BeGreaterThan 0
|
|
|
|
|
|
|
|
|
|
Add-NTFSAccess -Path $file -Account 'Everyone' -AccessRights ReadData
|
|
|
|
|
|
|
|
|
|
$acl = Get-Acl -LiteralPath $file
|
|
|
|
|
@($acl.GetAccessRules($true, $false, $sidType)) | Should -HaveCount 1
|
|
|
|
|
@($acl.GetAccessRules($false, $true, $sidType)) | Should -HaveCount $inheritedCount
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
fix: resolve the security descriptor sets without -AppliesTo
Defect 14. Add-NTFSAccess, Remove-NTFSAccess, Add-NTFSAudit, and
Remove-NTFSAudit have the sets PathSimple, PathComplex, SDSimple, and
SDComplex; the default PathComplex needs -Path. A command with
-SecurityDescriptor and without -AppliesTo, -InheritanceFlags, or
-PropagationFlags matched both SD sets and failed with "Parameter set
cannot be resolved". -AppliesTo is now mandatory in the Simple sets, so
such a command resolves to the Complex set and its default flags
(ContainerInherit, ObjectInherit / None, which is what AppliesTo
ThisFolderSubfoldersAndFiles means), as a command with -Path already did.
With -AppliesTo nothing changes.
The pages say "Required: True" for -AppliesTo and drop its never
reachable defaults; platyPS takes that metadata from the shipped help
file, so the help file was regenerated before the build.
Tests/Access.Tests.ps1: 6 tests on in-memory security descriptors.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
4 weeks ago
|
|
|
Describe 'Security descriptor parameter sets' {
|
|
|
|
|
BeforeAll {
|
|
|
|
|
$folder = New-TestSandboxItem -Sandbox $sandbox -Name 'ParameterSets' -Directory
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Before 5.0.0, PowerShell could not choose between the SDSimple and SDComplex parameter sets.
|
|
|
|
|
It '<_> should accept -SecurityDescriptor without -AppliesTo or the flag parameters' -ForEach @(
|
|
|
|
|
'Add-NTFSAccess', 'Remove-NTFSAccess'
|
|
|
|
|
) {
|
|
|
|
|
$sd = Get-NTFSSecurityDescriptor -Path $folder
|
|
|
|
|
|
|
|
|
|
{ & $_ -SecurityDescriptor $sd -Account 'Everyone' -AccessRights ReadData -ErrorAction Stop } | Should -Not -Throw
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# A descriptor from Get-NTFSSecurityDescriptor contains the audit entries only with the Security privilege.
|
|
|
|
|
It '<_> should accept -SecurityDescriptor without -AppliesTo or the flag parameters' -Skip:(-not $holdsSecurityPrivilege) -ForEach @(
|
|
|
|
|
'Add-NTFSAudit', 'Remove-NTFSAudit'
|
fix: resolve the security descriptor sets without -AppliesTo
Defect 14. Add-NTFSAccess, Remove-NTFSAccess, Add-NTFSAudit, and
Remove-NTFSAudit have the sets PathSimple, PathComplex, SDSimple, and
SDComplex; the default PathComplex needs -Path. A command with
-SecurityDescriptor and without -AppliesTo, -InheritanceFlags, or
-PropagationFlags matched both SD sets and failed with "Parameter set
cannot be resolved". -AppliesTo is now mandatory in the Simple sets, so
such a command resolves to the Complex set and its default flags
(ContainerInherit, ObjectInherit / None, which is what AppliesTo
ThisFolderSubfoldersAndFiles means), as a command with -Path already did.
With -AppliesTo nothing changes.
The pages say "Required: True" for -AppliesTo and drop its never
reachable defaults; platyPS takes that metadata from the shipped help
file, so the help file was regenerated before the build.
Tests/Access.Tests.ps1: 6 tests on in-memory security descriptors.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
4 weeks ago
|
|
|
) {
|
|
|
|
|
$sd = Get-NTFSSecurityDescriptor -Path $folder
|
|
|
|
|
|
|
|
|
|
{ & $_ -SecurityDescriptor $sd -Account 'Everyone' -AccessRights ReadData -ErrorAction Stop } | Should -Not -Throw
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
It 'Add-NTFSAccess should apply the entry to the folder, its subfolders, and files by default' {
|
|
|
|
|
$sd = Get-NTFSSecurityDescriptor -Path $folder
|
|
|
|
|
|
|
|
|
|
Add-NTFSAccess -SecurityDescriptor $sd -Account 'Everyone' -AccessRights ReadData
|
|
|
|
|
|
|
|
|
|
$rule = $sd.SecurityDescriptor.GetAccessRules($true, $false, [System.Security.Principal.SecurityIdentifier]) |
|
|
|
|
|
Where-Object -FilterScript { $_.IdentityReference.Value -eq 'S-1-1-0' }
|
|
|
|
|
$rule.InheritanceFlags | Should -Be ([System.Security.AccessControl.InheritanceFlags] 'ContainerInherit, ObjectInherit')
|
|
|
|
|
$rule.PropagationFlags | Should -Be ([System.Security.AccessControl.PropagationFlags]::None)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
It 'Add-NTFSAccess should still take -AppliesTo for a security descriptor' {
|
|
|
|
|
$sd = Get-NTFSSecurityDescriptor -Path $folder
|
|
|
|
|
|
|
|
|
|
Add-NTFSAccess -SecurityDescriptor $sd -Account 'Everyone' -AccessRights ReadData -AppliesTo ThisFolderOnly
|
|
|
|
|
|
|
|
|
|
$rule = $sd.SecurityDescriptor.GetAccessRules($true, $false, [System.Security.Principal.SecurityIdentifier]) |
|
|
|
|
|
Where-Object -FilterScript { $_.IdentityReference.Value -eq 'S-1-1-0' }
|
|
|
|
|
$rule.InheritanceFlags | Should -Be ([System.Security.AccessControl.InheritanceFlags]::None)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Describe 'Clear-NTFSAccess' {
|
|
|
|
|
Context 'With -DisableInheritance' {
|
|
|
|
|
# The cmdlet does not copy the inherited entries, so the item is left with an empty DACL. This is the
|
|
|
|
|
# documented behavior; Set-NTFSInheritance and Disable-NTFSAccessInheritance keep the entries.
|
|
|
|
|
It 'Should leave the item with an empty, protected DACL' {
|
|
|
|
|
$file = New-TestSandboxItem -Sandbox $sandbox -Name 'ClearAll'
|
|
|
|
|
Assert-TestSandboxPath -Sandbox $sandbox -Path $file
|
|
|
|
|
|
|
|
|
|
Clear-NTFSAccess -Path $file -DisableInheritance
|
|
|
|
|
|
|
|
|
|
$acl = Get-Acl -LiteralPath $file
|
|
|
|
|
$acl.AreAccessRulesProtected | Should -BeTrue
|
|
|
|
|
$acl.Access | Should -BeNullOrEmpty
|
|
|
|
|
}
|
|
|
|
|
}
|
fix: write only the sections of the security descriptor that a cmdlet changes
The access and audit cmdlets wrote the owner of an item back with the
entries they changed. For a DACL without the auto-inherit flag, Windows
returns the owner and the group even when only the DACL is read, and the
cmdlets wrote every section that the descriptor held. Without the Restore
privilege, or on a file server that refuses the owner, the write failed
with error 1307 (#34).
- Add-NTFSAccess, Clear-NTFSAccess, Add-NTFSAudit, and Clear-NTFSAudit
read only the DACL or the SACL. FileSystemSecurity2.Write(),
Remove-NTFSAccess, and Remove-NTFSAudit write only the sections they
read, which also fixes the access inheritance cmdlets.
- Read together with the SACL, the inherited entries of such a DACL lose
their inherited flag when the parent folder has no SACL, and the
cmdlets stored them as explicit copies. Get-NTFSSecurityDescriptor now
reads the DACL in a separate call.
- Set-NTFSSecurityDescriptor writes only the sections that changed since
they were read; an unchanged descriptor writes nothing (maintainer
decision of 2026-10-06).
- Clear-NTFSAudit writes nothing for an item without a SACL, and reports
an error without the Security privilege (maintainer decision).
The regression tests failed before and pass after the fix in Windows
PowerShell 5.1 and PowerShell 7, elevated and as a basic user.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
4 weeks ago
|
|
|
|
|
|
|
|
Context 'When the item has an owner that the user cannot assign' {
|
|
|
|
|
BeforeAll {
|
|
|
|
|
$privateData['EnablePrivileges'] = $false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
AfterAll {
|
|
|
|
|
$privateData['EnablePrivileges'] = $enablePrivileges
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Before 5.0.0-rc3, the cmdlet wrote the unchanged owner back, which Windows refuses without the Restore
|
|
|
|
|
# privilege (#34). For a DACL without the auto-inherit flag, such as that of a new file in the temp folder of
|
|
|
|
|
# the user, Windows returns the owner even when only the DACL is read. Any write of the DACL adds the flag, so
|
|
|
|
|
# the item keeps its DACL as created.
|
|
|
|
|
It 'Should write no error and keep the owner' -Skip:(-not $canAssignAnyOwner) {
|
|
|
|
|
$file = New-TestSandboxItem -Sandbox $sandbox -Name 'ClearOtherOwner'
|
|
|
|
|
Set-TestOwner -Sandbox $sandbox -Path $file -Sid $trustedInstaller
|
|
|
|
|
Get-RestorePrivilegeState | Should -Be 'Disabled'
|
|
|
|
|
|
|
|
|
|
Clear-NTFSAccess -Path $file -ErrorVariable clearErrors -ErrorAction SilentlyContinue
|
|
|
|
|
|
|
|
|
|
$clearErrors | Should -BeNullOrEmpty
|
|
|
|
|
$acl = Get-Acl -LiteralPath $file
|
|
|
|
|
$acl.GetOwner($sidType).Value | Should -Be $trustedInstaller
|
|
|
|
|
@($acl.GetAccessRules($true, $false, $sidType)) | Should -BeNullOrEmpty
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|