mirror of https://github.com/raandree/NTFSSecurity
Browse Source
Release 5.0.0-rc5: live tests in a lab and the Get-NTFSEffectiveAccess -ServerName fixpull/121/head 5.0.0-rc5
committed by
GitHub
19 changed files with 2089 additions and 57 deletions
@ -0,0 +1,27 @@ |
|||
--- |
|||
status: accepted |
|||
date: 2026-10-07 |
|||
last-verified: 2026-10-07 |
|||
owner: shared |
|||
source: maintainer decision of 2026-10-07 |
|||
--- |
|||
|
|||
# Decision 20: Live tests in a lab live in Tests\Lab |
|||
|
|||
- Choice: The live tests that need a file server and domain accounts live |
|||
in `Tests\Lab`: the Pester file `NTFSSecurity.Live.Tests.ps1`, the |
|||
controller `Invoke-NTFSSecurityLabTest.ps1`, which prepares an |
|||
AutomatedLab lab and runs the tests per module version and PowerShell |
|||
edition, and a README with the cases. CI excludes the folder through |
|||
`Run.ExcludePath` in `.github/scripts/Invoke-Tests.ps1`, and without a |
|||
configuration every live test skips. The default lab is the one of |
|||
WindowsAccessControl (`F1ADC1`, `F1AFile2`, `F1AFile1` in |
|||
`a.forest1.net`), which the maintainer allowed on 2026-10-07. |
|||
- Rationale: The tests in `Tests` run on one computer with local accounts. |
|||
#34 over SMB, the Security privilege that the file server checks, |
|||
`Get-NTFSEffectiveAccess -ServerName`, and orphaned domain accounts need a |
|||
file server and a domain. In the repository, a later release can repeat |
|||
the tests and review them in a pull request. |
|||
- Rejected: a handoff folder outside the repository, which keeps the tests |
|||
on one machine. |
|||
- Applied: 5.0.0-rc5. |
|||
File diff suppressed because it is too large
@ -0,0 +1,238 @@ |
|||
<# |
|||
Helpers of the live tests in a lab (README.md). NTFSSecurity.Live.Tests.ps1 dot-sources this file on the client |
|||
and on the file server, and Invoke-NTFSSecurityLabTest.ps1 runs it on both to prepare the fixtures. Dot-sourcing |
|||
it only defines the helpers. |
|||
#> |
|||
|
|||
if (-not ('NTFSSecurityLab.NativeMethods' -as [type])) { |
|||
Add-Type -TypeDefinition @' |
|||
using System; |
|||
using System.ComponentModel; |
|||
using System.Runtime.InteropServices; |
|||
|
|||
namespace NTFSSecurityLab |
|||
{ |
|||
// GetFileSecurity and SetFileSecurity read and write a security descriptor as Windows stores it. |
|||
// GetNamedSecurityInfo and SetNamedSecurityInfo, which .NET and the module use, convert a DACL without the |
|||
// auto-inherit flag when they read it, and add the flag when they write a DACL. |
|||
public static class NativeMethods |
|||
{ |
|||
[DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)] |
|||
private static extern bool GetFileSecurityW(string fileName, int requestedInformation, byte[] securityDescriptor, int length, out int lengthNeeded); |
|||
|
|||
[DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)] |
|||
private static extern bool SetFileSecurityW(string fileName, int securityInformation, byte[] securityDescriptor); |
|||
|
|||
public static byte[] GetFileSecurity(string path, int information) |
|||
{ |
|||
int needed; |
|||
GetFileSecurityW(path, information, null, 0, out needed); |
|||
if (needed == 0) |
|||
{ |
|||
throw new Win32Exception(Marshal.GetLastWin32Error()); |
|||
} |
|||
|
|||
var buffer = new byte[needed]; |
|||
if (!GetFileSecurityW(path, information, buffer, buffer.Length, out needed)) |
|||
{ |
|||
throw new Win32Exception(Marshal.GetLastWin32Error()); |
|||
} |
|||
|
|||
return buffer; |
|||
} |
|||
|
|||
public static void SetFileSecurity(string path, int information, byte[] securityDescriptor) |
|||
{ |
|||
if (!SetFileSecurityW(path, information, securityDescriptor)) |
|||
{ |
|||
throw new Win32Exception(Marshal.GetLastWin32Error()); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
'@ |
|||
} |
|||
|
|||
function Get-LabSecurityDescriptor { |
|||
<# |
|||
.SYNOPSIS |
|||
Returns the owner, the group, and the DACL of a file or folder as Windows stores them. |
|||
.DESCRIPTION |
|||
GetNamedSecurityInfo returns the owner with a DACL without the auto-inherit flag even when only the DACL is |
|||
read, and converts such a DACL. This function reads with GetFileSecurity, which does neither. |
|||
.PARAMETER Path |
|||
A local path or a UNC path. The account needs the right to read the permissions. |
|||
#> |
|||
[CmdletBinding()] |
|||
[OutputType([System.Security.AccessControl.RawSecurityDescriptor])] |
|||
param ( |
|||
[Parameter(Mandatory)] |
|||
[string] |
|||
$Path |
|||
) |
|||
|
|||
# OWNER_SECURITY_INFORMATION, GROUP_SECURITY_INFORMATION, and DACL_SECURITY_INFORMATION |
|||
$bytes = [NTFSSecurityLab.NativeMethods]::GetFileSecurity($Path, 7) |
|||
New-Object -TypeName 'System.Security.AccessControl.RawSecurityDescriptor' -ArgumentList $bytes, 0 |
|||
} |
|||
|
|||
function Test-LabDaclAutoInherited { |
|||
<# |
|||
.SYNOPSIS |
|||
Returns whether the stored DACL of a file or folder has the auto-inherit flag. |
|||
.PARAMETER Path |
|||
A local path or a UNC path. |
|||
#> |
|||
[CmdletBinding()] |
|||
[OutputType([bool])] |
|||
param ( |
|||
[Parameter(Mandatory)] |
|||
[string] |
|||
$Path |
|||
) |
|||
|
|||
$autoInherited = [System.Security.AccessControl.ControlFlags]::DiscretionaryAclAutoInherited |
|||
((Get-LabSecurityDescriptor -Path $Path).ControlFlags -band $autoInherited) -ne 0 |
|||
} |
|||
|
|||
function Set-LabLegacyDacl { |
|||
<# |
|||
.SYNOPSIS |
|||
Stores the DACL of a file or folder again, without the auto-inherit flag. |
|||
.DESCRIPTION |
|||
Windows adds the flag whenever SetNamedSecurityInfo writes a DACL. SetFileSecurity stores the DACL as given, |
|||
like tools that predate Windows 2000. For such a DACL, GetNamedSecurityInfo returns the owner also when only |
|||
the DACL is read, and NTFSSecurity before 5.0.0-rc3 wrote that owner back (#34). |
|||
.PARAMETER Path |
|||
A local path. The account needs the right to change the permissions. |
|||
#> |
|||
[CmdletBinding(SupportsShouldProcess)] |
|||
param ( |
|||
[Parameter(Mandatory)] |
|||
[string] |
|||
$Path |
|||
) |
|||
|
|||
$descriptor = Get-LabSecurityDescriptor -Path $Path |
|||
$autoInherited = [System.Security.AccessControl.ControlFlags]::DiscretionaryAclAutoInherited |
|||
$descriptor.SetFlags([System.Security.AccessControl.ControlFlags]($descriptor.ControlFlags -band -bnot $autoInherited)) |
|||
$bytes = New-Object -TypeName 'byte[]' -ArgumentList $descriptor.BinaryLength |
|||
$descriptor.GetBinaryForm($bytes, 0) |
|||
if ($PSCmdlet.ShouldProcess($Path, 'Store the DACL without the auto-inherit flag')) { |
|||
# DACL_SECURITY_INFORMATION |
|||
[NTFSSecurityLab.NativeMethods]::SetFileSecurity($Path, 4, $bytes) |
|||
} |
|||
} |
|||
|
|||
function Get-LabTokenSid { |
|||
<# |
|||
.SYNOPSIS |
|||
Returns the SIDs of the token that this computer creates for a domain account. |
|||
.DESCRIPTION |
|||
Logs the account on with Kerberos S4U, without its password, like the Effective Access tab of the advanced |
|||
security settings does. The token holds the account and all its groups that this computer knows: nested |
|||
domain groups and the local groups of this computer. |
|||
.PARAMETER UserPrincipalName |
|||
The user principal name of the account, such as user@contoso.com. |
|||
#> |
|||
[CmdletBinding()] |
|||
[OutputType([string])] |
|||
param ( |
|||
[Parameter(Mandatory)] |
|||
[string] |
|||
$UserPrincipalName |
|||
) |
|||
|
|||
$identity = New-Object -TypeName 'System.Security.Principal.WindowsIdentity' -ArgumentList $UserPrincipalName |
|||
try { |
|||
$identity.User.Value |
|||
foreach ($group in $identity.Groups) { |
|||
$group.Value |
|||
} |
|||
} |
|||
finally { |
|||
$identity.Dispose() |
|||
} |
|||
} |
|||
|
|||
function Get-LabGrantedRight { |
|||
<# |
|||
.SYNOPSIS |
|||
Returns the access mask that the allow entries of a DACL grant to a set of SIDs. |
|||
.DESCRIPTION |
|||
Combines the entries that apply to the item itself and name one of the SIDs. A deny entry for one of the SIDs |
|||
makes the function throw, because the calculation doesn't cover it. |
|||
.PARAMETER Descriptor |
|||
The security descriptor of the item. |
|||
.PARAMETER Sid |
|||
The SIDs of a token, such as the output of Get-LabTokenSid. |
|||
#> |
|||
[CmdletBinding()] |
|||
[OutputType([long])] |
|||
param ( |
|||
[Parameter(Mandatory)] |
|||
[System.Security.AccessControl.RawSecurityDescriptor] |
|||
$Descriptor, |
|||
|
|||
[Parameter(Mandatory)] |
|||
[string[]] |
|||
$Sid |
|||
) |
|||
|
|||
$granted = 0L |
|||
foreach ($ace in $Descriptor.DiscretionaryAcl) { |
|||
if ($ace -isnot [System.Security.AccessControl.CommonAce] -or $ace.SecurityIdentifier.Value -notin $Sid) { |
|||
continue |
|||
} |
|||
|
|||
# HasFlag, because Windows PowerShell can't apply -band to an enum of the type byte. |
|||
if ($ace.AceFlags.HasFlag([System.Security.AccessControl.AceFlags]::InheritOnly)) { |
|||
continue |
|||
} |
|||
|
|||
if ($ace.AceQualifier -ne [System.Security.AccessControl.AceQualifier]::AccessAllowed) { |
|||
throw "The DACL denies $($ace.SecurityIdentifier) access, which Get-LabGrantedRight doesn't calculate." |
|||
} |
|||
|
|||
$granted = $granted -bor ([long]$ace.AccessMask -band 0xFFFFFFFFL) |
|||
} |
|||
|
|||
$granted |
|||
} |
|||
|
|||
function Assert-LabTestTarget { |
|||
<# |
|||
.SYNOPSIS |
|||
Throws unless this process runs on the client or the file server that a configuration of the lab names, and |
|||
the folder of the run lies in the share of the lab. |
|||
.DESCRIPTION |
|||
The live tests change security descriptors and must never run on another computer, such as the host of the |
|||
lab or a workstation. |
|||
.PARAMETER Configuration |
|||
The configuration of the run that Invoke-NTFSSecurityLabTest.ps1 wrote. |
|||
#> |
|||
[CmdletBinding()] |
|||
param ( |
|||
[Parameter(Mandatory)] |
|||
[object] |
|||
$Configuration |
|||
) |
|||
|
|||
# Without CIM, which an account that isn't an administrator can't use in a remote session |
|||
$domainName = [System.Net.NetworkInformation.IPGlobalProperties]::GetIPGlobalProperties().DomainName |
|||
if ($domainName -ne $Configuration.DomainName) { |
|||
throw "The live tests run only on a computer of the lab domain '$($Configuration.DomainName)'." |
|||
} |
|||
|
|||
if ($env:COMPUTERNAME -notin $Configuration.Client, $Configuration.FileServer) { |
|||
throw "The live tests run only on '$($Configuration.Client)' and '$($Configuration.FileServer)', not on '$env:COMPUTERNAME'." |
|||
} |
|||
|
|||
$shareRoot = '\\{0}\{1}\' -f $Configuration.FileServerFqdn, $Configuration.ShareName |
|||
$comparison = [System.StringComparison]::OrdinalIgnoreCase |
|||
if (-not $Configuration.SharePath.StartsWith($shareRoot, $comparison) -or |
|||
-not $Configuration.ServerPath.StartsWith($Configuration.ShareLocalPath + '\', $comparison) -or |
|||
$Configuration.SharePath.Contains('..') -or $Configuration.ServerPath.Contains('..')) { |
|||
throw "The folder of the run must lie in the share '$shareRoot' of the lab." |
|||
} |
|||
} |
|||
@ -0,0 +1,468 @@ |
|||
<# |
|||
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) } |
|||
} |
|||
) |
|||
} |
|||
|
|||
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 <Description>' -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 <Description>' -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. |
|||
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. ' + |
|||
'For more accurate results, calculate effective access rights on the target computer') |
|||
$result | Should -HaveCount 1 |
|||
Format-LabRight -Right $result[0].AccessRights | Should -Be (Format-LabRight -Right $configuration.EffectiveAccess.ClientRights) |
|||
} |
|||
} |
|||
|
|||
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 'Security descriptors on the file server after the runs on the client' -Tag 'Server' -Skip:(-not $configured) { |
|||
It 'Should keep Administrators as the owner of <Folder>' -ForEach $ownedFolders { |
|||
Get-LabOwner -Path (Get-LabPath -RelativePath $Folder) | Should -Be $administrators |
|||
} |
|||
|
|||
It 'Should have <Count> explicit audit entries on <Folder>' -ForEach $auditExpectations { |
|||
$acl = Get-Acl -LiteralPath (Get-LabPath -RelativePath $Folder) -Audit |
|||
|
|||
@($acl.GetAuditRules($true, $false, $sidType)).Count | Should -Be $Count |
|||
} |
|||
} |
|||
@ -0,0 +1,124 @@ |
|||
# Live tests in a lab |
|||
|
|||
The tests in this folder run the module against a Windows file server with |
|||
domain accounts, in an [AutomatedLab](https://automatedlab.org) lab. They cover |
|||
the cases that depend on the file server or on the accounts, which the tests in |
|||
`Tests` can't cover: those run on one computer, against local folders, with |
|||
local and well-known accounts. CI doesn't run the tests in this folder, and |
|||
without a lab they skip every test. |
|||
|
|||
## Cases |
|||
|
|||
| Case | Role | What the tests check | |
|||
| --- | --- | --- | |
|||
| 1, [#34][issue-34] | Delegate | `Add-NTFSAccess`, `Remove-NTFSAccess`, `Clear-NTFSAccess`, `Disable-NTFSAccessInheritance`, `Enable-NTFSAccessInheritance`, `Set-NTFSInheritance`, and `Set-NTFSSecurityDescriptor` on share folders that Administrators own and on which a domain group has Full Control, run by a member of that group who isn't an administrator of the file server. They succeed and keep the owner. | |
|||
| 2 | Admin, ServerAdmin, Delegate | `Get-NTFSAudit`, `Add-NTFSAudit`, and `Remove-NTFSAudit` on share folders. Over SMB, the file server checks the Security privilege of the account. The administrators of the file server read and change the audit entries; the delegated account gets the errors that the cmdlet pages describe, and the folders stay unchanged. | |
|||
| 3 | Admin | `Get-NTFSEffectiveAccess` for a domain account with rights through two nested domain groups and through a local group of the file server. With `-ServerName`, the result includes the local group, without a warning; without it, the client doesn't know that group. With an unreachable server, the cmdlet falls back to the client and warns. | |
|||
| 4 | Admin | `Get-NTFSOrphanedAccess` returns the entry of a deleted domain account with its SID, on the folder and as inherited entry on a file in it. | |
|||
| Long paths | Admin | `Get-ChildItem2` and `Get-NTFSAccess` with a share path longer than 260 characters. | |
|||
| [#108][issue-108] | Admin | `Copy-Item2` and `Move-Item2` with `-WhatIf` onto an existing file on the share write no error. | |
|||
| State | Server | After the runs on the client, the file server checks the owners and the audit entries of the folders itself, without the module. | |
|||
|
|||
Case 1 uses two kinds of folders. Before 5.0.0-rc3, the cmdlets wrote back the |
|||
owner that Windows returns with a DACL without the auto-inherit flag, and the |
|||
file server refused it with error 1307, "This security ID may not be assigned |
|||
as the owner of this object". Windows sets that flag whenever it writes a DACL |
|||
with `SetNamedSecurityInfo`, so the fixture stores the DACL of one kind of |
|||
folders again with `SetFileSecurity`, without the flag, like tools that predate |
|||
Windows 2000. `Set-NTFSSecurityDescriptor` wrote the owner on both kinds. |
|||
|
|||
The expected rights of case 3 come from the tokens that the file server and the |
|||
client create for the account with a Kerberos S4U logon, the way the Effective |
|||
Access tab of the advanced security settings does. |
|||
|
|||
## Roles |
|||
|
|||
| Role | Account | Administrator of the client | Administrator of the file server | |
|||
| --- | --- | --- | --- | |
|||
| Delegate | `NtfsLiveDelegate`, member of `NtfsLiveDelegates` | Yes | No | |
|||
| ServerAdmin | `NtfsLiveServerAdmin`, member of Remote Management Users on the client | No | Yes | |
|||
| Admin | `NtfsLiveAdmin` | Yes | Yes | |
|||
| Server | The installation account of the lab, on the file server | Yes | Yes | |
|||
|
|||
The script also creates `NtfsLiveSubject`, the account of case 3, which is a |
|||
member of `NtfsLiveInner`, a member of `NtfsLiveOuter`, and of the local group |
|||
`NtfsLiveLocal` of the file server, and `NtfsLiveOrphan`, which it deletes in |
|||
every run. |
|||
|
|||
## Lab |
|||
|
|||
The lab needs a domain controller, a file server, and a client of one domain, |
|||
PowerShell 7 and Pester 5.7.1 on the client and the file server, and |
|||
remoting with CredSSP from the host, which AutomatedLab sets up. The defaults |
|||
use the lab of |
|||
[WindowsAccessControl](https://github.com/raandree/WindowsAccessControl), which |
|||
`tests/Lab/Deploy-WindowsAccessControlLab.ps1` in that repository deploys: |
|||
`F1ADC1` as domain controller, `F1AFile2` as file server, and `F1AFile1` as |
|||
client, all in `a.forest1.net`. `-DomainController`, `-FileServer`, and |
|||
`-Client` select other machines. |
|||
|
|||
The script adds to the lab: |
|||
|
|||
- the organizational unit `NTFSSecurityLive` with the accounts and groups |
|||
- the local group `NtfsLiveLocal` and members of Administrators on the file |
|||
server, and members of Administrators and Remote Management Users on the |
|||
client |
|||
- the share `NTFSSecurityLive` on `C:\NTFSSecurityLive` of the file server, |
|||
with a folder for each run |
|||
- the folder `C:\NTFSSecurityLab` with the tests on the file server, and with |
|||
the modules and the tests on the client |
|||
|
|||
## Run the tests |
|||
|
|||
In an elevated Windows PowerShell 5.1 session on the Hyper-V host of the lab: |
|||
|
|||
```powershell |
|||
.\Tests\Lab\Invoke-NTFSSecurityLabTest.ps1 -Version 5.0.0-rc2, 5.0.0-rc4 -Confirm:$false |
|||
``` |
|||
|
|||
The script downloads each version from the PowerShell Gallery, checks the hash |
|||
that the gallery publishes, and runs the tests for each version in Windows |
|||
PowerShell 5.1 and PowerShell 7. Each version runs in its own process, because |
|||
all versions of `NTFSSecurity.dll` have the same assembly version. To test a |
|||
build, add `-ModulePath .\NTFSSecurity\bin\Release`; it runs as the version |
|||
`local`. |
|||
|
|||
The script sets new random passwords for the accounts in every call and keeps |
|||
them in memory only. The tests refuse to run on a computer other than the |
|||
client and the file server of the configuration, and on a folder outside the |
|||
share. |
|||
|
|||
Remove everything the script added to the lab: |
|||
|
|||
```powershell |
|||
.\Tests\Lab\Invoke-NTFSSecurityLabTest.ps1 -RemoveFixture |
|||
``` |
|||
|
|||
## Results |
|||
|
|||
Each call writes to a new folder in `$env:TEMP\NTFSSecurityLab\Results`: |
|||
|
|||
- `Summary.md` and `Summary.json`: the counts per version, edition, and role, |
|||
and the failed tests with their messages |
|||
- `<run>-<role>.result.json` and `<run>-<role>.log`: the result and the |
|||
error message of each test that ran, and the output of Pester |
|||
- `<run>.json`: the configuration of the run |
|||
- `<run>-State.json`: the stored owner, group, and DACL, and the SACL of each |
|||
folder after the run |
|||
|
|||
A version before 5.0.0-rc3 fails case 1 with error 1307, a version before |
|||
5.0.0-rc4 fails the tests of #108, and a version before 5.0.0-rc5 fails the |
|||
test of case 3 with a computer that can't be reached: it returned no access |
|||
instead of the result of the client. |
|||
|
|||
## Files |
|||
|
|||
| File | Purpose | |
|||
| --- | --- | |
|||
| `Invoke-NTFSSecurityLabTest.ps1` | Prepares the lab, runs the tests, and writes the results; runs on the host. | |
|||
| `NTFSSecurity.Live.Tests.ps1` | The tests; run on the client and the file server. | |
|||
| `Start-NTFSSecurityLiveTest.ps1` | Runs the tests of one role in a new process. | |
|||
| `NTFSSecurity.LabHelpers.ps1` | Reads and writes security descriptors as Windows stores them, and calculates the expected rights. | |
|||
|
|||
[issue-34]: https://github.com/raandree/NTFSSecurity/issues/34 |
|||
[issue-108]: https://github.com/raandree/NTFSSecurity/issues/108 |
|||
@ -0,0 +1,85 @@ |
|||
<# |
|||
.SYNOPSIS |
|||
Runs NTFSSecurity.Live.Tests.ps1 for one role and writes the result file. |
|||
|
|||
.DESCRIPTION |
|||
Invoke-NTFSSecurityLabTest.ps1 starts this script in a new Windows PowerShell 5.1 or PowerShell 7 process on the |
|||
client or the file server of the lab, as the account of the role. Each module version runs in its own process, |
|||
because all versions of NTFSSecurity.dll have the same assembly version, and a second import in a process would |
|||
use the first DLL. Exits with 0 when all tests passed and with 1 otherwise. |
|||
|
|||
.PARAMETER ModulePath |
|||
The folder that contains NTFSSecurity.psd1 of the version to test. The role Server needs no module. |
|||
|
|||
.PARAMETER ConfigurationPath |
|||
The configuration of the run that Invoke-NTFSSecurityLabTest.ps1 wrote. |
|||
|
|||
.PARAMETER Role |
|||
The role whose tests run: Delegate, ServerAdmin, Admin, or Server. |
|||
|
|||
.PARAMETER ResultPath |
|||
The path of the result file: a JSON array with the name, the result, and the error message of each test that |
|||
ran, and of each test file that failed. Pester's result formats read the operating system through CIM, which an |
|||
account that isn't an administrator can't use in a remote session. |
|||
|
|||
.EXAMPLE |
|||
.\Start-NTFSSecurityLiveTest.ps1 -ModulePath C:\NTFSSecurityLab\Modules\5.0.0-rc4\NTFSSecurity -ConfigurationPath C:\NTFSSecurityLab\Configuration\Run.json -Role Delegate -ResultPath $env:TEMP\Delegate.json |
|||
|
|||
Runs the tests of the delegated account against 5.0.0-rc4. |
|||
#> |
|||
[CmdletBinding()] |
|||
param ( |
|||
[string] |
|||
$ModulePath, |
|||
|
|||
[Parameter(Mandatory)] |
|||
[ValidateNotNullOrEmpty()] |
|||
[string] |
|||
$ConfigurationPath, |
|||
|
|||
[Parameter(Mandatory)] |
|||
[ValidateSet('Delegate', 'ServerAdmin', 'Admin', 'Server')] |
|||
[string] |
|||
$Role, |
|||
|
|||
[Parameter(Mandatory)] |
|||
[ValidateNotNullOrEmpty()] |
|||
[string] |
|||
$ResultPath |
|||
) |
|||
|
|||
$ErrorActionPreference = 'Stop' |
|||
Import-Module -Name Pester -RequiredVersion 5.7.1 |
|||
|
|||
$data = @{ |
|||
ModulePath = $ModulePath |
|||
ConfigurationPath = $ConfigurationPath |
|||
Role = $Role |
|||
} |
|||
$configuration = New-PesterConfiguration |
|||
$configuration.Run.Container = New-PesterContainer -Path (Join-Path -Path $PSScriptRoot -ChildPath 'NTFSSecurity.Live.Tests.ps1') -Data $data |
|||
$configuration.Run.PassThru = $true |
|||
$configuration.Filter.Tag = $Role |
|||
$configuration.Output.Verbosity = 'Detailed' |
|||
$configuration.Output.RenderMode = 'Plaintext' |
|||
$result = Invoke-Pester -Configuration $configuration |
|||
|
|||
$tests = foreach ($test in $result.Tests | Where-Object -FilterScript { $_.Result -ne 'NotRun' }) { |
|||
[pscustomobject]@{ |
|||
Name = $test.ExpandedPath |
|||
Result = [string]$test.Result |
|||
Message = (@($test.ErrorRecord) | Where-Object -FilterScript { $_ } | ForEach-Object -Process { $_.ToString() }) -join [Environment]::NewLine |
|||
} |
|||
} |
|||
|
|||
# A test file fails also when only its tests fail; it is listed only when it failed with an error of its own. |
|||
$failedFiles = foreach ($container in $result.Containers | Where-Object -FilterScript { $_.Result -eq 'Failed' -and @($_.ErrorRecord).Count -gt 0 }) { |
|||
[pscustomobject]@{ |
|||
Name = 'Test file {0}' -f $container.Item |
|||
Result = 'Failed' |
|||
Message = (@($container.ErrorRecord) | Where-Object -FilterScript { $_ } | ForEach-Object -Process { $_.ToString() }) -join [Environment]::NewLine |
|||
} |
|||
} |
|||
|
|||
ConvertTo-Json -InputObject @(@($tests) + @($failedFiles)) -Depth 3 | Set-Content -LiteralPath $ResultPath -Encoding UTF8 |
|||
exit [int]($result.Result -ne 'Passed') |
|||
Loading…
Reference in new issue