diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a80a74..2144024 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -269,5 +269,9 @@ The format is based on - Fix `Get-NTFSEffectiveAccess`, which returned no access when the computer of `-ServerName` couldn't be reached, although it warned that it had calculated the result on this computer; it now returns that result +- Fix the cmdlets that enable the Backup, Restore, Take Ownership, and + Security privileges, which left them enabled in the session when a later + command, such as `Select-Object -First`, or a terminating error stopped + the pipeline early; they now disable them also then [Unreleased]: https://github.com/raandree/NTFSSecurity/compare/4.2.6...HEAD diff --git a/Docs/Concepts.md b/Docs/Concepts.md index 9ba01cf..d196d8e 100644 --- a/Docs/Concepts.md +++ b/Docs/Concepts.md @@ -188,11 +188,14 @@ session runs elevated (**Run as administrator**). The access, audit, inheritance, owner, and security descriptor cmdlets enable these privileges automatically while they run and disable the ones they -enabled when they finish. If a privilege cannot be enabled, the cmdlet -continues without it. You can turn this behavior off with the -`EnablePrivileges` module setting. Before 5.0.0, the inheritance cmdlets were -an exception: they always tried to enable the privileges, and when -`EnablePrivileges` was `$false`, they left them enabled. +enabled when they finish, also when a later command such as +`Select-Object -First` or a terminating error stops the pipeline early. If a +privilege cannot be enabled, the cmdlet continues without it. You can turn +this behavior off with the `EnablePrivileges` module setting. Before 5.0.0, +the inheritance cmdlets were an exception: they always tried to enable the +privileges, and when `EnablePrivileges` was `$false`, they left them enabled. +And before 5.0.0, every cmdlet left the privileges enabled in the session when +the pipeline stopped early. `Enable-Privileges` enables the four privileges for the current PowerShell process until you run `Disable-Privileges` or close the session. diff --git a/NTFSSecurity/BaseCmdlets.cs b/NTFSSecurity/BaseCmdlets.cs index 91b5353..9a300be 100644 --- a/NTFSSecurity/BaseCmdlets.cs +++ b/NTFSSecurity/BaseCmdlets.cs @@ -182,13 +182,19 @@ namespace NTFSSecurity #endregion } - public class BaseCmdletWithPrivControl : BaseCmdlet + public class BaseCmdletWithPrivControl : BaseCmdlet, IDisposable { protected PrivilegeAndAttributesCollection privileges = null; protected PrivilegeControl privControl = new PrivilegeControl(); private List enabledPrivileges = new List(); Hashtable privateData = null; + // A cmdlet that enables the privileges for the session, such as Enable-Privileges, keeps them enabled. + protected virtual bool KeepEnabledPrivileges + { + get { return false; } + } + protected override void BeginProcessing() { privateData = (Hashtable)MyInvocation.MyCommand.Module.PrivateData; @@ -208,15 +214,40 @@ namespace NTFSSecurity //disable all privileges that have been enabled by this cmdlet WriteVerbose(string.Format("Disabeling all {0} enabled privileges...", enabledPrivileges.Count)); - foreach (var privilege in enabledPrivileges) + foreach (var privilege in DisableEnabledPrivileges()) { - DisablePrivilege((Privilege)Enum.Parse(typeof(Privilege), privilege)); WriteVerbose(string.Format("\t{0} disabled", privilege)); } WriteVerbose(string.Format("...finished")); } } + // PowerShell calls Dispose also when a later command or a terminating error stops the pipeline, and then + // skips EndProcessing. Before 5.0.0-rc6, the privileges that the cmdlet had enabled stayed enabled in the + // session in that case. + public void Dispose() + { + DisableEnabledPrivileges(); + GC.SuppressFinalize(this); + } + + // Disables the privileges that this cmdlet enabled, once, and returns their names. + private List DisableEnabledPrivileges() + { + var disabled = new List(); + if (!KeepEnabledPrivileges) + { + foreach (var privilege in enabledPrivileges) + { + DisablePrivilege((Privilege)Enum.Parse(typeof(Privilege), privilege)); + disabled.Add(privilege); + } + } + + enabledPrivileges.Clear(); + return disabled; + } + protected void EnablePrivilege(Privilege privilege) { //throw an exception if the specified prililege is not held by the client diff --git a/NTFSSecurity/OtherCmdlets.cs b/NTFSSecurity/OtherCmdlets.cs index 6cd18d3..fd21efd 100644 --- a/NTFSSecurity/OtherCmdlets.cs +++ b/NTFSSecurity/OtherCmdlets.cs @@ -61,6 +61,12 @@ namespace NTFSSecurity { //nothing as we want to keep the privileges enabled } + + // Keeps the privileges enabled also when the pipeline stops early and PowerShell calls only Dispose. + protected override bool KeepEnabledPrivileges + { + get { return true; } + } } #endregion Enable-Privileges diff --git a/Tests/Privileges.Tests.ps1 b/Tests/Privileges.Tests.ps1 index 55b3c97..c688cf2 100644 --- a/Tests/Privileges.Tests.ps1 +++ b/Tests/Privileges.Tests.ps1 @@ -119,3 +119,44 @@ Describe 'Inheritance cmdlets' { } } } + +Describe 'Privileges when the pipeline stops early' { + BeforeAll { + $files = 1..3 | ForEach-Object -Process { New-TestSandboxItem -Sandbox $sandbox -Name "Stopped$_" } + $missing = Join-Path -Path $sandbox -ChildPath 'StoppedMissing.txt' + } + + BeforeEach { + Disable-Privileges -ErrorAction SilentlyContinue -WarningAction SilentlyContinue + } + + AfterEach { + # A failing test must not leave the privileges enabled for the tests that follow. + Disable-Privileges -ErrorAction SilentlyContinue -WarningAction SilentlyContinue + } + + # Before 5.0.0-rc6, a cmdlet disabled the privileges that it had enabled only in EndProcessing, which PowerShell + # skips when a later command or a terminating error stops the pipeline. The Backup, Restore, Take Ownership, and + # Security privileges then stayed enabled in the session. + It 'Should disable the privileges after Select-Object -First stops the pipeline' -Skip:(-not $holdsPrivileges) { + Get-BackupPrivilegeState | Should -Be 'Disabled' + + Get-NTFSOwner -Path $files | Select-Object -First 1 | Out-Null + + Get-BackupPrivilegeState | Should -Be 'Disabled' + } + + It 'Should disable the privileges after a terminating error' -Skip:(-not $holdsPrivileges) { + Get-BackupPrivilegeState | Should -Be 'Disabled' + + { Get-NTFSAccess -Path $missing, $files[0] -ErrorAction Stop } | Should -Throw + + Get-BackupPrivilegeState | Should -Be 'Disabled' + } + + It 'Enable-Privileges should keep the privileges enabled also when the pipeline stops early' -Skip:(-not $holdsPrivileges) { + Enable-Privileges -PassThru | Select-Object -First 1 | Out-Null + + Get-BackupPrivilegeState | Should -Be 'Enabled' + } +}