diff --git a/CHANGELOG.md b/CHANGELOG.md index 89733d2..33a14d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -304,5 +304,8 @@ The format is based on for a descriptor that the cmdlet wrote as the owner, and which turned a failed read after a successful write into another attempt of the write and a `WriteSdError`; it now writes a `ReadSecurityError` for that read +- Fix `Get-NTFSOrphanedAccess`, which reported an item that it couldn't + read as an `AddAceError`; it now writes a `ReadSecurityError`, like + `Get-NTFSAccess` [Unreleased]: https://github.com/raandree/NTFSSecurity/compare/4.2.6...HEAD diff --git a/Docs/Cmdlets/Add-NTFSAccess.md b/Docs/Cmdlets/Add-NTFSAccess.md index 8d79939..d30e4c6 100644 --- a/Docs/Cmdlets/Add-NTFSAccess.md +++ b/Docs/Cmdlets/Add-NTFSAccess.md @@ -288,7 +288,7 @@ With `-PassThru`, the cmdlet writes all access control entries, explicit and inh 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. -If the ACL of an item cannot be written because access is denied, the cmdlet tries once more after making the current account the owner of the item, and restores the previous owner afterwards. Changing the owner of an item requires the Take Ownership and Restore privileges, so this fallback only succeeds in an elevated session of an account that holds them. +If the ACL of an item cannot be written because access is denied, the cmdlet tries once more after making the current account the owner of the item, and restores the previous owner afterwards. That fallback only succeeds when the account can take ownership of the item, through the Take Ownership right on the item or the Take Ownership privilege, and can set the previous owner back, which needs the Restore privilege unless that owner is the account itself or one of its groups. When the owner changes, Windows removes the entries for OWNER RIGHTS of the item. In the `Path` parameter sets, the cmdlet reads and writes only the DACL of the item and leaves its owner, its group, and its SACL as they are. Before 5.0.0, it also wrote the owner back, which failed with error 1307, "This security ID may not be assigned as the owner of this object", when the account may not assign that owner, such as on some file servers. In an elevated session, it could also store the inherited entries of the item as explicit entries. diff --git a/Docs/Cmdlets/Clear-NTFSAccess.md b/Docs/Cmdlets/Clear-NTFSAccess.md index 863c82c..075d73d 100644 --- a/Docs/Cmdlets/Clear-NTFSAccess.md +++ b/Docs/Cmdlets/Clear-NTFSAccess.md @@ -143,7 +143,7 @@ The cmdlet writes nothing. Use `Get-NTFSAccess` to inspect the result. 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. -If the ACL of an item cannot be written because access is denied, the cmdlet tries once more after making the current account the owner of the item, and restores the previous owner afterwards. Changing the owner of an item requires the Take Ownership and Restore privileges, so this fallback only succeeds in an elevated session of an account that holds them. +If the ACL of an item cannot be written because access is denied, the cmdlet tries once more after making the current account the owner of the item, and restores the previous owner afterwards. That fallback only succeeds when the account can take ownership of the item, through the Take Ownership right on the item or the Take Ownership privilege, and can set the previous owner back, which needs the Restore privilege unless that owner is the account itself or one of its groups. When the owner changes, Windows removes the entries for OWNER RIGHTS of the item. In the `Path` parameter set, the cmdlet reads and writes only the DACL of the item and leaves its owner, its group, and its SACL as they are. Before 5.0.0, it also wrote the owner back, which failed with error 1307, "This security ID may not be assigned as the owner of this object", when the account may not assign that owner, such as on some file servers. diff --git a/Docs/Cmdlets/Get-NTFSAccess.md b/Docs/Cmdlets/Get-NTFSAccess.md index 5e407db..26ff37b 100644 --- a/Docs/Cmdlets/Get-NTFSAccess.md +++ b/Docs/Cmdlets/Get-NTFSAccess.md @@ -180,7 +180,7 @@ One object per access control entry, with the account, the rights, the access ty 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. -If the ACL of an item cannot be read because access is denied, the cmdlet tries once more after making the current account the owner of the item, and restores the previous owner afterwards. Changing the owner of an item requires the Take Ownership and Restore privileges, so this fallback only succeeds in an elevated session of an account that holds them. +If the ACL of an item cannot be read because access is denied, the cmdlet tries once more after making the current account the owner of the item, and restores the previous owner afterwards. Reading the owner, which that fallback needs first, requires the same Read Permissions right as reading the ACL, so the cmdlet then writes a non-terminating `ReadSecurityError` and continues with the next item. Entries whose account cannot be translated into a name are returned with their SID. Use `Get-NTFSOrphanedAccess` to list only those entries. diff --git a/Docs/Cmdlets/Get-NTFSOrphanedAccess.md b/Docs/Cmdlets/Get-NTFSOrphanedAccess.md index 4207c87..aa3b8d8 100644 --- a/Docs/Cmdlets/Get-NTFSOrphanedAccess.md +++ b/Docs/Cmdlets/Get-NTFSOrphanedAccess.md @@ -172,7 +172,7 @@ One object per orphaned access control entry. The `Account` property holds the u 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. -If the ACL of an item cannot be read because access is denied, the cmdlet tries once more after making the current account the owner of the item, and restores the previous owner afterwards. Changing the owner of an item requires the Take Ownership and Restore privileges, so this fallback only succeeds in an elevated session of an account that holds them. +If the ACL of an item cannot be read because access is denied, the cmdlet tries once more after making the current account the owner of the item, and restores the previous owner afterwards. Reading the owner, which that fallback needs first, requires the same Read Permissions right as reading the ACL, so the cmdlet then writes a non-terminating `ReadSecurityError` and continues with the next item. Before 5.0.0, it reported that error as an `AddAceError`. Before 5.0.0, the cmdlet ignored `-Account` and `-SecurityDescriptor`, and after a path whose ACL could not be read, it returned the orphaned entries of the previous item again. diff --git a/Docs/Cmdlets/Remove-NTFSAccess.md b/Docs/Cmdlets/Remove-NTFSAccess.md index c82dfab..86e8d5f 100644 --- a/Docs/Cmdlets/Remove-NTFSAccess.md +++ b/Docs/Cmdlets/Remove-NTFSAccess.md @@ -300,7 +300,7 @@ With `-PassThru`, the cmdlet writes all access control entries, explicit and inh 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. -If the ACL of an item cannot be written because access is denied, the cmdlet tries once more after making the current account the owner of the item, and restores the previous owner afterwards. Changing the owner of an item requires the Take Ownership and Restore privileges, so this fallback only succeeds in an elevated session of an account that holds them. +If the ACL of an item cannot be written because access is denied, the cmdlet tries once more after making the current account the owner of the item, and restores the previous owner afterwards. That fallback only succeeds when the account can take ownership of the item, through the Take Ownership right on the item or the Take Ownership privilege, and can set the previous owner back, which needs the Restore privilege unless that owner is the account itself or one of its groups. When the owner changes, Windows removes the entries for OWNER RIGHTS of the item. Removing rights from an entry that does not exist is not an error; the cmdlet leaves the ACL unchanged. diff --git a/Docs/Concepts.md b/Docs/Concepts.md index 680c0c3..e63abbc 100644 --- a/Docs/Concepts.md +++ b/Docs/Concepts.md @@ -209,7 +209,13 @@ state. When reading or changing an item fails with an access-denied error, most of these cmdlets make the current user the owner of the item, retry, and then restore the previous owner, also when the retry fails. Before 5.0.0, a failed -retry left the current user as the owner. This requires the privileges above. +retry left the current user as the owner. Taking ownership needs the Take +Ownership right on the item or the Take Ownership privilege, and setting the +previous owner back needs the Restore privilege unless that owner is the user +or one of its groups. When the owner of an item changes, Windows removes its +entries for OWNER RIGHTS, so the retry removes such entries as well. For +reading, the retry doesn't help: the cmdlet must read the owner first, which +needs the same right as reading the permissions. Reading or changing audit entries always requires the Security privilege. Without it, the audit cmdlets fail, and `Get-NTFSEffectiveAccess` warns that diff --git a/NTFSSecurity/AccessCmdlets/GetOrphanedAccess.cs b/NTFSSecurity/AccessCmdlets/GetOrphanedAccess.cs index 94ad30c..14ecb9d 100644 --- a/NTFSSecurity/AccessCmdlets/GetOrphanedAccess.cs +++ b/NTFSSecurity/AccessCmdlets/GetOrphanedAccess.cs @@ -55,7 +55,8 @@ namespace NTFSSecurity } catch (Exception ex2) { - this.WriteError(new ErrorRecord(ex2, "AddAceError", ErrorCategory.WriteError, path)); + // A read error; before 5.0.0-rc6, it was reported as an AddAceError. + this.WriteError(new ErrorRecord(ex2, "ReadSecurityError", ErrorCategory.ReadError, path)); continue; } } diff --git a/NTFSSecurity/en-US/NTFSSecurity.dll-Help.xml b/NTFSSecurity/en-US/NTFSSecurity.dll-Help.xml index 9f406d2..9caecdc 100644 --- a/NTFSSecurity/en-US/NTFSSecurity.dll-Help.xml +++ b/NTFSSecurity/en-US/NTFSSecurity.dll-Help.xml @@ -719,7 +719,7 @@ 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. - If the ACL of an item cannot be written because access is denied, the cmdlet tries once more after making the current account the owner of the item, and restores the previous owner afterwards. Changing the owner of an item requires the Take Ownership and Restore privileges, so this fallback only succeeds in an elevated session of an account that holds them. + If the ACL of an item cannot be written because access is denied, the cmdlet tries once more after making the current account the owner of the item, and restores the previous owner afterwards. That fallback only succeeds when the account can take ownership of the item, through the Take Ownership right on the item or the Take Ownership privilege, and can set the previous owner back, which needs the Restore privilege unless that owner is the account itself or one of its groups. When the owner changes, Windows removes the entries for OWNER RIGHTS of the item. In the `Path` parameter sets, the cmdlet reads and writes only the DACL of the item and leaves its owner, its group, and its SACL as they are. Before 5.0.0, it also wrote the owner back, which failed with error 1307, "This security ID may not be assigned as the owner of this object", when the account may not assign that owner, such as on some file servers. In an elevated session, it could also store the inherited entries of the item as explicit entries. @@ -1711,7 +1711,7 @@ PS C:\> Set-NTFSSecurityDescriptor -SecurityDescriptor $sd 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. - If the ACL of an item cannot be written because access is denied, the cmdlet tries once more after making the current account the owner of the item, and restores the previous owner afterwards. Changing the owner of an item requires the Take Ownership and Restore privileges, so this fallback only succeeds in an elevated session of an account that holds them. + If the ACL of an item cannot be written because access is denied, the cmdlet tries once more after making the current account the owner of the item, and restores the previous owner afterwards. That fallback only succeeds when the account can take ownership of the item, through the Take Ownership right on the item or the Take Ownership privilege, and can set the previous owner back, which needs the Restore privilege unless that owner is the account itself or one of its groups. When the owner changes, Windows removes the entries for OWNER RIGHTS of the item. In the `Path` parameter set, the cmdlet reads and writes only the DACL of the item and leaves its owner, its group, and its SACL as they are. Before 5.0.0, it also wrote the owner back, which failed with error 1307, "This security ID may not be assigned as the owner of this object", when the account may not assign that owner, such as on some file servers. @@ -4585,7 +4585,7 @@ PS C:\> Disable-Privileges 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. - If the ACL of an item cannot be read because access is denied, the cmdlet tries once more after making the current account the owner of the item, and restores the previous owner afterwards. Changing the owner of an item requires the Take Ownership and Restore privileges, so this fallback only succeeds in an elevated session of an account that holds them. + If the ACL of an item cannot be read because access is denied, the cmdlet tries once more after making the current account the owner of the item, and restores the previous owner afterwards. Reading the owner, which that fallback needs first, requires the same Read Permissions right as reading the ACL, so the cmdlet then writes a non-terminating `ReadSecurityError` and continues with the next item. Entries whose account cannot be translated into a name are returned with their SID. Use `Get-NTFSOrphanedAccess` to list only those entries. Before 5.0.0, after a path whose ACL could not be read, the cmdlet returned the entries of the previous item again. For the root of a drive, such as `C:`, or of a volume, such as `\?\Volume{GUID}`, the cmdlets that read and change security use the root folder of the volume, like Explorer, `icacls`, and `Get-Acl`. Before 5.0.0, they read and changed the security descriptor of the drive itself, a device object with other entries. @@ -5741,7 +5741,7 @@ PS C:\> Get-NTFSAudit -SecurityDescriptor $sd 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. - If the ACL of an item cannot be read because access is denied, the cmdlet tries once more after making the current account the owner of the item, and restores the previous owner afterwards. Changing the owner of an item requires the Take Ownership and Restore privileges, so this fallback only succeeds in an elevated session of an account that holds them. + If the ACL of an item cannot be read because access is denied, the cmdlet tries once more after making the current account the owner of the item, and restores the previous owner afterwards. Reading the owner, which that fallback needs first, requires the same Read Permissions right as reading the ACL, so the cmdlet then writes a non-terminating `ReadSecurityError` and continues with the next item. Before 5.0.0, it reported that error as an `AddAceError`. Before 5.0.0, the cmdlet ignored `-Account` and `-SecurityDescriptor`, and after a path whose ACL could not be read, it returned the orphaned entries of the previous item again. @@ -8425,7 +8425,7 @@ PS C:\Data> Get-NTFSSecurityDescriptor 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. - If the ACL of an item cannot be written because access is denied, the cmdlet tries once more after making the current account the owner of the item, and restores the previous owner afterwards. Changing the owner of an item requires the Take Ownership and Restore privileges, so this fallback only succeeds in an elevated session of an account that holds them. + If the ACL of an item cannot be written because access is denied, the cmdlet tries once more after making the current account the owner of the item, and restores the previous owner afterwards. That fallback only succeeds when the account can take ownership of the item, through the Take Ownership right on the item or the Take Ownership privilege, and can set the previous owner back, which needs the Restore privilege unless that owner is the account itself or one of its groups. When the owner changes, Windows removes the entries for OWNER RIGHTS of the item. Removing rights from an entry that does not exist is not an error; the cmdlet leaves the ACL unchanged. In the `Path` parameter sets, the cmdlet writes only the DACL of the item and leaves its owner, its group, and its SACL as they are. Before 5.0.0, it could also write the owner back, which failed with error 1307, "This security ID may not be assigned as the owner of this object", when the account may not assign that owner, such as on some file servers. An entry with a generic right, such as `GenericAll`, can be removed, for example by piping it from `Get-NTFSAccess`. Windows keeps generic rights in the inherit-only entries of folders. Before 5.0.0, the cmdlet failed for such an entry with the error "The value '269484032' is not valid for this usage of the type FileSystemRights". diff --git a/Tests/PathErrors.Tests.ps1 b/Tests/PathErrors.Tests.ps1 new file mode 100644 index 0000000..3202c8e --- /dev/null +++ b/Tests/PathErrors.Tests.ps1 @@ -0,0 +1,232 @@ +<# + Tests the error handling that the cmdlets with -Path share, with the module built in NTFSSecurity\bin\Release on + files in a sandbox folder: a path that doesn't exist, an item whose owner may not read its permissions, and an item + whose owner may not change its permissions, which the cmdlets that write the DACL handle by taking ownership. Each + error belongs to its path only, and the cmdlet continues with the next one. The tests turn the module setting + EnablePrivileges off, so that an elevated session meets the same denials as a basic user, and restore it. +#> +[Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSUseDeclaredVarsMoreThanAssignments', '', Justification = 'Pester shares variables between blocks.' +)] +param () + +BeforeDiscovery { + Import-Module -Name (Join-Path -Path $PSScriptRoot -ChildPath 'TestHelpers.psm1') -Force + $holdsSecurityPrivilege = Test-PrivilegeHeld -Name 'SeSecurityPrivilege' + $currentUser = [System.Security.Principal.WindowsIdentity]::GetCurrent().User.Value + $readEntry = @{ Account = 'S-1-1-0'; AccessRights = 'ReadData' } + # An audit entry on a file has no inheritance flags. + $auditEntry = @{ Account = 'S-1-1-0'; AccessRights = 'ReadData'; InheritanceFlags = 'None'; PropagationFlags = 'None' } + + # Output: whether the cmdlet writes an object for the next path, an existing file. + $missingPathCases = @( + @{ Command = 'Get-NTFSAccess'; Parameters = @{}; ErrorId = 'ReadFileError'; Output = $true } + @{ Command = 'Add-NTFSAccess'; Parameters = $readEntry; ErrorId = 'ReadFileError'; Output = $false } + @{ Command = 'Remove-NTFSAccess'; Parameters = $readEntry; ErrorId = 'ReadFileError'; Output = $false } + @{ Command = 'Clear-NTFSAccess'; Parameters = @{}; ErrorId = 'ReadFileError'; Output = $false } + @{ Command = 'Get-NTFSEffectiveAccess'; Parameters = @{ Account = 'S-1-1-0' }; ErrorId = 'ReadFileError'; Output = $true } + @{ Command = 'Get-NTFSOrphanedAccess'; Parameters = @{}; ErrorId = 'ReadFileError'; Output = $false } + @{ Command = 'Get-NTFSInheritance'; Parameters = @{}; ErrorId = 'ReadFileError'; Output = $true } + @{ Command = 'Enable-NTFSAccessInheritance'; Parameters = @{}; ErrorId = 'ReadFileError'; Output = $false } + @{ Command = 'Disable-NTFSAccessInheritance'; Parameters = @{}; ErrorId = 'ReadFileError'; Output = $false } + @{ Command = 'Set-NTFSInheritance'; Parameters = @{ AccessInheritanceEnabled = $true }; ErrorId = 'ReadFileError'; Output = $false } + @{ Command = 'Get-NTFSOwner'; Parameters = @{}; ErrorId = 'ReadFileError'; Output = $true } + @{ Command = 'Set-NTFSOwner'; Parameters = @{ Account = $currentUser }; ErrorId = 'ReadFileError'; Output = $false } + @{ Command = 'Get-NTFSSecurityDescriptor'; Parameters = @{}; ErrorId = 'ReadFileError'; Output = $true } + @{ Command = 'Get-Item2'; Parameters = @{}; ErrorId = 'FileNotFound'; Output = $true } + @{ Command = 'Get-FileHash2'; Parameters = @{}; ErrorId = 'ReadFileError'; Output = $true } + @{ Command = 'Get-NTFSHardLink'; Parameters = @{}; ErrorId = 'FileNotFound'; Output = $true } + ) + + # The audit cmdlets need the Security privilege for the next path. + $missingPathAuditCases = @( + @{ Command = 'Get-NTFSAudit'; Parameters = @{} } + @{ Command = 'Add-NTFSAudit'; Parameters = $auditEntry } + @{ Command = 'Remove-NTFSAudit'; Parameters = $auditEntry } + @{ Command = 'Clear-NTFSAudit'; Parameters = @{} } + @{ Command = 'Enable-NTFSAuditInheritance'; Parameters = @{} } + @{ Command = 'Disable-NTFSAuditInheritance'; Parameters = @{} } + ) + + $deniedReadCases = @( + @{ Command = 'Get-NTFSAccess'; Parameters = @{}; Output = $true } + @{ Command = 'Get-NTFSEffectiveAccess'; Parameters = @{ Account = 'S-1-1-0' }; Output = $true } + @{ Command = 'Get-NTFSOrphanedAccess'; Parameters = @{}; Output = $false } + @{ Command = 'Get-NTFSInheritance'; Parameters = @{}; Output = $true } + @{ Command = 'Get-NTFSOwner'; Parameters = @{}; Output = $true } + @{ Command = 'Get-NTFSSecurityDescriptor'; Parameters = @{}; Output = $true } + ) +} + +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 'PathErrors' + Push-Location -LiteralPath $sandbox + + $privateData = (Get-Module -Name NTFSSecurity).PrivateData + $enablePrivileges = $privateData['EnablePrivileges'] + $privateData['EnablePrivileges'] = $false + $sidType = [System.Security.Principal.SecurityIdentifier] + + function Get-TestMissingPath { + $path = Join-Path -Path $sandbox -ChildPath ('Missing-{0}' -f [guid]::NewGuid().ToString('N').Substring(0, 8)) + Assert-TestSandboxPath -Sandbox $sandbox -Path $path + $path + } + + function Get-TestAcl { + # .NET, because Get-Acl of an elevated Windows PowerShell reads also items that deny reading their permissions. + # .NET Core has the method as an extension method. + param ([string] $Path) + + $info = New-Object -TypeName 'System.IO.FileInfo' -ArgumentList $Path + if ($PSVersionTable.PSEdition -eq 'Desktop') { + $info.GetAccessControl() + } + else { + [System.IO.FileSystemAclExtensions]::GetAccessControl($info) + } + } +} + +AfterAll { + $privateData['EnablePrivileges'] = $enablePrivileges + Pop-Location + Remove-TestSandbox -Sandbox $sandbox + Remove-Module -Name NTFSSecurity -Force -ErrorAction SilentlyContinue +} + +Describe 'A path that does not exist' { + It ' should write a for it and continue with the next path' -ForEach $missingPathCases { + $missing = Get-TestMissingPath + $file = New-TestSandboxItem -Sandbox $sandbox -Name 'Next' + + $output = @(& $Command -Path $missing, $file @Parameters -ErrorVariable pathErrors -ErrorAction SilentlyContinue -WarningAction SilentlyContinue) + + $pathErrors | Should -HaveCount 1 + $pathErrors[0].FullyQualifiedErrorId | Should -BeLike "$ErrorId,*" + $pathErrors[0].TargetObject | Should -Be $missing + if ($Output) { + $output | Should -Not -BeNullOrEmpty + } + } + + It ' should write a ReadFileError for it and continue with the next path' -ForEach $missingPathAuditCases -Skip:(-not $holdsSecurityPrivilege) { + $privateData['EnablePrivileges'] = $true + try { + $missing = Get-TestMissingPath + $file = New-TestSandboxItem -Sandbox $sandbox -Name 'NextAudit' + + & $Command -Path $missing, $file @Parameters -ErrorVariable pathErrors -ErrorAction SilentlyContinue | Out-Null + } + finally { + $privateData['EnablePrivileges'] = $false + } + + $pathErrors | Should -HaveCount 1 + $pathErrors[0].FullyQualifiedErrorId | Should -BeLike 'ReadFileError,*' + $pathErrors[0].TargetObject | Should -Be $missing + } +} + +Describe 'An item whose owner may not read its permissions' { + # A deny entry for OWNER RIGHTS replaces the right of the owner to read the security descriptor. Taking ownership + # can't help: the cmdlet must read the owner first, which needs the same right. Before 5.0.0-rc6, + # Get-NTFSOrphanedAccess reported this as an AddAceError. + It ' should write a ReadSecurityError for it and continue with the next path' -ForEach $deniedReadCases { + $blocked = New-TestSandboxItem -Sandbox $sandbox -Name 'Unreadable' + Block-TestReadPermission -Sandbox $sandbox -Path $blocked + $file = New-TestSandboxItem -Sandbox $sandbox -Name 'NextReadable' + + $output = @(& $Command -Path $blocked, $file @Parameters -ErrorVariable pathErrors -ErrorAction SilentlyContinue -WarningAction SilentlyContinue) + + $pathErrors | Should -HaveCount 1 + $pathErrors[0].FullyQualifiedErrorId | Should -BeLike 'ReadSecurityError,*' + $pathErrors[0].TargetObject | Should -Be $blocked + if ($Output) { + $output | Should -Not -BeNullOrEmpty + } + } +} + +Describe 'An item whose owner may not change its permissions' { + # A deny entry for OWNER RIGHTS replaces the right of the owner to change the DACL. The cmdlets take ownership, + # which Windows answers by removing the OWNER RIGHTS entries, write the DACL, and set the previous owner back. + BeforeEach { + $file = New-TestSandboxItem -Sandbox $sandbox -Name 'Unchangeable' + $acl = Get-TestAcl -Path $file + $owner = $acl.GetOwner($sidType).Value + } + + It 'Add-NTFSAccess should take ownership, add the entry, and set the owner back' { + Add-TestDenyRule -Sandbox $sandbox -Path $file -Rights @{ 'S-1-3-4' = 'ChangePermissions' } + + Add-NTFSAccess -Path $file -Account 'S-1-1-0' -AccessRights ReadData -ErrorVariable changeErrors -ErrorAction SilentlyContinue + + $changeErrors | Should -BeNullOrEmpty + $acl = Get-TestAcl -Path $file + $acl.GetOwner($sidType).Value | Should -Be $owner + @($acl.GetAccessRules($true, $false, $sidType) | Where-Object -FilterScript { $_.IdentityReference.Value -eq 'S-1-1-0' }) | Should -HaveCount 1 + @($acl.GetAccessRules($true, $false, $sidType) | Where-Object -FilterScript { $_.IdentityReference.Value -eq 'S-1-3-4' }) | Should -BeNullOrEmpty + } + + It 'Remove-NTFSAccess should take ownership, remove the entry, and set the owner back' { + Add-NTFSAccess -Path $file -Account 'S-1-1-0' -AccessRights ReadData + Add-TestDenyRule -Sandbox $sandbox -Path $file -Rights @{ 'S-1-3-4' = 'ChangePermissions' } + + Remove-NTFSAccess -Path $file -Account 'S-1-1-0' -AccessRights ReadData -ErrorVariable changeErrors -ErrorAction SilentlyContinue + + $changeErrors | Should -BeNullOrEmpty + $acl = Get-TestAcl -Path $file + $acl.GetOwner($sidType).Value | Should -Be $owner + @($acl.GetAccessRules($true, $false, $sidType) | Where-Object -FilterScript { $_.IdentityReference.Value -eq 'S-1-1-0' }) | Should -BeNullOrEmpty + } + + It 'Clear-NTFSAccess should take ownership, remove the explicit entries, and set the owner back' { + Add-NTFSAccess -Path $file -Account 'S-1-1-0' -AccessRights ReadData + Add-TestDenyRule -Sandbox $sandbox -Path $file -Rights @{ 'S-1-3-4' = 'ChangePermissions' } + + Clear-NTFSAccess -Path $file -ErrorVariable changeErrors -ErrorAction SilentlyContinue + + $changeErrors | Should -BeNullOrEmpty + $acl = Get-TestAcl -Path $file + $acl.GetOwner($sidType).Value | Should -Be $owner + @($acl.GetAccessRules($true, $false, $sidType)) | Should -BeNullOrEmpty + } + + It 'Disable-NTFSAccessInheritance should take ownership, protect the DACL, and set the owner back' { + Add-TestDenyRule -Sandbox $sandbox -Path $file -Rights @{ 'S-1-3-4' = 'ChangePermissions' } + + Disable-NTFSAccessInheritance -Path $file -ErrorVariable changeErrors -ErrorAction SilentlyContinue + + $changeErrors | Should -BeNullOrEmpty + $acl = Get-TestAcl -Path $file + $acl.GetOwner($sidType).Value | Should -Be $owner + $acl.AreAccessRulesProtected | Should -BeTrue + } + + It 'Enable-NTFSAccessInheritance should take ownership, let the DACL inherit, and set the owner back' { + Disable-NTFSAccessInheritance -Path $file + Add-TestDenyRule -Sandbox $sandbox -Path $file -Rights @{ 'S-1-3-4' = 'ChangePermissions' } + + Enable-NTFSAccessInheritance -Path $file -ErrorVariable changeErrors -ErrorAction SilentlyContinue + + $changeErrors | Should -BeNullOrEmpty + $acl = Get-TestAcl -Path $file + $acl.GetOwner($sidType).Value | Should -Be $owner + $acl.AreAccessRulesProtected | Should -BeFalse + } + + It 'Set-NTFSInheritance should take ownership, protect the DACL, and set the owner back' { + Add-TestDenyRule -Sandbox $sandbox -Path $file -Rights @{ 'S-1-3-4' = 'ChangePermissions' } + + Set-NTFSInheritance -Path $file -AccessInheritanceEnabled $false -ErrorVariable changeErrors -ErrorAction SilentlyContinue + + $changeErrors | Should -BeNullOrEmpty + $acl = Get-TestAcl -Path $file + $acl.GetOwner($sidType).Value | Should -Be $owner + $acl.AreAccessRulesProtected | Should -BeTrue + } +}