Browse Source

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>
ai/release-5.0.0-rc6
Raimund Andree 4 days ago
parent
commit
c4b422ae97
  1. 4
      CHANGELOG.md
  2. 2
      Docs/Cmdlets/Get-NTFSSimpleAccess.md
  3. 4
      NTFSSecurity/SimpleAccessCmdlets/SimpleAccessCmdlets.cs
  4. 2
      NTFSSecurity/en-US/NTFSSecurity.dll-Help.xml
  5. 4
      Security2/FileSystem/SimpleFileSystemAccessRule.cs
  6. 161
      Tests/Access.Tests.ps1

4
CHANGELOG.md

@ -283,5 +283,9 @@ The format is based on
another command in the pipeline, such as `Disable-Privileges`, had
disabled one of them; a privilege that they can't disable now gives a
warning, and they still disable the others
- Fix `Get-NTFSSimpleAccess`, which showed no rights for an entry that
grants only `ReadData`, and which left out the parent folder of a
relative path with a single folder name, although `-IncludeRootFolder` is
on by default
[Unreleased]: https://github.com/raandree/NTFSSecurity/compare/4.2.6...HEAD

2
Docs/Cmdlets/Get-NTFSSimpleAccess.md

@ -190,7 +190,7 @@ When the module setting `EnablePrivileges` is `$true` (the default in the `Priva
The simplified rights hide which exact rights an account holds. Use `Get-NTFSAccess` when you need the full access control entry, and `Get-NTFSEffectiveAccess` when you need the rights that result from all entries together.
Before 5.0.0, the cmdlet ignored `-Account` and `-SecurityDescriptor`, and its output had no table view.
Before 5.0.0, the cmdlet ignored `-Account` and `-SecurityDescriptor`, and its output had no table view. It also showed no rights for an entry that grants only `ReadData`, which other tools than .NET create, and it left out the parent folder of a relative path with a single folder name, such as `Data`.
## RELATED LINKS

4
NTFSSecurity/SimpleAccessCmdlets/SimpleAccessCmdlets.cs

@ -47,7 +47,9 @@ namespace NTFSSecurity
//as this cmdlet retreives also the current working folder to show the permissions.
if (includeRootFolder & isFirstFolder)
{
string rootPath = System.IO.Path.GetDirectoryName(paths[0]);
// Resolved first, so that a relative path with a single folder name has the current location as its
// parent folder; before 5.0.0-rc6, such a path had no parent folder in the result.
string rootPath = System.IO.Path.GetDirectoryName(GetRelativePath(paths[0]));
if (!string.IsNullOrEmpty(rootPath))
{

2
NTFSSecurity/en-US/NTFSSecurity.dll-Help.xml

@ -6624,7 +6624,7 @@ PS C:\Data&gt; Get-NTFSSecurityDescriptor</dev:code>
<maml:alert>
<maml:para>When the module setting `EnablePrivileges` is `$true` (the default in the `PrivateData` section of NTFSSecurity.psd1), this cmdlet tries to enable the Backup, Restore, Take Ownership, and Security privileges while it runs and disables the privileges it enabled when it finishes. These privileges are only available in an elevated session of an account that holds them, such as a member of the local Administrators group. If a privilege cannot be enabled, the cmdlet continues without it and writes a debug message.</maml:para>
<maml:para>The simplified rights hide which exact rights an account holds. Use `Get-NTFSAccess` when you need the full access control entry, and `Get-NTFSEffectiveAccess` when you need the rights that result from all entries together.</maml:para>
<maml:para>Before 5.0.0, the cmdlet ignored `-Account` and `-SecurityDescriptor`, and its output had no table view.</maml:para>
<maml:para>Before 5.0.0, the cmdlet ignored `-Account` and `-SecurityDescriptor`, and its output had no table view. It also showed no rights for an entry that grants only `ReadData`, which other tools than .NET create, and it left out the parent folder of a relative path with a single folder name, such as `Data`.</maml:para>
</maml:alert>
</maml:alertSet>
<command:examples>

4
Security2/FileSystem/SimpleFileSystemAccessRule.cs

@ -42,6 +42,10 @@ namespace Security2
if ((accessRights & FileSystemRights2.Read) == FileSystemRights2.Read)
{ result |= SimpleFileSystemAccessRights.Read; }
// An entry with ReadData alone, which other tools than .NET create, was None before 5.0.0-rc6.
if ((accessRights & FileSystemRights2.ReadData) == FileSystemRights2.ReadData)
{ result |= SimpleFileSystemAccessRights.Read; }
if ((accessRights & FileSystemRights2.CreateFiles) == FileSystemRights2.CreateFiles)
{ result |= SimpleFileSystemAccessRights.Write; }

161
Tests/Access.Tests.ps1

@ -228,6 +228,167 @@ Describe 'Get-NTFSSimpleAccess' {
$text | Should -Match 'Account\s+Access Rights\s+Type'
}
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' {

Loading…
Cancel
Save