Browse Source

fix: let a stopped pipeline end Get-NTFSOwner without errors

Defect 9, both parts:

- Get-NTFSOwner wrote each owner inside a try block whose catch-all
  turned every exception into a ReadSecurityError. When a command such as
  Select-Object -First 1 stopped the pipeline, the cmdlet wrote "The
  pipeline has been stopped" as an error for every path, and the stop
  escaped into the caller's script. The owner is now written after the
  try block.
- After access was denied, the retry called the same failing GetOwner
  again before taking ownership, so it always failed and reported a
  WriteError. Taking ownership would also replace the owner that the
  cmdlet reports. The cmdlet now writes a ReadSecurityError with the
  category PermissionDenied and continues.

Tests/Owner.Tests.ps1 (new): 2 tests; the access-denied test skips where
the Backup privilege may bypass the deny entry (CI).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
pull/100/head
Raimund Andree 1 week ago
parent
commit
c00bc6af88
  1. 4
      CHANGELOG.md
  2. 4
      Docs/Cmdlets/Get-NTFSOwner.md
  3. 31
      NTFSSecurity/OwnerCmdlets/GetOwner.cs
  4. 2
      NTFSSecurity/en-US/NTFSSecurity.dll-Help.xml
  5. 54
      Tests/Owner.Tests.ps1

4
CHANGELOG.md

@ -77,5 +77,9 @@ The format is based on
- Fix `Get-NTFSInheritance -SecurityDescriptor`, which reported - Fix `Get-NTFSInheritance -SecurityDescriptor`, which reported
`AuditInheritanceEnabled` as `$true` for a security descriptor that was `AuditInheritanceEnabled` as `$true` for a security descriptor that was
read without its audit section; it now reports `$null`, like `-Path` read without its audit section; it now reports `$null`, like `-Path`
- Fix `Get-NTFSOwner`, which wrote a "The pipeline has been stopped" error
for every path when a command such as `Select-Object -First 1` stopped the
pipeline, and which repeated a failed read instead of reporting the
denied access
[Unreleased]: https://github.com/raandree/NTFSSecurity/compare/4.2.6...HEAD [Unreleased]: https://github.com/raandree/NTFSSecurity/compare/4.2.6...HEAD

4
Docs/Cmdlets/Get-NTFSOwner.md

