Browse Source

test: cover token handles, drive-root writes, name filters, and descriptor APIs

New guards cover the default AllAccess token handle, token handles that lack the right to query or to adjust privileges, and the finalizer of an abandoned PrivilegeEnabler, which closes the handle that it owns; writes to the root of a drive that subst maps to a sandbox folder, which skip for the restricted token of the basic-user runner that cannot define a drive letter; Get-ChildItem2 -Filter against a match by short name; retargeting a descriptor with Item; the path overloads of the inheritance helpers for a missing path and of the audit rule reader for folders; removing a generic right while another account holds an exact entry; and an empty -ServerName.

Follow-ups of the independent review: dispose the enabler in the finally block before its handle, don't pin that the iterator overloads write only when enumerated, arrange the previous owner of the RestoreOwnerError test, and describe what ObjectApis.Tests.ps1 does.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
pull/118/head
Raimund Andree 2 days ago
parent
commit
630926f897
  1. 32
      Tests/Access.Tests.ps1
  2. 70
      Tests/DriveRoot.Tests.ps1
  3. 20
      Tests/ItemCmdlets.Tests.ps1
  4. 49
      Tests/ObjectApis.Tests.ps1
  5. 3
      Tests/PathErrors.Tests.ps1
  6. 104
      Tests/Privileges.Tests.ps1
  7. 109
      Tests/TestHelpers.psm1

32
Tests/Access.Tests.ps1

