<# Live tests of the module against a Windows file server in a lab, for the cases that depend on the file server or on domain accounts and that the tests in the Tests folder can't cover. Invoke-NTFSSecurityLabTest.ps1 prepares the lab and runs this file on the client in the roles Delegate, ServerAdmin, and Admin, as the accounts of these roles, and then on the file server in the role Server, which checks the security descriptors that the runs on the client left, without the module. README.md describes the cases and the lab. Without a configuration, all tests are skipped. #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute( 'PSUseDeclaredVarsMoreThanAssignments', '', Justification = 'Pester shares variables between blocks.' )] [Diagnostics.CodeAnalysis.SuppressMessageAttribute( 'PSReviewUnusedParameter', '', Justification = 'Pester passes the data of the container to the blocks.' )] param ( [string] $ModulePath, [string] $ConfigurationPath, [ValidateSet('', 'Delegate', 'ServerAdmin', 'Admin', 'Server')] [string] $Role ) BeforeDiscovery { $configured = -not [string]::IsNullOrEmpty($ConfigurationPath) $variants = @( @{ Variant = 'LegacyDacl'; Description = 'a DACL without the auto-inherit flag'; AutoInherited = $false } @{ Variant = 'AutoInheritedDacl'; Description = 'an auto-inherited DACL'; AutoInherited = $true } ) $operations = 'AddAccess', 'RemoveAccess', 'ClearAccess', 'DisableInheritance', 'EnableInheritance', 'SetInheritance', 'SetSecurityDescriptor' $auditSuccessCases = @( @{ AuditRole = 'Admin'; Description = 'an administrator of the file server and the client' } @{ AuditRole = 'ServerAdmin'; Description = 'an administrator of the file server only' } ) $ownedFolders = @( foreach ($variant in $variants) { foreach ($operation in $operations) { @{ Folder = 'Case1\{0}\{1}' -f $variant.Variant, $operation } } } foreach ($auditRole in 'Admin', 'ServerAdmin', 'Delegate') { foreach ($operation in 'GetAudit', 'AddAudit', 'RemoveAudit') { @{ Folder = 'Case2\{0}\{1}' -f $auditRole, $operation } } } ) # The administrators of the file server add and remove the audit entries; the delegated account changes nothing. $auditExpectations = @( foreach ($auditRole in 'Admin', 'ServerAdmin', 'Delegate') { $mayWrite = $auditRole -ne 'Delegate' @{ Folder = "Case2\$auditRole\GetAudit"; Count = 1 } @{ Folder = "Case2\$auditRole\AddAudit"; Count = [int]$mayWrite } @{ Folder = "Case2\$auditRole\RemoveAudit"; Count = [int](-not $mayWrite) } } ) # Case 5: only the administrators of the file server hold the Restore privilege there, which assigning an owner # other than the account itself needs. $ownerCases = @( @{ OwnerRole = 'Admin'; Description = 'an administrator of the file server and the client'; MayAssign = $true } @{ OwnerRole = 'ServerAdmin'; Description = 'an administrator of the file server only'; MayAssign = $true } @{ OwnerRole = 'Delegate'; Description = 'the delegated account, an administrator of the client only'; MayAssign = $false } ) $ownerExpectations = @( foreach ($ownerCase in $ownerCases) { @{ Folder = "Case5\$($ownerCase.OwnerRole)\GetOwner"; Owner = 'Administrators' } @{ Folder = "Case5\$($ownerCase.OwnerRole)\TakeOwnership"; Owner = $ownerCase.OwnerRole } @{ Folder = "Case5\$($ownerCase.OwnerRole)\AssignOwner"; Owner = if ($ownerCase.MayAssign) { 'Subject' } else { 'Administrators' } } } ) # Case 6: the state of the SACL that each folder has after the runs. The folders inherit one audit entry; the # administrators of the file server change them, the delegated account changes nothing. $auditInheritanceExpectations = @( foreach ($auditRole in 'Admin', 'ServerAdmin', 'Delegate') { $mayWrite = $auditRole -ne 'Delegate' @{ Folder = "Case6\$auditRole\DisableAuditInheritance"; Protected = $mayWrite; Explicit = [int]$mayWrite; Inherited = [int](-not $mayWrite) } @{ Folder = "Case6\$auditRole\EnableAuditInheritance"; Protected = -not $mayWrite; Explicit = 0; Inherited = [int]$mayWrite } @{ Folder = "Case6\$auditRole\ClearAudit"; Protected = $false; Explicit = [int](-not $mayWrite); Inherited = 1 } @{ Folder = "Case6\$auditRole\GetInheritance"; Protected = $false; Explicit = 0; Inherited = 1 } } ) $ownedFolders += @( foreach ($auditRole in 'Admin', 'ServerAdmin', 'Delegate') { foreach ($operation in 'DisableAuditInheritance', 'EnableAuditInheritance', 'ClearAudit', 'GetInheritance') { @{ Folder = "Case6\$auditRole\$operation" } } } ) # Case 9: the accounts of other domains and forests that the script created, if any. $foreignAccounts = @( if ($configured) { foreach ($account in (Get-Content -LiteralPath $ConfigurationPath -Raw | ConvertFrom-Json).ForeignAccounts) { @{ Name = $account.Name; Sid = $account.Sid; Rights = $account.Rights; EffectiveRights = [long]$account.EffectiveRights } } } ) # Case 10: the cmdlets that a later command in the pipeline stops, and the roles whose items the file server checks. $laterCommandCases = @( foreach ($name in 'Remove-Item2', 'Copy-Item2', 'Move-Item2', 'Set-NTFSOwner', 'Set-NTFSSecurityDescriptor') { foreach ($style in 'Select-Object -First 1', 'throw') { @{ Name = $name; Style = $style } } } ) $laterCommandStreamCases = @( foreach ($case in @( @{ Name = 'Set-NTFSSecurityDescriptor'; Stream = 'verbose' } @{ Name = 'Get-FileHash2'; Stream = 'verbose' } @{ Name = 'Set-NTFSOwner'; Stream = 'debug' } )) { foreach ($style in 'Select-Object -First 1', 'throw') { @{ Name = $case.Name; Stream = $case.Stream; Style = $style } } } ) $laterCommandStates = @( foreach ($stateRole in 'Admin', 'ServerAdmin', 'Delegate') { @{ StateRole = $stateRole } } ) } BeforeAll { if ($ConfigurationPath) { . (Join-Path -Path $PSScriptRoot -ChildPath 'NTFSSecurity.LabHelpers.ps1') $configuration = Get-Content -LiteralPath $ConfigurationPath -Raw | ConvertFrom-Json Assert-LabTestTarget -Configuration $configuration # On the client, the tests use the share; on the file server, the folder of the share. $runRoot = if ($Role -eq 'Server') { $configuration.ServerPath } else { $configuration.SharePath } if ($ModulePath) { Import-Module -Name (Join-Path -Path $ModulePath -ChildPath 'NTFSSecurity.psd1') -Force -ErrorAction Stop } $administrators = 'S-1-5-32-544' $everyone = 'S-1-1-0' $sidType = [System.Security.Principal.SecurityIdentifier] $synchronize = 0x100000L } function Get-LabPath { param ([string] $RelativePath) # Normalized, so that neither '..' nor '/' leads out of the folder of the run. $path = [System.IO.Path]::GetFullPath((Join-Path -Path $runRoot -ChildPath $RelativePath)) if ([System.IO.Path]::IsPathRooted($RelativePath) -or -not $path.StartsWith($runRoot.TrimEnd('\') + '\', [System.StringComparison]::OrdinalIgnoreCase)) { throw "'$RelativePath' must be a path in the folder of the run." } $path } function Get-LabOwner { param ([string] $Path) (Get-LabSecurityDescriptor -Path $Path).Owner.Value } function Get-LabExplicitAccessRule { param ([string] $Path, [string] $Sid) (Get-Acl -LiteralPath $Path).GetAccessRules($true, $false, $sidType) | Where-Object -FilterScript { $_.IdentityReference.Value -eq $Sid } } function Format-LabError { param ([object[]] $ErrorRecord) foreach ($record in $ErrorRecord) { if ($null -ne $record) { '{0}: {1}' -f $record.FullyQualifiedErrorId, $record.Exception.Message } } } function Format-LabRight { param ([object] $Right) # .NET adds Synchronize to every allow entry that it creates. '0x{0:X}' -f (([long]$Right) -bor $synchronize) } } AfterAll { Remove-Module -Name NTFSSecurity -Force -ErrorAction SilentlyContinue } Describe 'Account of the run' -Tag 'Delegate', 'ServerAdmin', 'Admin', 'Server' -Skip:(-not $configured) { It 'Should run as the account of the role' { [System.Security.Principal.WindowsIdentity]::GetCurrent().User.Value | Should -Be $configuration.Accounts.$Role.Sid } It 'Should be an administrator of this computer only in the roles that are' { $principal = New-Object -TypeName 'System.Security.Principal.WindowsPrincipal' -ArgumentList ( [System.Security.Principal.WindowsIdentity]::GetCurrent() ) $expected = if ($env:COMPUTERNAME -eq $configuration.FileServer) { $configuration.Accounts.$Role.FileServerAdministrator } else { $configuration.Accounts.$Role.ClientAdministrator } $principal.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) | Should -Be $expected } It 'Should test the module version of the run' -Skip:($Role -eq 'Server') { $module = Get-Module -Name NTFSSecurity $version = [string]$module.Version if ($module.PrivateData.PSData.Prerelease) { $version = '{0}-{1}' -f $version, $module.PrivateData.PSData.Prerelease } $version | Should -Be $configuration.ModuleVersion } } Describe 'Access and inheritance cmdlets on a share folder whose owner the account may not assign (#34)' -Tag 'Delegate' -Skip:(-not $configured) { # The delegated account has Full Control on the folders through a domain group, but isn't an administrator of the # file server, so the file server refuses Administrators as the owner that the account writes: (1307) This security # ID may not be assigned as the owner of this object. Before 5.0.0-rc3, the cmdlets wrote the unchanged owner back # whenever they had read it: Windows returns the owner with a DACL that is read alone when the DACL has no # auto-inherit flag, and Get-NTFSSecurityDescriptor always reads it. Context 'With ' -ForEach $variants { BeforeAll { $folder = Get-LabPath -RelativePath "Case1\$Variant" } It 'Should start with folders that Administrators own' { foreach ($operation in 'AddAccess', 'RemoveAccess', 'ClearAccess', 'DisableInheritance', 'EnableInheritance', 'SetInheritance', 'SetSecurityDescriptor') { $path = Join-Path -Path $folder -ChildPath $operation Get-LabOwner -Path $path | Should -Be $administrators -Because $operation Test-LabDaclAutoInherited -Path $path | Should -Be $AutoInherited -Because $operation } } It 'Add-NTFSAccess should add the entry and keep the owner' { $path = Join-Path -Path $folder -ChildPath 'AddAccess' Add-NTFSAccess -Path $path -Account $everyone -AccessRights ReadData -ErrorVariable operationErrors -ErrorAction SilentlyContinue Format-LabError -ErrorRecord $operationErrors | Should -BeNullOrEmpty Get-LabOwner -Path $path | Should -Be $administrators @(Get-LabExplicitAccessRule -Path $path -Sid $everyone) | Should -HaveCount 1 } It 'Remove-NTFSAccess should remove the entry and keep the owner' { $path = Join-Path -Path $folder -ChildPath 'RemoveAccess' @(Get-LabExplicitAccessRule -Path $path -Sid $everyone) | Should -HaveCount 1 Remove-NTFSAccess -Path $path -Account $everyone -AccessRights ReadAndExecute -InheritanceFlags 'ContainerInherit, ObjectInherit' -PropagationFlags None -ErrorVariable operationErrors -ErrorAction SilentlyContinue Format-LabError -ErrorRecord $operationErrors | Should -BeNullOrEmpty Get-LabOwner -Path $path | Should -Be $administrators Get-LabExplicitAccessRule -Path $path -Sid $everyone | Should -BeNullOrEmpty } It 'Clear-NTFSAccess should remove the explicit entries and keep the owner' { $path = Join-Path -Path $folder -ChildPath 'ClearAccess' Clear-NTFSAccess -Path $path -ErrorVariable operationErrors -ErrorAction SilentlyContinue Format-LabError -ErrorRecord $operationErrors | Should -BeNullOrEmpty Get-LabOwner -Path $path | Should -Be $administrators (Get-Acl -LiteralPath $path).GetAccessRules($true, $false, $sidType) | Should -BeNullOrEmpty } It 'Disable-NTFSAccessInheritance should disable the inheritance and keep the owner' { $path = Join-Path -Path $folder -ChildPath 'DisableInheritance' Disable-NTFSAccessInheritance -Path $path -ErrorVariable operationErrors -ErrorAction SilentlyContinue Format-LabError -ErrorRecord $operationErrors | Should -BeNullOrEmpty Get-LabOwner -Path $path | Should -Be $administrators (Get-Acl -LiteralPath $path).AreAccessRulesProtected | Should -BeTrue } It 'Enable-NTFSAccessInheritance should enable the inheritance and keep the owner' { $path = Join-Path -Path $folder -ChildPath 'EnableInheritance' (Get-Acl -LiteralPath $path).AreAccessRulesProtected | Should -BeTrue Enable-NTFSAccessInheritance -Path $path -ErrorVariable operationErrors -ErrorAction SilentlyContinue Format-LabError -ErrorRecord $operationErrors | Should -BeNullOrEmpty Get-LabOwner -Path $path | Should -Be $administrators (Get-Acl -LiteralPath $path).AreAccessRulesProtected | Should -BeFalse } It 'Set-NTFSInheritance should disable the inheritance and keep the owner' { $path = Join-Path -Path $folder -ChildPath 'SetInheritance' Set-NTFSInheritance -Path $path -AccessInheritanceEnabled $false -ErrorVariable operationErrors -ErrorAction SilentlyContinue Format-LabError -ErrorRecord $operationErrors | Should -BeNullOrEmpty Get-LabOwner -Path $path | Should -Be $administrators (Get-Acl -LiteralPath $path).AreAccessRulesProtected | Should -BeTrue } It 'Set-NTFSSecurityDescriptor should write the added entry and keep the owner' { $path = Join-Path -Path $folder -ChildPath 'SetSecurityDescriptor' $descriptor = Get-NTFSSecurityDescriptor -Path $path -ErrorVariable operationErrors -ErrorAction SilentlyContinue Add-NTFSAccess -SecurityDescriptor $descriptor -Account $everyone -AccessRights ReadData -ErrorVariable +operationErrors -ErrorAction SilentlyContinue Set-NTFSSecurityDescriptor -SecurityDescriptor $descriptor -ErrorVariable +operationErrors -ErrorAction SilentlyContinue Format-LabError -ErrorRecord $operationErrors | Should -BeNullOrEmpty Get-LabOwner -Path $path | Should -Be $administrators @(Get-LabExplicitAccessRule -Path $path -Sid $everyone) | Should -HaveCount 1 } } } Describe 'Audit cmdlets on a share folder' -Skip:(-not $configured) { # Over SMB, the file server checks whether the account holds the Security privilege there; the role Server checks # the audit entries that the runs left on the file server. foreach ($auditCase in $auditSuccessCases) { Context 'As ' -Tag $auditCase.AuditRole -ForEach @($auditCase) { BeforeAll { $folder = Get-LabPath -RelativePath "Case2\$AuditRole" } It 'Get-NTFSAudit should return the audit entry of the folder' { $entries = @(Get-NTFSAudit -Path (Join-Path -Path $folder -ChildPath 'GetAudit') -ErrorVariable operationErrors -ErrorAction SilentlyContinue) Format-LabError -ErrorRecord $operationErrors | Should -BeNullOrEmpty $entries | Should -HaveCount 1 $entries[0].Account.Sid | Should -Be $everyone $entries[0].AuditFlags | Should -Be 'Success' [long]$entries[0].AccessRights | Should -Be 0x10000 } It 'Add-NTFSAudit should add the audit entry and keep the owner' { $path = Join-Path -Path $folder -ChildPath 'AddAudit' Add-NTFSAudit -Path $path -Account $everyone -AccessRights ReadData -AuditFlags Failure -InheritanceFlags None -PropagationFlags None -ErrorVariable operationErrors -ErrorAction SilentlyContinue Format-LabError -ErrorRecord $operationErrors | Should -BeNullOrEmpty Get-LabOwner -Path $path | Should -Be $administrators } It 'Remove-NTFSAudit should remove the audit entry and keep the owner' { $path = Join-Path -Path $folder -ChildPath 'RemoveAudit' Remove-NTFSAudit -Path $path -Account $everyone -AccessRights Delete -AuditFlags Success -InheritanceFlags None -PropagationFlags None -ErrorVariable operationErrors -ErrorAction SilentlyContinue Format-LabError -ErrorRecord $operationErrors | Should -BeNullOrEmpty Get-LabOwner -Path $path | Should -Be $administrators } } } Context 'As the delegated account, an administrator of the client only' -Tag 'Delegate' { # The account has Full Control on the folders, so taking ownership succeeds, but it doesn't hold the Security # privilege on the file server, and it may not assign Administrators as the owner again. BeforeAll { $folder = Get-LabPath -RelativePath 'Case2\Delegate' } It 'Get-NTFSAudit should write a ReadSecurityError that names the missing privilege' { $entries = @(Get-NTFSAudit -Path (Join-Path -Path $folder -ChildPath 'GetAudit') -ErrorVariable operationErrors -ErrorAction SilentlyContinue) $entries | Should -BeNullOrEmpty @(Format-LabError -ErrorRecord $operationErrors) | Should -HaveCount 1 $operationErrors[0].FullyQualifiedErrorId | Should -BeLike 'ReadSecurityError,*' $operationErrors[0].Exception.Message | Should -Match 'privilege' } It 'Add-NTFSAudit should write an AddAceError that names the missing privilege and leave the folder unchanged' { $path = Join-Path -Path $folder -ChildPath 'AddAudit' $before = (Get-LabSecurityDescriptor -Path $path).GetSddlForm('All') Add-NTFSAudit -Path $path -Account $everyone -AccessRights ReadData -AuditFlags Failure -InheritanceFlags None -PropagationFlags None -ErrorVariable operationErrors -ErrorAction SilentlyContinue $written = (Format-LabError -ErrorRecord $operationErrors) -join ' | ' (Get-LabSecurityDescriptor -Path $path).GetSddlForm('All') | Should -Be $before -Because "the cmdlet wrote: $written" @(Format-LabError -ErrorRecord $operationErrors) | Should -HaveCount 1 -Because "the cmdlet wrote: $written" $operationErrors[0].FullyQualifiedErrorId | Should -BeLike 'AddAceError,*' $operationErrors[0].Exception.Message | Should -Match 'privilege' } It 'Remove-NTFSAudit should write a RemoveAceError that names the missing privilege and leave the folder unchanged' { $path = Join-Path -Path $folder -ChildPath 'RemoveAudit' $before = (Get-LabSecurityDescriptor -Path $path).GetSddlForm('All') Remove-NTFSAudit -Path $path -Account $everyone -AccessRights Delete -AuditFlags Success -InheritanceFlags None -PropagationFlags None -ErrorVariable operationErrors -ErrorAction SilentlyContinue $written = (Format-LabError -ErrorRecord $operationErrors) -join ' | ' (Get-LabSecurityDescriptor -Path $path).GetSddlForm('All') | Should -Be $before -Because "the cmdlet wrote: $written" @(Format-LabError -ErrorRecord $operationErrors) | Should -HaveCount 1 -Because "the cmdlet wrote: $written" $operationErrors[0].FullyQualifiedErrorId | Should -BeLike 'RemoveAceError,*' $operationErrors[0].Exception.Message | Should -Match 'privilege' } } } Describe 'Get-NTFSEffectiveAccess for a domain account on a share folder' -Tag 'Admin' -Skip:(-not $configured) { # The account gets ReadAndExecute through two nested domain groups and Write through a local group of the file # server. Only the file server knows its local groups. The expected rights come from the S4U tokens that the file # server and the client create for the account, which hold the same groups as the Effective Access tab there. BeforeAll { $path = Get-LabPath -RelativePath 'Case3\EffectiveAccess' $subject = $configuration.Accounts.Subject.Name } It 'Should return the rights through the domain groups and the local group of the file server with -ServerName, without a warning' { $result = @(Get-NTFSEffectiveAccess -Path $path -Account $subject -ServerName $configuration.FileServerFqdn -WarningVariable operationWarnings -WarningAction SilentlyContinue -ErrorVariable operationErrors -ErrorAction SilentlyContinue) Format-LabError -ErrorRecord $operationErrors | Should -BeNullOrEmpty $operationWarnings | Should -BeNullOrEmpty $result | Should -HaveCount 1 Format-LabRight -Right $result[0].AccessRights | Should -Be (Format-LabRight -Right $configuration.EffectiveAccess.FileServerRights) } It 'Should return only the rights through the domain groups without -ServerName' { $result = @(Get-NTFSEffectiveAccess -Path $path -Account $subject -WarningAction SilentlyContinue -ErrorVariable operationErrors -ErrorAction SilentlyContinue) Format-LabError -ErrorRecord $operationErrors | Should -BeNullOrEmpty $result | Should -HaveCount 1 Format-LabRight -Right $result[0].AccessRights | Should -Be (Format-LabRight -Right $configuration.EffectiveAccess.ClientRights) } # The cmdlet page: when the remote authorization manager can't be reached, the cmdlet falls back to the local one # and warns that the result may be inaccurate; since 5.0.0-rc7, the warning names the computer. It 'Should fall back to the authorization manager of the client and warn when -ServerName can''t be reached' { $result = @(Get-NTFSEffectiveAccess -Path $path -Account $subject -ServerName $configuration.UnreachableServerName -WarningVariable operationWarnings -WarningAction SilentlyContinue -ErrorVariable operationErrors -ErrorAction SilentlyContinue) Format-LabError -ErrorRecord $operationErrors | Should -BeNullOrEmpty $operationWarnings.Message | Should -Contain ('The effective rights can only be computed based on group membership on this computer, ' + "because the computer '$($configuration.UnreachableServerName)' can't be reached for a remote access check. " + 'For more accurate results, calculate effective access rights on that computer.') $result | Should -HaveCount 1 Format-LabRight -Right $result[0].AccessRights | Should -Be (Format-LabRight -Right $configuration.EffectiveAccess.ClientRights) } } Describe 'Get-NTFSEffectiveAccess as an account that is not an administrator of the file server' -Tag 'Delegate' -Skip:(-not $configured) { # The cmdlet page: the authorization manager of a computer answers only its administrators and the members of its # group Access Control Assistance Operators. The error must name the denial; no access instead of an error would be # a wrong result. It 'Should write a GetEffectiveAccessError that names the denial with -ServerName, and no result' { $path = Get-LabPath -RelativePath 'Case5\Delegate\GetOwner' $result = @(Get-NTFSEffectiveAccess -Path $path -Account $configuration.Accounts.Delegate.Sid -ServerName $configuration.FileServerFqdn -WarningVariable operationWarnings -WarningAction SilentlyContinue -ErrorVariable operationErrors -ErrorAction SilentlyContinue) $result | Should -BeNullOrEmpty $operationWarnings | Should -BeNullOrEmpty @(Format-LabError -ErrorRecord $operationErrors) | Should -HaveCount 1 $operationErrors[0].FullyQualifiedErrorId | Should -BeLike 'GetEffectiveAccessError,*' $operationErrors[0].Exception.InnerException.NativeErrorCode | Should -Be 5 } } Describe 'Get-NTFSOrphanedAccess with the entry of a deleted domain account on a share folder' -Tag 'Admin' -Skip:(-not $configured) { BeforeAll { $folder = Get-LabPath -RelativePath 'Case4\OrphanedAccess' $file = Join-Path -Path $folder -ChildPath 'File.txt' $orphan = $configuration.Accounts.Orphan.Sid } It 'Should return the entry of the deleted account with its SID' { $entries = @(Get-NTFSOrphanedAccess -Path $folder -WarningVariable operationWarnings -WarningAction SilentlyContinue -ErrorVariable operationErrors -ErrorAction SilentlyContinue) Format-LabError -ErrorRecord $operationErrors | Should -BeNullOrEmpty $operationWarnings | Should -BeNullOrEmpty $entries | Should -HaveCount 1 $entries[0].Account.Sid | Should -Be $orphan $entries[0].Account.AccountName | Should -BeNullOrEmpty $entries[0].IsInherited | Should -BeFalse } It 'Should return the inherited entry for a file in the folder, and nothing with -ExcludeInherited' { $entries = @(Get-NTFSOrphanedAccess -Path $file -ErrorVariable operationErrors -ErrorAction SilentlyContinue) $explicitEntries = @(Get-NTFSOrphanedAccess -Path $file -ExcludeInherited -ErrorVariable +operationErrors -ErrorAction SilentlyContinue) Format-LabError -ErrorRecord $operationErrors | Should -BeNullOrEmpty $entries | Should -HaveCount 1 $entries[0].Account.Sid | Should -Be $orphan $entries[0].IsInherited | Should -BeTrue $explicitEntries | Should -BeNullOrEmpty } } Describe 'Paths longer than 260 characters on a share' -Tag 'Admin' -Skip:(-not $configured) { BeforeAll { $folder = Get-LabPath -RelativePath 'LongPath' $file = Join-Path -Path $folder -ChildPath $configuration.LongPath } It 'Get-ChildItem2 should return the file at the end of the long path' { $files = @(Get-ChildItem2 -Path $folder -Recurse -File -ErrorVariable operationErrors -ErrorAction SilentlyContinue) Format-LabError -ErrorRecord $operationErrors | Should -BeNullOrEmpty $files | Should -HaveCount 1 $files[0].FullName.Length | Should -BeGreaterThan 260 } It 'Get-NTFSAccess should return the entries of that file' { $file.Length | Should -BeGreaterThan 260 $entries = @(Get-NTFSAccess -Path $file -ErrorVariable operationErrors -ErrorAction SilentlyContinue) Format-LabError -ErrorRecord $operationErrors | Should -BeNullOrEmpty $entries | Should -Not -BeNullOrEmpty } } Describe 'Copy-Item2 and Move-Item2 with -WhatIf onto an existing file on a share (#108)' -Tag 'Admin' -Skip:(-not $configured) { # Before 5.0.0-rc4, the cmdlets wrote an error with -WhatIf when the destination file existed. BeforeAll { $folder = Get-LabPath -RelativePath 'WhatIf' $source = Join-Path -Path $folder -ChildPath 'Source.txt' $destination = Join-Path -Path $folder -ChildPath 'Destination.txt' } It 'Copy-Item2 should write no error and leave the destination unchanged' { Copy-Item2 -Path $source -Destination $destination -WhatIf -ErrorVariable operationErrors -ErrorAction SilentlyContinue Format-LabError -ErrorRecord $operationErrors | Should -BeNullOrEmpty Get-Content -LiteralPath $destination -Raw | Should -Be 'Destination' } It 'Move-Item2 should write no error and leave both files unchanged' { Move-Item2 -Path $source -Destination $destination -WhatIf -ErrorVariable operationErrors -ErrorAction SilentlyContinue Format-LabError -ErrorRecord $operationErrors | Should -BeNullOrEmpty Get-Content -LiteralPath $source -Raw | Should -Be 'Source' Get-Content -LiteralPath $destination -Raw | Should -Be 'Destination' } } Describe 'Owner cmdlets on share folders' -Skip:(-not $configured) { # The file server decides: taking ownership needs the Take Ownership right, which Full Control includes, and # assigning another account needs the Restore privilege there. The folders start owned by Administrators. foreach ($ownerCase in $ownerCases) { Context 'As ' -Tag $ownerCase.OwnerRole -ForEach @($ownerCase) { BeforeAll { $folder = Get-LabPath -RelativePath "Case5\$OwnerRole" $accountSid = $configuration.Accounts.$OwnerRole.Sid } It 'Get-NTFSOwner should return Administrators' { $owners = @(Get-NTFSOwner -Path (Join-Path -Path $folder -ChildPath 'GetOwner') -ErrorVariable operationErrors -ErrorAction SilentlyContinue) Format-LabError -ErrorRecord $operationErrors | Should -BeNullOrEmpty $owners | Should -HaveCount 1 $owners[0].Owner.Sid | Should -Be $administrators } It 'Set-NTFSOwner should make the account itself the owner' { $path = Join-Path -Path $folder -ChildPath 'TakeOwnership' Set-NTFSOwner -Path $path -Account $accountSid -ErrorVariable operationErrors -ErrorAction SilentlyContinue Format-LabError -ErrorRecord $operationErrors | Should -BeNullOrEmpty Get-LabOwner -Path $path | Should -Be $accountSid } It 'Set-NTFSOwner should assign another account only with the Restore privilege of the file server' { $path = Join-Path -Path $folder -ChildPath 'AssignOwner' Set-NTFSOwner -Path $path -Account $configuration.Accounts.Subject.Sid -ErrorVariable operationErrors -ErrorAction SilentlyContinue $written = (Format-LabError -ErrorRecord $operationErrors) -join ' | ' if ($MayAssign) { $written | Should -BeNullOrEmpty Get-LabOwner -Path $path | Should -Be $configuration.Accounts.Subject.Sid } else { @(Format-LabError -ErrorRecord $operationErrors) | Should -HaveCount 1 -Because "the cmdlet wrote: $written" $operationErrors[0].FullyQualifiedErrorId | Should -BeLike 'SetOwnerError,*' Get-LabOwner -Path $path | Should -Be $administrators } } } } } Describe 'Audit inheritance cmdlets and Clear-NTFSAudit on share folders' -Skip:(-not $configured) { # The subfolders of Case6\ inherit one audit entry; the role Server checks the SACLs that the runs left. foreach ($auditCase in $auditSuccessCases) { Context 'As ' -Tag $auditCase.AuditRole -ForEach @($auditCase) { BeforeAll { $folder = Get-LabPath -RelativePath "Case6\$AuditRole" } It 'Disable-NTFSAuditInheritance should protect the audit entries and keep the owner' { $path = Join-Path -Path $folder -ChildPath 'DisableAuditInheritance' Disable-NTFSAuditInheritance -Path $path -ErrorVariable operationErrors -ErrorAction SilentlyContinue Format-LabError -ErrorRecord $operationErrors | Should -BeNullOrEmpty Get-LabOwner -Path $path | Should -Be $administrators (Get-NTFSInheritance -Path $path).AuditInheritanceEnabled | Should -BeFalse } It 'Enable-NTFSAuditInheritance should let the folder inherit the audit entries and keep the owner' { $path = Join-Path -Path $folder -ChildPath 'EnableAuditInheritance' Enable-NTFSAuditInheritance -Path $path -ErrorVariable operationErrors -ErrorAction SilentlyContinue Format-LabError -ErrorRecord $operationErrors | Should -BeNullOrEmpty Get-LabOwner -Path $path | Should -Be $administrators (Get-NTFSInheritance -Path $path).AuditInheritanceEnabled | Should -BeTrue } It 'Clear-NTFSAudit should remove the explicit audit entry, keep the inherited one, and keep the owner' { $path = Join-Path -Path $folder -ChildPath 'ClearAudit' Clear-NTFSAudit -Path $path -ErrorVariable operationErrors -ErrorAction SilentlyContinue Format-LabError -ErrorRecord $operationErrors | Should -BeNullOrEmpty Get-LabOwner -Path $path | Should -Be $administrators @(Get-NTFSAudit -Path $path -ExcludeInherited) | Should -BeNullOrEmpty @(Get-NTFSAudit -Path $path -ExcludeExplicit) | Should -HaveCount 1 } It 'Get-NTFSInheritance should report the protected DACL and the inherited audit entries' { $states = @(Get-NTFSInheritance -Path (Join-Path -Path $folder -ChildPath 'GetInheritance') -ErrorVariable operationErrors -ErrorAction SilentlyContinue) Format-LabError -ErrorRecord $operationErrors | Should -BeNullOrEmpty $states | Should -HaveCount 1 $states[0].AccessInheritanceEnabled | Should -BeFalse $states[0].AuditInheritanceEnabled | Should -BeTrue } } } Context 'As the delegated account, an administrator of the client only' -Tag 'Delegate' { BeforeAll { $folder = Get-LabPath -RelativePath 'Case6\Delegate' } It ' should write a that names the missing privilege and leave the folder unchanged' -ForEach @( @{ Command = 'Disable-NTFSAuditInheritance'; SubFolder = 'DisableAuditInheritance'; ErrorId = 'ModifySdError' } @{ Command = 'Enable-NTFSAuditInheritance'; SubFolder = 'EnableAuditInheritance'; ErrorId = 'ModifySdError' } @{ Command = 'Clear-NTFSAudit'; SubFolder = 'ClearAudit'; ErrorId = 'ClearAclError' } ) { $path = Join-Path -Path $folder -ChildPath $SubFolder $before = (Get-LabSecurityDescriptor -Path $path).GetSddlForm('All') & $Command -Path $path -ErrorVariable operationErrors -ErrorAction SilentlyContinue $written = (Format-LabError -ErrorRecord $operationErrors) -join ' | ' (Get-LabSecurityDescriptor -Path $path).GetSddlForm('All') | Should -Be $before -Because "the cmdlet wrote: $written" @(Format-LabError -ErrorRecord $operationErrors) | Should -HaveCount 1 -Because "the cmdlet wrote: $written" $operationErrors[0].FullyQualifiedErrorId | Should -BeLike "$ErrorId,*" $operationErrors[0].Exception.Message | Should -Match 'privilege' } # The cmdlet page: without the Security privilege, AuditInheritanceEnabled is $null and no error is written. It 'Get-NTFSInheritance should report the protected DACL, no audit state, and no error' { $states = @(Get-NTFSInheritance -Path (Join-Path -Path $folder -ChildPath 'GetInheritance') -ErrorVariable operationErrors -ErrorAction SilentlyContinue) Format-LabError -ErrorRecord $operationErrors | Should -BeNullOrEmpty $states | Should -HaveCount 1 $states[0].AccessInheritanceEnabled | Should -BeFalse $states[0].AuditInheritanceEnabled | Should -BeNullOrEmpty } } } Describe 'Item cmdlets on a share folder' -Tag 'Delegate' -Skip:(-not $configured) { # The delegated account fully controls the folder through its domain group. BeforeAll { $folder = Get-LabPath -RelativePath 'Case7\Items' $source = Join-Path -Path $folder -ChildPath 'Source.txt' } It 'Get-Item2 should return the file with its path on the share' { $item = @(Get-Item2 -Path $source -ErrorVariable operationErrors -ErrorAction SilentlyContinue) Format-LabError -ErrorRecord $operationErrors | Should -BeNullOrEmpty $item | Should -HaveCount 1 $item[0].FullName | Should -Be $source $item[0].Length | Should -Be 6 } It 'Test-Path2 should find the file and the folder, and not a missing item' { Test-Path2 -Path $source -PathType Leaf | Should -BeTrue Test-Path2 -Path (Join-Path -Path $folder -ChildPath 'Folder') -PathType Container | Should -BeTrue Test-Path2 -Path (Join-Path -Path $folder -ChildPath 'Missing.txt') | Should -BeFalse } It 'Get-FileHash2 should return the hash that Get-FileHash returns' { $hash = @(Get-FileHash2 -Path $source -Algorithm SHA256 -ErrorVariable operationErrors -ErrorAction SilentlyContinue) Format-LabError -ErrorRecord $operationErrors | Should -BeNullOrEmpty $hash | Should -HaveCount 1 $hash[0].Hash | Should -Be (Get-FileHash -LiteralPath $source -Algorithm SHA256).Hash } It 'Copy-Item2 should copy a file, and a folder with its file' { Copy-Item2 -Path $source -Destination (Join-Path -Path $folder -ChildPath 'Copy.txt') -ErrorVariable operationErrors -ErrorAction SilentlyContinue Copy-Item2 -Path (Join-Path -Path $folder -ChildPath 'Folder') -Destination (Join-Path -Path $folder -ChildPath 'FolderCopy') -ErrorVariable +operationErrors -ErrorAction SilentlyContinue Format-LabError -ErrorRecord $operationErrors | Should -BeNullOrEmpty Get-Content -LiteralPath (Join-Path -Path $folder -ChildPath 'Copy.txt') -Raw | Should -Be 'Source' Get-Content -LiteralPath (Join-Path -Path $folder -ChildPath 'FolderCopy\File.txt') -Raw | Should -Be 'File' Get-Content -LiteralPath $source -Raw | Should -Be 'Source' } It 'Move-Item2 should move a file' { $moving = Join-Path -Path $folder -ChildPath 'Move.txt' Move-Item2 -Path $moving -Destination (Join-Path -Path $folder -ChildPath 'Moved.txt') -ErrorVariable operationErrors -ErrorAction SilentlyContinue Format-LabError -ErrorRecord $operationErrors | Should -BeNullOrEmpty Test-Path -LiteralPath $moving | Should -BeFalse Get-Content -LiteralPath (Join-Path -Path $folder -ChildPath 'Moved.txt') -Raw | Should -Be 'Move' } It 'Remove-Item2 should remove a file' { $removing = Join-Path -Path $folder -ChildPath 'Remove.txt' Remove-Item2 -Path $removing -ErrorVariable operationErrors -ErrorAction SilentlyContinue Format-LabError -ErrorRecord $operationErrors | Should -BeNullOrEmpty Test-Path -LiteralPath $removing | Should -BeFalse } It 'Get-ChildItem2 -Hidden should include the first hidden file without explicit -Force over SMB' { $hiddenFolder = Get-LabPath -RelativePath 'Case7\Hidden' $hiddenFile = Join-Path -Path $hiddenFolder -ChildPath 'Only.txt' $result = @(Get-ChildItem2 -Path $hiddenFolder -Hidden -ErrorVariable operationErrors -ErrorAction SilentlyContinue) Format-LabError -ErrorRecord $operationErrors | Should -BeNullOrEmpty $result | Should -HaveCount 1 $result[0].FullName | Should -Be $hiddenFile [System.IO.File]::GetAttributes($hiddenFile).HasFlag([System.IO.FileAttributes]::Hidden) | Should -BeTrue } It 'Get-ChildItem2 should list the file and the folder that the tests leave in place' { $names = @(Get-ChildItem2 -Path $folder -ErrorVariable operationErrors -ErrorAction SilentlyContinue).Name Format-LabError -ErrorRecord $operationErrors | Should -BeNullOrEmpty $names | Should -Contain 'Source.txt' $names | Should -Contain 'Folder' } } Describe 'Link cmdlets on a share folder' -Tag 'Admin' -Skip:(-not $configured) { BeforeAll { $folder = Get-LabPath -RelativePath 'Case8\Links' $target = Join-Path -Path $folder -ChildPath 'Target.txt' $hardLink = Join-Path -Path $folder -ChildPath 'HardLink.txt' } It 'New-NTFSHardLink should give the file a second name' { New-NTFSHardLink -Path $hardLink -Target $target -ErrorVariable operationErrors -ErrorAction SilentlyContinue Format-LabError -ErrorRecord $operationErrors | Should -BeNullOrEmpty Get-Content -LiteralPath $hardLink -Raw | Should -Be 'Target' } # The cmdlet pages: Windows can't list the names of a file on a share, so the cmdlets write a GetHardLinkError. It 'Get-NTFSHardLink should write a GetHardLinkError, because Windows cannot list the names on a share' { $links = @(Get-NTFSHardLink -Path $target -ErrorVariable operationErrors -ErrorAction SilentlyContinue) $links | Should -BeNullOrEmpty @(Format-LabError -ErrorRecord $operationErrors) | Should -HaveCount 1 $operationErrors[0].FullyQualifiedErrorId | Should -BeLike 'GetHardLinkError,*' } It 'New-NTFSHardLink -PassThru should create the link and write a GetHardLinkError instead of the names' { $passThruLink = Join-Path -Path $folder -ChildPath 'PassThruLink.txt' $result = @(New-NTFSHardLink -Path $passThruLink -Target $target -PassThru -ErrorVariable operationErrors -ErrorAction SilentlyContinue) $result | Should -BeNullOrEmpty @(Format-LabError -ErrorRecord $operationErrors) | Should -HaveCount 1 $operationErrors[0].FullyQualifiedErrorId | Should -BeLike 'GetHardLinkError,*' Get-Content -LiteralPath $passThruLink -Raw | Should -Be 'Target' } It 'New-NTFSSymbolicLink should create a link to a file and a link to a folder' { New-NTFSSymbolicLink -Path (Join-Path -Path $folder -ChildPath 'FileLink.txt') -Target $target -ErrorVariable operationErrors -ErrorAction SilentlyContinue New-NTFSSymbolicLink -Path (Join-Path -Path $folder -ChildPath 'FolderLink') -Target (Join-Path -Path $folder -ChildPath 'TargetFolder') -ErrorVariable +operationErrors -ErrorAction SilentlyContinue Format-LabError -ErrorRecord $operationErrors | Should -BeNullOrEmpty } } Describe 'Get-NTFSSimpleAccess on share folders' -Tag 'Delegate' -Skip:(-not $configured) { It 'Should report the parent folder first and for the subfolder only the entry of its own' { $parent = Get-LabPath -RelativePath 'Case9\Simple' $child = Join-Path -Path $parent -ChildPath 'Child' $entries = @(Get-NTFSSimpleAccess -Path $child -ErrorVariable operationErrors -ErrorAction SilentlyContinue) Format-LabError -ErrorRecord $operationErrors | Should -BeNullOrEmpty $entries | Should -Not -BeNullOrEmpty $entries[0].FullName | Should -Be $parent @($entries | Where-Object -Property FullName -EQ -Value $child).Identity.Sid | Should -Be $configuration.Accounts.Subject.Sid } } Describe 'Get-NTFSOrphanedAudit with the audit entry of a deleted domain account on a share folder' -Tag 'Admin' -Skip:(-not $configured) { It 'Should return the audit entry of the deleted account with its SID' { $entries = @(Get-NTFSOrphanedAudit -Path (Get-LabPath -RelativePath 'Case4\OrphanedAudit') -WarningVariable operationWarnings -WarningAction SilentlyContinue -ErrorVariable operationErrors -ErrorAction SilentlyContinue) Format-LabError -ErrorRecord $operationErrors | Should -BeNullOrEmpty $operationWarnings | Should -BeNullOrEmpty $entries | Should -HaveCount 1 $entries[0].Account.Sid | Should -Be $configuration.Accounts.Orphan.Sid } } Describe 'Accounts of another domain and of other forests on share folders' -Tag 'Admin' -Skip:(-not $configured -or $foreignAccounts.Count -eq 0) { # Through the trusts, the client resolves the names of the accounts, and the file server creates their tokens. BeforeAll { $folder = Get-LabPath -RelativePath 'Case9\Foreign' } It 'Get-NTFSAccess should return the entry of with its name' -ForEach $foreignAccounts { $entries = @(Get-NTFSAccess -Path $folder -ExcludeInherited -ErrorVariable operationErrors -ErrorAction SilentlyContinue | Where-Object -FilterScript { $_.Account.Sid -eq $Sid }) Format-LabError -ErrorRecord $operationErrors | Should -BeNullOrEmpty $entries | Should -HaveCount 1 $entries[0].Account.AccountName | Should -Be $Name } It 'Get-NTFSOrphanedAccess should not report the entries of the accounts' { $entries = @(Get-NTFSOrphanedAccess -Path $folder -ErrorVariable operationErrors -ErrorAction SilentlyContinue) Format-LabError -ErrorRecord $operationErrors | Should -BeNullOrEmpty $entries | Should -BeNullOrEmpty } It 'Add-NTFSAccess should add an entry for by its name' -ForEach $foreignAccounts { $path = Get-LabPath -RelativePath 'Case9\ForeignAdd' Add-NTFSAccess -Path $path -Account $Name -AccessRights ReadAndExecute -ErrorVariable operationErrors -ErrorAction SilentlyContinue Format-LabError -ErrorRecord $operationErrors | Should -BeNullOrEmpty @(Get-LabExplicitAccessRule -Path $path -Sid $Sid) | Should -HaveCount 1 } It 'Remove-NTFSAccess should remove the entry of by its name' -ForEach $foreignAccounts { $path = Get-LabPath -RelativePath 'Case9\ForeignRemove' Remove-NTFSAccess -Path $path -Account $Name -AccessRights ReadAndExecute -InheritanceFlags 'ContainerInherit, ObjectInherit' -PropagationFlags None -ErrorVariable operationErrors -ErrorAction SilentlyContinue Format-LabError -ErrorRecord $operationErrors | Should -BeNullOrEmpty Get-LabExplicitAccessRule -Path $path -Sid $Sid | Should -BeNullOrEmpty } It 'Get-NTFSEffectiveAccess should return the rights of on the file server with -ServerName, without a warning' -ForEach $foreignAccounts { $EffectiveRights | Should -BeGreaterThan 0 -Because 'the file server must create a token for the account to calculate the expected rights' $result = @(Get-NTFSEffectiveAccess -Path $folder -Account $Name -ServerName $configuration.FileServerFqdn -WarningVariable operationWarnings -WarningAction SilentlyContinue -ErrorVariable operationErrors -ErrorAction SilentlyContinue) Format-LabError -ErrorRecord $operationErrors | Should -BeNullOrEmpty $operationWarnings | Should -BeNullOrEmpty $result | Should -HaveCount 1 Format-LabRight -Right $result[0].AccessRights | Should -Be (Format-LabRight -Right $EffectiveRights) } } # Case 10: the behavior that the fixes of the quality gate before 5.0.0 changed. Each test works in a folder of its role below # Case10, which the delegated group fully controls, and fails on a build before the fix that its comment names. Describe 'An item that the account owns and whose owner may not change its permissions on a share' -Tag 'Delegate' -Skip:(-not $configured) { # The delegated account owns what it creates on the share. A deny entry for OWNER RIGHTS replaces the right of the owner to # change the DACL, so the cmdlets take ownership for the write, which the file server answers by removing that entry. Once # the DACL is cleared and protected, nobody holds the right to set an owner. Before 5.0.0, the cmdlets set the previous # owner back also when it was the account itself: the file server refused it, and they reported a RestoreOwnerError for an # owner that had not changed. BeforeAll { $folder = Get-LabPath -RelativePath "Case10\$Role\Owner" $null = New-Item -ItemType Directory -Path $folder -Force $ownerRights = 'S-1-3-4' $protectedFlag = [System.Security.AccessControl.ControlFlags]::DiscretionaryAclProtected function New-LabUnchangeableFile { [Diagnostics.CodeAnalysis.SuppressMessageAttribute( 'PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Test helper that only writes to the folder of the run.' )] param ([string] $Name) $file = Join-Path -Path $folder -ChildPath $Name Set-Content -LiteralPath $file -Value $Name -NoNewline Get-LabOwner -Path $file | Should -Be $configuration.Accounts.$Role.Sid -Because 'the account owns what it creates' Add-NTFSAccess -Path $file -Account $ownerRights -AccessType Deny -AccessRights ChangePermissions -ErrorAction Stop # icacls reports the refusal on its error stream, which a terminating error action would turn into an exception $savedPreference = $ErrorActionPreference $ErrorActionPreference = 'Continue' try { $null = & icacls.exe $file /grant '*S-1-1-0:(R)' 2>&1 $exitCode = $LASTEXITCODE } finally { $ErrorActionPreference = $savedPreference } $exitCode | Should -Not -Be 0 -Because 'a plain write of the DACL fails for the owner now' $file } function Assert-LabClearedDescriptor { param ([string] $File) $descriptor = Get-LabSecurityDescriptor -Path $File $descriptor.Owner.Value | Should -Be $configuration.Accounts.$Role.Sid ($descriptor.ControlFlags -band $protectedFlag) | Should -Be $protectedFlag $null -ne $descriptor.DiscretionaryAcl | Should -BeTrue -Because 'the DACL is empty, not NULL' $descriptor.DiscretionaryAcl.Count | Should -Be 0 } } It 'Clear-NTFSAccess -DisableInheritance should take ownership, clear and protect the DACL, and report no RestoreOwnerError' { $file = New-LabUnchangeableFile -Name 'Clear.txt' Clear-NTFSAccess -Path $file -DisableInheritance -ErrorVariable operationErrors -ErrorAction SilentlyContinue Format-LabError -ErrorRecord $operationErrors | Should -BeNullOrEmpty Assert-LabClearedDescriptor -File $file } It 'Set-NTFSSecurityDescriptor should write the cleared DACL and report no RestoreOwnerError' { $file = New-LabUnchangeableFile -Name 'Descriptor.txt' $descriptor = Get-NTFSSecurityDescriptor -Path $file -ErrorAction Stop Clear-NTFSAccess -SecurityDescriptor $descriptor -DisableInheritance -ErrorAction Stop Set-NTFSSecurityDescriptor -SecurityDescriptor $descriptor -ErrorVariable operationErrors -ErrorAction SilentlyContinue Format-LabError -ErrorRecord $operationErrors | Should -BeNullOrEmpty Assert-LabClearedDescriptor -File $file } } # Windows can't name the folders that the inherited entries of an item come from when the item is gone, for example deleted by # another process after its security descriptor was read, or when a folder above it can't be read. The entries still come # back. Before 5.0.0, the text lost its last character, and an explicit entry, which has no source, got it as well. Describe 'InheritedFrom of access entries that Windows cannot resolve on a share' -Tag 'Delegate', 'ServerAdmin', 'Admin' -Skip:(-not $configured) { BeforeAll { $folder = Get-LabPath -RelativePath "Case10\$Role\InheritedFrom" $null = New-Item -ItemType Directory -Path $folder -Force } It 'Should name an unknown parent for an inherited entry and no source for an explicit entry when the file is gone' { $file = Join-Path -Path $folder -ChildPath 'Gone.txt' Set-Content -LiteralPath $file -Value 'Gone' -NoNewline Add-NTFSAccess -Path $file -Account $everyone -AccessRights ReadData -ErrorAction Stop $descriptor = Get-NTFSSecurityDescriptor -Path $file -ErrorAction Stop Remove-Item -LiteralPath $file -Force $entries = @([Security2.FileSystemAccessRule2]::GetFileSystemAccessRules($descriptor, $true, $true, $true)) $inherited = @($entries | Where-Object -FilterScript { $_.IsInherited }) $inherited | Should -Not -BeNullOrEmpty foreach ($entry in $inherited) { $entry.InheritedFrom | Should -BeExactly 'unknown parent' } $explicit = @($entries | Where-Object -FilterScript { -not $_.IsInherited }) $explicit | Should -HaveCount 1 $explicit[0].InheritedFrom | Should -BeNullOrEmpty } } Describe 'InheritedFrom of audit entries that Windows cannot resolve on a share' -Tag 'ServerAdmin', 'Admin' -Skip:(-not $configured) { # The administrators of the file server read and change the audit entries over SMB (case 2). BeforeAll { $folder = Get-LabPath -RelativePath "Case10\$Role\InheritedFromAudit" $null = New-Item -ItemType Directory -Path $folder -Force Add-NTFSAudit -Path $folder -Account $everyone -AccessRights ReadData -InheritanceFlags 'ContainerInherit, ObjectInherit' -PropagationFlags None -ErrorAction Stop } It 'Should name an unknown parent for an inherited audit entry and no source for an explicit entry when the file is gone' { $file = Join-Path -Path $folder -ChildPath 'Gone.txt' Set-Content -LiteralPath $file -Value 'Gone' -NoNewline Add-NTFSAudit -Path $file -Account 'S-1-5-32-546' -AccessRights Delete -InheritanceFlags None -PropagationFlags None -ErrorAction Stop $descriptor = Get-NTFSSecurityDescriptor -Path $file -ErrorAction Stop Remove-Item -LiteralPath $file -Force $entries = @([Security2.FileSystemAuditRule2]::GetFileSystemAuditRules($descriptor, $true, $true, $true)) $inherited = @($entries | Where-Object -FilterScript { $_.IsInherited }) $inherited | Should -HaveCount 1 $inherited[0].InheritedFrom | Should -BeExactly 'unknown parent' $explicit = @($entries | Where-Object -FilterScript { -not $_.IsInherited }) $explicit | Should -HaveCount 1 $explicit[0].InheritedFrom | Should -BeNullOrEmpty } } Describe 'InheritedFrom of an item below a folder on a share whose permissions the account cannot read' -Tag 'Delegate' -Skip:(-not $configured) { # Administrators own the folder, so a deny entry for the delegated account takes effect for it. The entry applies to the # folder only: the item below it keeps its entries and stays readable. BeforeAll { $locked = Get-LabPath -RelativePath 'Case10\Locked' $child = Join-Path -Path $locked -ChildPath 'Child.txt' Set-Content -LiteralPath $child -Value 'Child' -NoNewline Add-NTFSAccess -Path $child -Account $everyone -AccessRights ReadData -ErrorAction Stop Add-NTFSAccess -Path $locked -Account $configuration.Accounts.Delegate.Sid -AccessType Deny -AccessRights ReadPermissions -AppliesTo ThisFolderOnly -ErrorAction Stop } It 'Should start with a folder whose permissions the account cannot read, and an item that it can' { { Get-Acl -LiteralPath $locked -ErrorAction Stop } | Should -Throw { Get-Acl -LiteralPath $child -ErrorAction Stop } | Should -Not -Throw } It 'Get-NTFSAccess should name an unknown parent for an inherited entry and no source for an explicit entry' { $entries = @(Get-NTFSAccess -Path $child -ErrorVariable operationErrors -ErrorAction SilentlyContinue) Format-LabError -ErrorRecord $operationErrors | Should -BeNullOrEmpty $inherited = @($entries | Where-Object -FilterScript { $_.IsInherited }) $inherited | Should -Not -BeNullOrEmpty foreach ($entry in $inherited) { $entry.InheritedFrom | Should -BeExactly 'unknown parent' } $explicit = @($entries | Where-Object -FilterScript { -not $_.IsInherited -and $_.Account.Sid -eq $everyone }) $explicit | Should -HaveCount 1 $explicit[0].InheritedFrom | Should -BeNullOrEmpty } } # Before 5.0.0, a cmdlet took what a later command ended the pipeline with (Select-Object -First, a break) or threw for a failure # of the item and went on with the next item: Remove-Item2 removed every item after Select-Object -First 1, and the caller # never saw a throw. Each case runs one command over two items and the file server checks the items afterwards (role Server). Describe 'A later command that ends the pipeline or throws, for the item cmdlets on a share' -Tag 'Delegate', 'ServerAdmin', 'Admin' -Skip:(-not $configured) { BeforeAll { $account = $configuration.Accounts.$Role.Sid $caseRoot = Get-LabPath -RelativePath "Case10\$Role\LaterCommand" $privateData = (Get-Module -Name NTFSSecurity).PrivateData $savedEnablePrivileges = $privateData['EnablePrivileges'] function Get-LabSlug { param ([string] $Style) if ($Style -eq 'throw') { 'Throw' } else { 'Select' } } function New-LabCaseFolder { [Diagnostics.CodeAnalysis.SuppressMessageAttribute( 'PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Test helper that only writes to the folder of the run.' )] param ([string] $Name) $path = Join-Path -Path $caseRoot -ChildPath $Name $null = New-Item -ItemType Directory -Path $path -Force $path } function New-LabPair { [Diagnostics.CodeAnalysis.SuppressMessageAttribute( 'PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Test helper that only writes to the folder of the run.' )] param ([string] $Name) $directory = New-LabCaseFolder -Name $Name foreach ($item in 'First', 'Second') { Set-Content -LiteralPath (Join-Path -Path $directory -ChildPath "$item.txt") -Value $item -NoNewline } @{ Directory = $directory First = (Join-Path -Path $directory -ChildPath 'First.txt') Second = (Join-Path -Path $directory -ChildPath 'Second.txt') } } # Each case runs one command over the two items of its context. Untouched tells whether the second item is as it was, # which it is only when the command stopped after the first one. $cases = @{ 'Remove-Item2' = @{ Prepare = { param ($Slug) New-LabPair -Name "RemoveItem2-$Slug" } Run = { param ($Context) Remove-Item2 -Path $Context.First, $Context.Second -PassThru -ErrorAction SilentlyContinue } Untouched = { param ($Context) Test-Path -LiteralPath $Context.Second } } 'Copy-Item2' = @{ Prepare = { param ($Slug) $context = New-LabPair -Name "CopyItem2-$Slug" $context.Destination = New-LabCaseFolder -Name "CopyItem2-$Slug-To" $context } Run = { param ($Context) Copy-Item2 -Path $Context.First, $Context.Second -Destination $Context.Destination -PassThru $true -ErrorAction SilentlyContinue } Untouched = { param ($Context) -not (Test-Path -LiteralPath (Join-Path -Path $Context.Destination -ChildPath 'Second.txt')) } } 'Move-Item2' = @{ Prepare = { param ($Slug) $context = New-LabPair -Name "MoveItem2-$Slug" $context.Destination = New-LabCaseFolder -Name "MoveItem2-$Slug-To" $context } Run = { param ($Context) Move-Item2 -Path $Context.First, $Context.Second -Destination $Context.Destination -PassThru $true -ErrorAction SilentlyContinue } Untouched = { param ($Context) Test-Path -LiteralPath $Context.Second } } # The files of the fixture are owned by Administrators, so that the first one changes its owner. 'Set-NTFSOwner' = @{ Prepare = { param ($Slug) $directory = Join-Path -Path $caseRoot -ChildPath "SetOwner-$Slug" @{ First = (Join-Path -Path $directory -ChildPath 'First.txt'); Second = (Join-Path -Path $directory -ChildPath 'Second.txt') } } Run = { param ($Context) Set-NTFSOwner -Path $Context.First, $Context.Second -Account $account -PassThru -ErrorAction SilentlyContinue } Untouched = { param ($Context) (Get-LabOwner -Path $Context.Second) -eq $administrators } } 'Set-NTFSSecurityDescriptor' = @{ Prepare = { param ($Slug) $context = New-LabPair -Name "SetDescriptor-$Slug" $context.Descriptors = @(Get-NTFSSecurityDescriptor -Path $context.First, $context.Second -ErrorAction Stop) Add-NTFSAccess -SecurityDescriptor $context.Descriptors -Account $everyone -AccessRights ReadData -ErrorAction Stop $context } Run = { param ($Context) Set-NTFSSecurityDescriptor -SecurityDescriptor $Context.Descriptors -PassThru -ErrorAction SilentlyContinue } Untouched = { param ($Context) -not (Get-LabExplicitAccessRule -Path $Context.Second -Sid $everyone) } } } # The command writes a verbose or a debug message inside the try of its loop, which the later command takes. With the # privileges enabled, the cmdlet writes a message before that, outside the try, so the module setting is off for these # cases. Get-FileHash2 skips the folder that comes first with a verbose message. $streamCases = @{ 'Set-NTFSSecurityDescriptor/verbose' = @{ Prepare = $cases['Set-NTFSSecurityDescriptor'].Prepare Run = { param ($Context) Set-NTFSSecurityDescriptor -SecurityDescriptor $Context.Descriptors -Verbose -ErrorAction SilentlyContinue 4>&1 } Untouched = $cases['Set-NTFSSecurityDescriptor'].Untouched RecordType = [System.Management.Automation.VerboseRecord] } 'Get-FileHash2/verbose' = @{ Prepare = { param ($Slug) @{ First = (New-LabCaseFolder -Name "FileHash2-$Slug-Folder") File = (New-LabPair -Name "FileHash2-$Slug").First } } Run = { param ($Context) Get-FileHash2 -Path $Context.First, $Context.File -Verbose -ErrorAction SilentlyContinue 4>&1 } RecordType = [System.Management.Automation.VerboseRecord] } 'Set-NTFSOwner/debug' = @{ Prepare = $cases['Set-NTFSOwner'].Prepare Run = { param ($Context) Set-NTFSOwner -Path $Context.First, $Context.Second -Account $account -ErrorAction SilentlyContinue 5>&1 } Untouched = $cases['Set-NTFSOwner'].Untouched RecordType = [System.Management.Automation.DebugRecord] } } function Assert-LabPipelineStop { param ([hashtable] $Case, [string] $Slug, [string] $Stream) if ($Stream -eq 'debug') { $DebugPreference = 'Continue' } $context = & $Case.Prepare $Slug $Error.Clear() $result = @(& $Case.Run $context | Select-Object -First 1) $result | Should -HaveCount 1 if ($Case.RecordType) { $result[0] | Should -BeOfType $Case.RecordType } $Error.Count | Should -Be 0 if ($Case.Untouched) { (& $Case.Untouched $context) | Should -BeTrue } } # A later command that throws ends the pipeline for the commands before it. The error is the caller's: the cmdlet must # neither report it as an error of an item nor go on with the next item. function Assert-LabDownstreamFailure { param ([hashtable] $Case, [string] $Slug, [string] $Stream) if ($Stream -eq 'debug') { $DebugPreference = 'Continue' } $context = & $Case.Prepare $Slug $emitted = 0 $caught = $null $Error.Clear() try { & $Case.Run $context | ForEach-Object -Process { $emitted++ throw 'Downstream failure' } } catch { $caught = $_ } $caught.Exception.Message | Should -BeLike '*Downstream failure*' $emitted | Should -Be 1 @($Error | Where-Object -FilterScript { $_.Exception.Message -notlike '*Downstream failure*' }) | Should -BeNullOrEmpty if ($Case.Untouched) { (& $Case.Untouched $context) | Should -BeTrue } } } It ' should stop after the first object for