@ -127,6 +127,10 @@ When the module setting `EnablePrivileges` is `$true` (the default in the `Priva
The module also adds an `Owner` script property to `System.IO.FileInfo` and `System.IO.DirectoryInfo`, so `(Get-Item C:\Data).Owner` returns the owning account as a `Security2.IdentityReference2` object as well. The module also adds an `Owner` script property to `System.IO.FileInfo` and `System.IO.DirectoryInfo`, so `(Get-Item C:\Data).Owner` returns the owning account as a `Security2.IdentityReference2` object as well.
If the owner of an item cannot be read because access is denied, the cmdlet writes the non-terminating error `ReadSecurityError` with the category `PermissionDenied` and continues with the next path. It does not take ownership of the item, which would replace the owner that it reports.
Before 5.0.0, a command that stopped the pipeline early, such as `Select-Object -First 1`, made the cmdlet write a `ReadSecurityError` with the message "The pipeline has been stopped" for every path.
## RELATED LINKS ## RELATED LINKS
[Set-NTFSOwner](Set-NTFSOwner.md) [Set-NTFSOwner](Set-NTFSOwner.md)

31
NTFSSecurity/OwnerCmdlets/GetOwner.cs

@ -43,10 +43,11 @@ namespace NTFSSecurity.OwnerCmdlets
{ {
if (ParameterSetName == "Path") if (ParameterSetName == "Path")
{ {
FileSystemInfo item = null;
foreach (var path in paths) foreach (var path in paths)
{ {
FileSystemInfo item = null;
FileSystemOwner owner = null;
try try
{ {
item = GetFileSystemInfo2(path); item = GetFileSystemInfo2(path);
@ -59,32 +60,22 @@ namespace NTFSSecurity.OwnerCmdlets
try try
{ {
WriteObject(FileSystemOwner.GetOwner(item)); owner = FileSystemOwner.GetOwner(item);
} }
catch (UnauthorizedAccessException) catch (UnauthorizedAccessException ex)
{ {
try // Taking ownership to read the owner would replace the owner that the cmdlet reports.
{ WriteError(new ErrorRecord(ex, "ReadSecurityError", ErrorCategory.PermissionDenied, path));
var ownerInfo = FileSystemOwner.GetOwner(item); continue;
var previousOwner = ownerInfo.Owner;
FileSystemOwner.SetOwner(item, System.Security.Principal.WindowsIdentity.GetCurrent().User);
WriteObject(FileSystemOwner.GetOwner(item));
FileSystemOwner.SetOwner(item, previousOwner);
}
catch (Exception ex2)
{
WriteError(new ErrorRecord(ex2, "ReadSecurityError", ErrorCategory.WriteError, path));
continue;
}
} }
catch (Exception ex) catch (Exception ex)
{ {
WriteError(new ErrorRecord(ex, "ReadSecurityError", ErrorCategory.OpenError, path)); WriteError(new ErrorRecord(ex, "ReadSecurityError", ErrorCategory.OpenError, path));
continue; continue;
} }
// Outside the try block, so that a stopped pipeline isn't reported as a read error.
WriteObject(owner);
} }
} }
else else

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

@ -6146,6 +6146,8 @@ PS C:\&gt; $orphaned | Select-Object FullName, Account, AccessRights, AuditFlags
<maml:alert> <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>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 module also adds an `Owner` script property to `System.IO.FileInfo` and `System.IO.DirectoryInfo`, so `(Get-Item C:\Data).Owner` returns the owning account as a `Security2.IdentityReference2` object as well.</maml:para> <maml:para>The module also adds an `Owner` script property to `System.IO.FileInfo` and `System.IO.DirectoryInfo`, so `(Get-Item C:\Data).Owner` returns the owning account as a `Security2.IdentityReference2` object as well.</maml:para>
<maml:para>If the owner of an item cannot be read because access is denied, the cmdlet writes the non-terminating error `ReadSecurityError` with the category `PermissionDenied` and continues with the next path. It does not take ownership of the item, which would replace the owner that it reports.</maml:para>
<maml:para>Before 5.0.0, a command that stopped the pipeline early, such as `Select-Object -First 1`, made the cmdlet write a `ReadSecurityError` with the message "The pipeline has been stopped" for every path.</maml:para>
</maml:alert> </maml:alert>
</maml:alertSet> </maml:alertSet>
<command:examples> <command:examples>

54
Tests/Owner.Tests.ps1

@ -0,0 +1,54 @@
<#
Tests the owner 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 Backup privilege, Windows may grant reading the owner despite a deny entry.
$canBypassDeny = Test-PrivilegeHeld -Name 'SeBackupPrivilege'
}
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 'Owner'
Push-Location -LiteralPath $sandbox
}
AfterAll {
Pop-Location
Remove-TestSandbox -Sandbox $sandbox
Remove-Module -Name NTFSSecurity -Force -ErrorAction SilentlyContinue
}
Describe 'Get-NTFSOwner' {
Context 'When a downstream command stops the pipeline' {
It 'Should stop without writing errors' {
$files = 1..3 | ForEach-Object -Process { New-TestSandboxItem -Sandbox $sandbox -Name "Owner$_" }
$result = @(Get-NTFSOwner -Path $files -ErrorVariable ownerErrors -ErrorAction SilentlyContinue | Select-Object -First 1)
$ownerErrors | Should -BeNullOrEmpty
$result | Should -HaveCount 1
}
}
Context 'When the owner cannot be read' {
It 'Should write one permission error and keep the owner' -Skip:$canBypassDeny {
$file = New-TestSandboxItem -Sandbox $sandbox -Name 'Denied'
Block-TestReadPermission -Sandbox $sandbox -Path $file
$result = @(Get-NTFSOwner -Path $file -ErrorVariable ownerErrors -ErrorAction SilentlyContinue)
$result | Should -BeNullOrEmpty
$ownerErrors | Should -HaveCount 1
$ownerErrors[0].FullyQualifiedErrorId | Should -BeLike 'ReadSecurityError,*'
$ownerErrors[0].CategoryInfo.Category | Should -Be 'PermissionDenied'
}
}
}
Loading…
Cancel
Save