@ -155,6 +155,20 @@ Describe 'Get-NTFSEffectiveAccess' {
"because the computer 'ntfssecurity-test.invalid' can't be reached for a remote access check. " +
'For more accurate results, calculate effective access rights on that computer.')
}
# An empty name names no computer, so it names this one no more than any other name that can't be reached.
It 'Should return the result of this computer and warn for an empty -ServerName' {
$expected = Get-NTFSEffectiveAccess -Path $effectiveFile -WarningAction SilentlyContinue -ErrorAction Stop
$result = @(Get-NTFSEffectiveAccess -Path $effectiveFile -ServerName '' -WarningVariable accessWarnings -WarningAction SilentlyContinue -ErrorVariable accessErrors -ErrorAction SilentlyContinue)
$accessErrors | Should -BeNullOrEmpty
$result | Should -HaveCount 1
$result[0].AccessRights | Should -Be $expected.AccessRights
$accessWarnings.Message | Should -Contain ("The effective rights can only be computed based on group membership on this computer, " +
"because the computer '' can't be reached for a remote access check. " +
'For more accurate results, calculate effective access rights on that computer.')
}
}
# Not every computer offers the remote interface of the authorization manager; the cmdlet then calculates the result
@ -604,6 +618,24 @@ Describe 'Remove-NTFSAccess' {
$removeErrors | Should -BeNullOrEmpty
Get-GuestsRule -Path $folder | Should -BeNullOrEmpty
}
# The entry of another account with exactly the rights to remove is not an exact match for the entry of the
# account, so the rights that the entry of the account keeps still have their Synchronize.
It 'Should take only the requested generic right from the entry of the account when another account has an exact entry' {
$users = [System.Security.Principal.SecurityIdentifier]'S-1-5-32-545'
$folder = New-GenericRightFolder -Entry '(A;OICIIO;0x10100000;;;BU)(A;OICIIO;0x90100000;;;BG)'
Remove-NTFSAccess -Path $folder -Account 'S-1-5-32-546' -AccessRights GenericAll -InheritanceFlags ContainerInherit, ObjectInherit -PropagationFlags InheritOnly -ErrorVariable removeErrors -ErrorAction SilentlyContinue
$removeErrors | Should -BeNullOrEmpty
$guestsRule = @(Get-GuestsRule -Path $folder)
$guestsRule | Should -HaveCount 1
[int] $guestsRule[0].FileSystemRights | Should -Be 0x80100000
$usersRule = @((Get-Acl -LiteralPath $folder).GetAccessRules($true, $false, [System.Security.Principal.SecurityIdentifier]) |
Where-Object -Property IdentityReference -EQ -Value $users)
$usersRule | Should -HaveCount 1
[int] $usersRule[0].FileSystemRights | Should -Be 0x10100000
}
}
Context 'With -RemoveSpecific' {
BeforeEach {

70
Tests/DriveRoot.Tests.ps1

@ -1,12 +1,18 @@
<#
Tests the cmdlets of the module built in NTFSSecurity\bin\Release on the root folder of the system drive. The tests
only read, so they need no sandbox.
Tests the cmdlets of the module built in NTFSSecurity\bin\Release on the root folder of the system drive, which they
only read, and on the root of a drive that maps a folder of a sandbox, which they change.
#>
[Diagnostics.CodeAnalysis.SuppressMessageAttribute(
'PSUseDeclaredVarsMoreThanAssignments', '', Justification = 'Pester shares variables between blocks.'
)]
param ()
BeforeDiscovery {
Import-Module -Name (Join-Path -Path $PSScriptRoot -ChildPath 'TestHelpers.psm1') -Force
# The restricted token of the basic-user runner cannot define a drive letter.
$canMapDrive = Test-DriveMappingAvailable
}
BeforeAll {
$modulePath = Join-Path -Path $PSScriptRoot -ChildPath '..\NTFSSecurity\bin\Release\NTFSSecurity.psd1'
Import-Module -Name $modulePath -Force -ErrorAction Stop
@ -51,3 +57,63 @@ Describe 'The root folder of a drive' {
@($entries | ForEach-Object -Process { $_.Account.Sid } | Sort-Object) | Should -Be $expected
}
}
# A test must not change the permissions of a volume. A drive letter that subst maps to a folder of a sandbox is the root
# of a drive for Windows and for the module, so the code that changes the root folder of a drive changes that folder.
Describe 'Changing the root folder of a drive' -Skip:(-not $canMapDrive) {
BeforeAll {
Import-Module -Name (Join-Path -Path $PSScriptRoot -ChildPath 'TestHelpers.psm1') -Force
$sandbox = New-TestSandbox -Name 'DriveRootChange'
$mapped = New-TestSandboxItem -Sandbox $sandbox -Name 'Mapped' -Directory
$driveRoot = New-TestDriveMapping -Sandbox $sandbox -Path $mapped
if (-not $driveRoot) {
throw 'No drive letter could be mapped to the sandbox folder.'
}
function Get-MappedEntry {
[Diagnostics.CodeAnalysis.SuppressMessageAttribute(
'PSUseSingularNouns', '', Justification = 'The helper returns the explicit entries of the folder.'
)]
param ([string] $Account)
@((Get-Acl -LiteralPath $mapped).GetAccessRules($true, $false, $sidType) |
Where-Object -FilterScript { $_.IdentityReference.Value -eq $Account })
}
}
AfterAll {
if ($driveRoot) {
Remove-TestDriveMapping -Root $driveRoot
}
Remove-TestSandbox -Sandbox $sandbox
}
It 'Should read the access entries of the folder that the drive maps' {
$expected = @((Get-Acl -LiteralPath $mapped).GetAccessRules($true, $true, $sidType) |
ForEach-Object -Process { $_.IdentityReference.Value } | Sort-Object)
$entries = @(Get-NTFSAccess -Path $driveRoot)
@($entries | ForEach-Object -Process { $_.Account.Sid } | Sort-Object) | Should -Be $expected
}
It 'Should add and remove an access entry of the folder that the drive maps' {
Add-NTFSAccess -Path $driveRoot -Account 'S-1-1-0' -AccessRights ReadData -ErrorAction Stop
Get-MappedEntry -Account 'S-1-1-0' | Should -HaveCount 1
Remove-NTFSAccess -Path $driveRoot -Account 'S-1-1-0' -AccessRights ReadData -ErrorAction Stop
Get-MappedEntry -Account 'S-1-1-0' | Should -BeNullOrEmpty
}
It 'Should block and restore the access inheritance of the folder that the drive maps' {
Disable-NTFSAccessInheritance -Path $driveRoot -ErrorAction Stop
(Get-Acl -LiteralPath $mapped).AreAccessRulesProtected | Should -BeTrue
Enable-NTFSAccessInheritance -Path $driveRoot -ErrorAction Stop
(Get-Acl -LiteralPath $mapped).AreAccessRulesProtected | Should -BeFalse
}
}

20
Tests/ItemCmdlets.Tests.ps1

@ -140,6 +140,26 @@ Describe 'Get-ChildItem2' {
($relative | Sort-Object) -join ',' | Should -Be (($Expected | Sort-Object) -join ',')
}
# The pattern must match the name of the item. When Windows lists a folder with a pattern, it also compares the
# short name (8.3) of an item, so *.htm finds Page2.html as well, as Get-ChildItem does where the volume creates
# short names. The cmdlet compares the name again.
It 'Should return only the items whose name matches -Filter <Filter>' -ForEach @(
@{ Filter = '*.htm'; Expected = @('Page.htm') }
@{ Filter = 'Page?.html'; Expected = @('Page2.html') }
@{ Filter = 'PAGE*'; Expected = @('Page.htm', 'Page2.html') }
) {
$folder = New-TestSandboxItem -Sandbox $sandbox -Name 'FilterNames' -Directory
foreach ($name in 'Page.htm', 'Page2.html') {
$file = Join-Path -Path $folder -ChildPath $name
Assert-TestSandboxPath -Sandbox $sandbox -Path $file
Set-Content -LiteralPath $file -Value $name
}
$result = @(Get-ChildItem2 -Path $folder -Filter $Filter -ErrorAction Stop)
($result.Name | Sort-Object) -join ',' | Should -Be (($Expected | Sort-Object) -join ',')
}
It 'Should stop a recursive pipeline without recording an enumeration error' {
$result = @(Get-ChildItem2 -Path $tree -Recurse -ErrorVariable childErrors -ErrorAction SilentlyContinue | Select-Object -First 1)

49
Tests/ObjectApis.Tests.ps1

@ -1,5 +1,5 @@
<#
Tests the public object APIs used with cmdlet output, without changing an item's security descriptor.
Tests the public object APIs used with cmdlet output, on files and folders in a sandbox folder.
#>
[Diagnostics.CodeAnalysis.SuppressMessageAttribute(
'PSUseDeclaredVarsMoreThanAssignments', '', Justification = 'Pester shares variables between blocks.'
@ -376,8 +376,9 @@ Describe 'Access rule helpers that take a path' {
$entries[0].FileSystemRights | Should -Be ([System.Security.AccessControl.FileSystemRights] 'ReadData, Synchronize')
}
# The overload for several accounts is an iterator, so it writes nothing until the caller enumerates the result.
It 'Should add the entries of several accounts to a <Kind> by its path only when the result is enumerated' -ForEach @(
# The overload that takes a path returns an iterator, so the caller must enumerate the result to write the entries.
# The overloads that take an item write them at once; this test doesn't pin the difference.
It 'Should add the entries of several accounts to a <Kind> by its path when the result is enumerated' -ForEach @(
@{ Kind = 'file'; Directory = $false }
@{ Kind = 'folder'; Directory = $true }
) {
@ -388,8 +389,6 @@ Describe 'Access rule helpers that take a path' {
$path, $accounts, [Security2.FileSystemRights2]::ReadData, $allow, $noInheritance, $noPropagation
)
@(Get-ExplicitEntries -Path $path) | Should -BeNullOrEmpty
@(Get-ExplicitEntries -Path $path -Account 'S-1-5-32-545') | Should -BeNullOrEmpty
@($pending) | Should -HaveCount 2
@(Get-ExplicitEntries -Path $path) | Should -HaveCount 1
@(Get-ExplicitEntries -Path $path -Account 'S-1-5-32-545') | Should -HaveCount 1
@ -565,9 +564,13 @@ Describe 'Audit rule helpers that take a path' -Skip:(-not $holdsSecurityPrivile
$entries | Should -HaveCount 1
$entries[0].AuditFlags | Should -Be 'Success'
$entries[0].FileSystemRights | Should -Be ([System.Security.AccessControl.FileSystemRights]::Delete)
$found = @([Security2.FileSystemAuditRule2]::GetFileSystemAuditRules($path, $true, $false))
$found | Should -HaveCount 1
$found[0].Account.Sid | Should -BeExactly 'S-1-1-0'
$found[0].FullName | Should -BeExactly $path
}
It 'Should add the entries of several accounts to a <Kind> by its path only when the result is enumerated, and remove them again' -ForEach @(
It 'Should add the entries of several accounts to a <Kind> by its path when the result is enumerated, and remove them again' -ForEach @(
@{ Kind = 'file'; Directory = $false }
@{ Kind = 'folder'; Directory = $true }
) {
@ -580,7 +583,6 @@ Describe 'Audit rule helpers that take a path' -Skip:(-not $holdsSecurityPrivile
$path, $accounts, [Security2.FileSystemRights2]::Delete, $success, $noInheritance, $noPropagation
)
@(Get-AuditEntries -Path $path) | Should -BeNullOrEmpty
@($pending) | Should -HaveCount 2
@(Get-AuditEntries -Path $path) | Should -HaveCount 1
@(Get-AuditEntries -Path $path -Account 'S-1-5-32-545') | Should -HaveCount 1
@ -668,6 +670,21 @@ Describe 'Inheritance helpers that take a path' {
(Get-Acl -LiteralPath $path).AreAccessRulesProtected | Should -BeFalse
}
# The overloads that take a path do nothing for a path that is neither a file nor a folder.
It '<Method> should change nothing for a path that does not exist' -ForEach @(
@{ Method = 'EnableAccessInheritance' }
@{ Method = 'DisableAccessInheritance' }
@{ Method = 'EnableAuditInheritance' }
@{ Method = 'DisableAuditInheritance' }
) {
$missing = Join-Path -Path $sandbox -ChildPath ('Missing-{0}' -f [guid]::NewGuid().ToString('N'))
Assert-TestSandboxPath -Sandbox $sandbox -Path $missing
{ [Security2.FileSystemInheritanceInfo]::$Method($missing, $true) } | Should -Not -Throw
Test-Path -LiteralPath $missing | Should -BeFalse
}
It 'Should block and restore the audit inheritance of a <Kind> by its path' -Skip:(-not $holdsSecurityPrivilege) -ForEach @(
@{ Kind = 'file'; Directory = $false }
@{ Kind = 'folder'; Directory = $true }
@ -718,6 +735,24 @@ Describe 'Owner and descriptor objects' {
Where-Object -FilterScript { $_.IdentityReference.Value -eq 'S-1-1-0' }) | Should -HaveCount 1
}
# The item decides where Write puts the sections that the descriptor was read with, and Name and FullName follow it.
It 'Should write a descriptor to the item that the caller assigns' {
$source = New-TestSandboxItem -Sandbox $sandbox -Name 'RetargetSource'
$target = New-TestSandboxItem -Sandbox $sandbox -Name 'RetargetTarget'
Add-NTFSAccess -Path $source -Account 'S-1-1-0' -AccessRights ReadData
$descriptor = Get-NTFSSecurityDescriptor -Path $source
$descriptor.Item = Get-Item2 -Path $target
$descriptor.FullName | Should -BeExactly $target
$descriptor.Name | Should -BeExactly (Split-Path -Path $target -Leaf)
$descriptor.Write()
foreach ($path in $source, $target) {
@((Get-Acl -LiteralPath $path).GetAccessRules($true, $false, [System.Security.Principal.SecurityIdentifier]) |
Where-Object -FilterScript { $_.IdentityReference.Value -eq 'S-1-1-0' }) | Should -HaveCount 1
}
}
It 'Should name the missing path when it writes a descriptor to an item that does not exist' {
$source = New-TestSandboxItem -Sandbox $sandbox -Name 'DescriptorMissingSource'
$missing = Join-Path -Path $sandbox -ChildPath ('Missing-{0}' -f [guid]::NewGuid().ToString('N'))

3
Tests/PathErrors.Tests.ps1

@ -281,7 +281,8 @@ Describe 'An item whose owner may not change its permissions' {
# with the right in the DACL, which the cleared DACL no longer holds. The cmdlet reports the owner it cannot set back.
It 'Clear-NTFSAccess -DisableInheritance should report RestoreOwnerError for a previous owner that it cannot set back' -Skip:(-not $holdsRestorePrivilege) {
$user = [Security.Principal.WindowsIdentity]::GetCurrent().User.Value
$owner | Should -Not -Be $user
Set-TestOwner -Sandbox $sandbox -Path $file -Sid 'S-1-5-32-544'
(Get-Privileges | Where-Object -Property Privilege -EQ -Value 'Restore').PrivilegeState | Should -Be 'Disabled'
Add-NTFSAccess -Path $file -Account 'S-1-1-0' -AccessRights ReadData
Add-TestDenyRule -Sandbox $sandbox -Path $file -Rights @{ 'S-1-3-4' = 'ChangePermissions' }

104
Tests/Privileges.Tests.ps1

@ -268,7 +268,20 @@ Describe 'The PrivilegeEnabler class' {
BeforeAll {
$privateData['EnablePrivileges'] = $false
$backup = [ProcessPrivileges.Privilege]::Backup
$changeNotify = [ProcessPrivileges.Privilege]::ChangeNotify
$currentProcess = [System.Diagnostics.Process]::GetCurrentProcess()
# The enabler goes out of scope in the function, so that nothing but the caller's handle refers to what it owns.
function New-AbandonedHandle {
[Diagnostics.CodeAnalysis.SuppressMessageAttribute(
'PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Test helper that only creates an object.'
)]
param ($Process)
$enabler = New-Object -TypeName 'ProcessPrivileges.PrivilegeEnabler' -ArgumentList $Process
$field = [ProcessPrivileges.PrivilegeEnabler].GetField('accessTokenHandle', [System.Reflection.BindingFlags] 'NonPublic, Instance')
$field.GetValue($enabler)
}
}
AfterAll {
@ -335,21 +348,51 @@ Describe 'The PrivilegeEnabler class' {
It 'Should enable a privilege through an access token handle that the caller owns' -Skip:(-not $holdsPrivileges) {
$rights = [ProcessPrivileges.TokenAccessRights]::AdjustPrivileges -bor [ProcessPrivileges.TokenAccessRights]::Query
$handle = [ProcessPrivileges.ProcessExtensions]::GetAccessTokenHandle($currentProcess, $rights)
$enabler = $null
try {
$enabler = New-Object -TypeName 'ProcessPrivileges.PrivilegeEnabler' -ArgumentList $handle, $backup
Get-BackupPrivilegeState | Should -Be 'Enabled'
$enabler.Dispose()
$enabler = $null
Get-BackupPrivilegeState | Should -Be 'Disabled'
$handle.IsClosed | Should -BeFalse
}
finally {
# The enabler first: a handle that is closed under an enabler that still owns a privilege fails when the
# enabler disables the privilege.
if ($enabler) {
$enabler.Dispose()
}
$handle.Dispose()
}
$handle.IsClosed | Should -BeTrue
}
# The finalizer closes the token handle that an abandoned enabler opened and drops its registration, so that the next
# enabler for the process opens a handle of its own instead of taking a closed one. The handle is private, so the test
# reads it by reflection. An enabler that enabled a privilege stays referenced by a static list until it is disposed,
# so it is never finalized and its privilege stays enabled; only an enabler without a privilege can be abandoned.
It 'Should close the token handle of an enabler that was never disposed when it is finalized' {
$handle = New-AbandonedHandle -Process $currentProcess
$handle.IsClosed | Should -BeFalse
for ($attempt = 0; $attempt -lt 10 -and -not $handle.IsClosed; $attempt++) {
[GC]::Collect()
[GC]::WaitForPendingFinalizers()
}
$handle.IsClosed | Should -BeTrue
$enabler = New-Object -TypeName 'ProcessPrivileges.PrivilegeEnabler' -ArgumentList $currentProcess
try {
$enabler.EnablePrivilege($changeNotify) | Should -Be 'None'
}
finally {
$enabler.Dispose()
}
}
# The access tokens of administrators don't hold the privilege to create a token, and those of basic users don't hold
# most of the others.
It 'Should leave a privilege that the access token does not hold alone' {
@ -383,6 +426,67 @@ Describe 'The PrivilegeEnabler class' {
}
}
# Every access token holds the privilege to bypass traverse checking, enabled. The tests use it because they need no other
# privilege and change nothing: a handle that lacks a right fails before it adjusts anything.
Describe 'The access token handle of a process' {
BeforeAll {
$currentProcess = [System.Diagnostics.Process]::GetCurrentProcess()
$changeNotify = [ProcessPrivileges.Privilege]::ChangeNotify
$tokenRights = [ProcessPrivileges.TokenAccessRights]
}
It 'Should open a handle with all access rights when the caller names none and close it on dispose' {
$handle = [ProcessPrivileges.ProcessExtensions]::GetAccessTokenHandle($currentProcess)
try {
$handle.IsInvalid | Should -BeFalse
@([ProcessPrivileges.ProcessExtensions]::GetPrivileges($handle)) | Should -Not -BeNullOrEmpty
[ProcessPrivileges.ProcessExtensions]::GetPrivilegeState($handle, $changeNotify) | Should -Be 'Enabled'
}
finally {
$handle.Dispose()
}
$handle.IsClosed | Should -BeTrue
}
It 'Should refuse to enable a privilege through a handle that may only query' {
$handle = [ProcessPrivileges.ProcessExtensions]::GetAccessTokenHandle($currentProcess, $tokenRights::Query)
try {
$failure = { [ProcessPrivileges.ProcessExtensions]::EnablePrivilege($handle, $changeNotify) } | Should -Throw -PassThru
$failure.Exception.InnerException | Should -BeOfType [System.ComponentModel.Win32Exception]
$failure.Exception.InnerException.NativeErrorCode | Should -Be 5
[ProcessPrivileges.ProcessExtensions]::GetPrivilegeState($handle, $changeNotify) | Should -Be 'Enabled'
}
finally {
$handle.Dispose()
}
}
It 'Should refuse to <Operation> through a handle that may only adjust privileges' -ForEach @(
@{ Operation = 'list the privileges' }
@{ Operation = 'read the state of a privilege' }
) {
$handle = [ProcessPrivileges.ProcessExtensions]::GetAccessTokenHandle($currentProcess, $tokenRights::AdjustPrivileges)
try {
$failure = {
if ($Operation -eq 'list the privileges') {
[ProcessPrivileges.ProcessExtensions]::GetPrivileges($handle)
}
else {
[ProcessPrivileges.ProcessExtensions]::GetPrivilegeState($handle, $changeNotify)
}
} | Should -Throw -PassThru
$failure.Exception.InnerException | Should -BeOfType [System.ComponentModel.Win32Exception]
$failure.Exception.InnerException.NativeErrorCode | Should -Be 5
}
finally {
$handle.Dispose()
}
}
}
Describe 'The PrivilegeControl class' {
BeforeAll {
$privateData['EnablePrivileges'] = $false

109
Tests/TestHelpers.psm1

@ -422,6 +422,113 @@ function ConvertTo-TestAdminSharePath {
'\\localhost\{0}${1}' -f $Path.Substring(0, 1), $Path.Substring(2)
}
function New-TestDriveMapping {
<#
.SYNOPSIS
Maps a free drive letter to a folder of the sandbox with subst and returns the root of the drive, such as Z:\.
Returns nothing when the process cannot define a drive letter, as the restricted token of a basic user cannot.
.DESCRIPTION
For Windows and for the module, the root of the mapped drive is the root folder of a drive, so that a test can
change it without changing a volume. The helper checks the folder with Assert-TestSandboxPath first and unmaps
the letter again, with an error, when a marker file of the folder is not visible through it, so that a mapping
that points elsewhere is never used. Remove the mapping with Remove-TestDriveMapping.
.PARAMETER Sandbox
The sandbox folder that New-TestSandbox returned.
.PARAMETER Path
The full path of the folder to map, in the sandbox.
#>
[Diagnostics.CodeAnalysis.SuppressMessageAttribute(
'PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Test helper that only maps sandbox folders.'
)]
[CmdletBinding()]
[OutputType([string])]
param (
[Parameter(Mandatory)]
[string]
$Sandbox,
[Parameter(Mandatory)]
[string]
$Path
)
Assert-TestSandboxPath -Sandbox $Sandbox -Path $Path
$marker = [guid]::NewGuid().ToString('N')
$markerPath = Join-Path -Path $Path -ChildPath $marker
Assert-TestSandboxPath -Sandbox $Sandbox -Path $markerPath
Set-Content -LiteralPath $markerPath -Value $marker
$subst = Join-Path -Path $env:SystemRoot -ChildPath 'System32\subst.exe'
# A test run in parallel can map a letter at the same moment, which makes subst fail for that letter.
foreach ($letter in 'Z', 'Y', 'X', 'W', 'V', 'U', 'T', 'S') {
$root = '{0}:\' -f $letter
if (Test-Path -LiteralPath $root) {
continue
}
& $subst ('{0}:' -f $letter) $Path *> $null
if ($LASTEXITCODE -ne 0) {
continue
}
if (Test-Path -LiteralPath (Join-Path -Path $root -ChildPath $marker)) {
return $root
}
& $subst ('{0}:' -f $letter) /d *> $null
throw "The drive '$root' does not show the sandbox folder '$Path'."
}
}
function Remove-TestDriveMapping {
<#
.SYNOPSIS
Removes a mapping of New-TestDriveMapping.
.PARAMETER Root
The root of the drive that New-TestDriveMapping returned, such as Z:\.
#>
[Diagnostics.CodeAnalysis.SuppressMessageAttribute(
'PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Test helper that only removes its own mapping.'
)]
[CmdletBinding()]
param (
[Parameter(Mandatory)]
[ValidatePattern('^[A-Z]:\\$')]
[string]
$Root
)
& (Join-Path -Path $env:SystemRoot -ChildPath 'System32\subst.exe') $Root.TrimEnd('\') /d *> $null
if (Test-Path -LiteralPath $Root) {
Write-Error -Message "The drive mapping '$Root' could not be removed."
}
}
function Test-DriveMappingAvailable {
<#
.SYNOPSIS
Returns $true when the process can define a drive letter for a folder with subst, which the restricted token of
the basic-user runner cannot.
#>
[CmdletBinding()]
[OutputType([bool])]
param ()
$sandbox = New-TestSandbox -Name 'DriveProbe'
try {
$root = New-TestDriveMapping -Sandbox $sandbox -Path $sandbox
if ($root) {
Remove-TestDriveMapping -Root $root
}
[bool] $root
}
finally {
Remove-TestSandbox -Sandbox $sandbox
}
}
Export-ModuleMember -Function New-TestSandbox, Assert-TestSandboxPath, Remove-TestSandbox, New-TestSandboxItem,
Block-TestReadPermission, Block-TestWritePermission, Add-TestDenyRule, Set-TestOwner, Test-IsElevated,
Test-PrivilegeHeld, Test-AdminShareAvailable, ConvertTo-TestAdminSharePath
Test-PrivilegeHeld, Test-AdminShareAvailable, ConvertTo-TestAdminSharePath, New-TestDriveMapping,
Remove-TestDriveMapping, Test-DriveMappingAvailable

Loading…
Cancel
Save