|
|
|
|
<#
|
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>
3 weeks ago
|
|
|
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.'
|
|
|
|
|
)]
|
|
|
|
|
param ()
|
|
|
|
|
|
|
|
|
|
BeforeDiscovery {
|
|
|
|
|
Import-Module -Name (Join-Path -Path $PSScriptRoot -ChildPath 'TestHelpers.psm1') -Force
|
|
|
|
|
$holdsSecurityPrivilege = Test-PrivilegeHeld -Name 'SeSecurityPrivilege'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
BeforeAll {
|
|
|
|
|
Import-Module -Name (Join-Path -Path $PSScriptRoot -ChildPath 'TestHelpers.psm1') -Force
|
|
|
|
|
$modulePath = Join-Path -Path $PSScriptRoot -ChildPath '..\NTFSSecurity\bin\Release\NTFSSecurity.psd1'
|
|
|
|
|
Import-Module -Name $modulePath -Force -ErrorAction Stop
|
|
|
|
|
$sandbox = New-TestSandbox -Name 'ObjectApis'
|
|
|
|
|
$objectPath = Join-Path -Path $sandbox -ChildPath 'Rule.txt'
|
|
|
|
|
$identity = [Security2.IdentityReference2] 'S-1-1-0'
|
|
|
|
|
$sid = New-Object -TypeName 'System.Security.Principal.SecurityIdentifier' -ArgumentList 'S-1-1-0'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
AfterAll {
|
|
|
|
|
Remove-TestSandbox -Sandbox $sandbox
|
|
|
|
|
Remove-Module -Name NTFSSecurity -Force -ErrorAction SilentlyContinue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Describe 'Rule constructors with a path' {
|
|
|
|
|
It 'Should preserve the supplied path and name of an <Kind> rule' -ForEach @(
|
|
|
|
|
@{ Kind = 'access' }
|
|
|
|
|
@{ Kind = 'audit' }
|
|
|
|
|
) {
|
|
|
|
|
if ($Kind -eq 'access') {
|
|
|
|
|
$raw = New-Object -TypeName 'System.Security.AccessControl.FileSystemAccessRule' -ArgumentList (
|
|
|
|
|
$sid, [System.Security.AccessControl.FileSystemRights]::ReadData,
|
|
|
|
|
[System.Security.AccessControl.AccessControlType]::Allow
|
|
|
|
|
)
|
|
|
|
|
$rule = New-Object -TypeName 'Security2.FileSystemAccessRule2' -ArgumentList $raw, $objectPath
|
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
$raw = New-Object -TypeName 'System.Security.AccessControl.FileSystemAuditRule' -ArgumentList (
|
|
|
|
|
$sid, [System.Security.AccessControl.FileSystemRights]::ReadData,
|
|
|
|
|
[System.Security.AccessControl.AuditFlags]::Success
|
|
|
|
|
)
|
|
|
|
|
$rule = New-Object -TypeName 'Security2.FileSystemAuditRule2' -ArgumentList $raw, $objectPath
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$rule.FullName | Should -BeExactly $objectPath
|
|
|
|
|
$rule.Name | Should -BeExactly 'Rule.txt'
|
|
|
|
|
$rule.InheritanceEnabled = $true
|
|
|
|
|
$rule.InheritedFrom = $sandbox
|
|
|
|
|
$rule.InheritanceEnabled | Should -BeTrue
|
|
|
|
|
$rule.InheritedFrom | Should -BeExactly $sandbox
|
|
|
|
|
$rule.GetHashCode() | Should -Be $raw.GetHashCode()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Describe 'Simplified audit entries' {
|
|
|
|
|
It 'Should reduce <Rights> to <Expected>' -ForEach @(
|
|
|
|
|
@{ Rights = 'None'; Expected = 'None' }
|
|
|
|
|
@{ Rights = 'ReadData'; Expected = 'Read' }
|
|
|
|
|
@{ Rights = 'Read'; Expected = 'Read' }
|
|
|
|
|
@{ Rights = 'CreateFiles'; Expected = 'Write' }
|
|
|
|
|
@{ Rights = 'AppendData'; Expected = 'Write' }
|
|
|
|
|
@{ Rights = 'ReadExtendedAttributes'; Expected = 'Read' }
|
|
|
|
|
@{ Rights = 'WriteExtendedAttributes'; Expected = 'Write' }
|
|
|
|
|
@{ Rights = 'ExecuteFile'; Expected = 'Read' }
|
|
|
|
|
@{ Rights = 'DeleteSubdirectoriesAndFiles'; Expected = 'Delete' }
|
|
|
|
|
@{ Rights = 'ReadAttributes'; Expected = 'Read' }
|
|
|
|
|
@{ Rights = 'WriteAttributes'; Expected = 'Write' }
|
|
|
|
|
@{ Rights = 'Delete'; Expected = 'Delete' }
|
|
|
|
|
@{ Rights = 'ReadPermissions'; Expected = 'Read' }
|
|
|
|
|
@{ Rights = 'ChangePermissions'; Expected = 'Write' }
|
|
|
|
|
@{ Rights = 'TakeOwnership'; Expected = 'Write' }
|
|
|
|
|
@{ Rights = 'Synchronize'; Expected = 'Read' }
|
|
|
|
|
@{ Rights = 'FullControl'; Expected = 'Read, Write, Delete' }
|
|
|
|
|
@{ Rights = 'GenericRead'; Expected = 'Read' }
|
|
|
|
|
@{ Rights = 'GenericWrite'; Expected = 'Write' }
|
|
|
|
|
@{ Rights = 'GenericExecute'; Expected = 'Read' }
|
|
|
|
|
@{ Rights = 'GenericAll'; Expected = 'Read, Write, Delete' }
|
|
|
|
|
) {
|
|
|
|
|
$rule = New-Object -TypeName 'Security2.SimpleFileSystemAuditRule' -ArgumentList (
|
|
|
|
|
$objectPath, $identity, [Security2.FileSystemRights2] $Rights
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
$rule.AccessRights | Should -Be ([Security2.SimpleFileSystemAccessRights] $Expected)
|
|
|
|
|
$rule.FullName | Should -BeExactly $objectPath
|
|
|
|
|
$rule.Name | Should -BeExactly 'Rule.txt'
|
|
|
|
|
$rule.Identity.Sid | Should -BeExactly 'S-1-1-0'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
It 'Should compare audit entries reflexively and symmetrically, never as access entries' {
|
|
|
|
|
$first = New-Object -TypeName 'Security2.SimpleFileSystemAuditRule' -ArgumentList (
|
|
|
|
|
$objectPath, $identity, [Security2.FileSystemRights2]::Read
|
|
|
|
|
)
|
|
|
|
|
$second = New-Object -TypeName 'Security2.SimpleFileSystemAuditRule' -ArgumentList (
|
|
|
|
|
$objectPath, $identity, [Security2.FileSystemRights2]::Read
|
|
|
|
|
)
|
|
|
|
|
$access = New-Object -TypeName 'Security2.SimpleFileSystemAccessRule' -ArgumentList (
|
|
|
|
|
$objectPath, $identity, [Security2.FileSystemRights2]::Read,
|
|
|
|
|
[System.Security.AccessControl.AccessControlType]::Allow
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
$first.Equals($first) | Should -BeTrue
|
|
|
|
|
$first.Equals($second) | Should -BeTrue
|
|
|
|
|
$second.Equals($first) | Should -BeTrue
|
|
|
|
|
$first.GetHashCode() | Should -Be $second.GetHashCode()
|
|
|
|
|
$first.Equals($access) | Should -BeFalse
|
|
|
|
|
$access.Equals($first) | Should -BeFalse
|
|
|
|
|
$first.Equals($null) | Should -BeFalse
|
|
|
|
|
$first.Equals('Read') | Should -BeFalse
|
|
|
|
|
$second.AccessControlType = 'Deny'
|
|
|
|
|
$first.Equals($second) | Should -BeFalse
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
It 'Should preserve the path, account and ReadData when converting an audit entry' {
|
|
|
|
|
$raw = New-Object -TypeName 'System.Security.AccessControl.FileSystemAuditRule' -ArgumentList (
|
|
|
|
|
$sid, [System.Security.AccessControl.FileSystemRights]::ReadData,
|
|
|
|
|
[System.Security.AccessControl.AuditFlags]::Success
|
|
|
|
|
)
|
|
|
|
|
$wrapped = New-Object -TypeName 'Security2.FileSystemAuditRule2' -ArgumentList $raw, $objectPath
|
|
|
|
|
|
|
|
|
|
$simple = $wrapped.ToSimpleFileSystemAuditRule2()
|
|
|
|
|
|
|
|
|
|
$simple.FullName | Should -BeExactly $objectPath
|
|
|
|
|
$simple.Identity.Sid | Should -BeExactly 'S-1-1-0'
|
|
|
|
|
$simple.AccessRights | Should -Be ([Security2.SimpleFileSystemAccessRights]::Read)
|
|
|
|
|
$wrapped.ToString() | Should -BeExactly $raw.ToString()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Describe 'Identity comparisons and conversions' {
|
|
|
|
|
It 'Should compare <Value> with the identity by SID or resolved name' -ForEach @(
|
|
|
|
|
@{ Value = 'self'; Expected = $true }
|
|
|
|
|
@{ Value = 'same SID'; Expected = $true }
|
|
|
|
|
@{ Value = 'different SID'; Expected = $false }
|
|
|
|
|
@{ Value = 'SecurityIdentifier'; Expected = $true }
|
|
|
|
|
@{ Value = 'NTAccount'; Expected = $true }
|
|
|
|
|
@{ Value = 'SID string'; Expected = $true }
|
|
|
|
|
@{ Value = 'account name'; Expected = $true }
|
|
|
|
|
@{ Value = 'different string'; Expected = $false }
|
|
|
|
|
@{ Value = 'null'; Expected = $false }
|
|
|
|
|
@{ Value = 'other type'; Expected = $false }
|
|
|
|
|
) {
|
|
|
|
|
$other = switch ($Value) {
|
|
|
|
|
'self' { $identity }
|
|
|
|
|
'same SID' { [Security2.IdentityReference2] 'S-1-1-0' }
|
|
|
|
|
'different SID' { [Security2.IdentityReference2] 'S-1-5-32-546' }
|
|
|
|
|
'SecurityIdentifier' { $sid }
|
|
|
|
|
'NTAccount' { $sid.Translate([System.Security.Principal.NTAccount]) }
|
|
|
|
|
'SID string' { 'S-1-1-0' }
|
|
|
|
|
'account name' { $identity.AccountName.ToUpperInvariant() }
|
|
|
|
|
'different string' { 'NTFSSecurity-not-an-account' }
|
|
|
|
|
'null' { $null }
|
|
|
|
|
'other type' { 42 }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$identity.Equals($other) | Should -Be $Expected
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# tr-TR lowercases I to U+0131, so a comparison that lowercases both names with the current culture misses an account name that holds an
|
|
|
|
|
# uppercase I (NT AUTHORITY, BUILTIN) when the other name has another case. The name must match in every culture.
|
|
|
|
|
It 'Should compare a name with the account ignoring case in Turkish, where I and i are different letters' {
|
|
|
|
|
$system = [Security2.IdentityReference2] 'S-1-5-18'
|
|
|
|
|
$system.AccountName | Should -MatchExactly 'I' -Because 'the test needs an account name with an uppercase I'
|
|
|
|
|
$original = [System.Threading.Thread]::CurrentThread.CurrentCulture
|
|
|
|
|
try {
|
|
|
|
|
[System.Threading.Thread]::CurrentThread.CurrentCulture = [System.Globalization.CultureInfo]::GetCultureInfo('tr-TR')
|
|
|
|
|
'I'.ToLower() | Should -Be ([string][char]0x131) -Because 'the culture of the thread has to be Turkish for the test to mean anything'
|
|
|
|
|
$system.Equals($system.AccountName) | Should -BeTrue
|
|
|
|
|
$system.Equals($system.AccountName.ToLowerInvariant()) | Should -BeTrue
|
|
|
|
|
$system.Equals($system.AccountName.ToUpperInvariant()) | Should -BeTrue
|
|
|
|
|
$system.Equals('NTFSSecurity-not-an-account') | Should -BeFalse
|
|
|
|
|
}
|
|
|
|
|
finally {
|
|
|
|
|
[System.Threading.Thread]::CurrentThread.CurrentCulture = $original
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
It 'Should compare null operands and distinct instances through both operators' {
|
|
|
|
|
$same = [Security2.IdentityReference2] 'S-1-1-0'
|
|
|
|
|
$different = [Security2.IdentityReference2] 'S-1-5-32-546'
|
|
|
|
|
|
|
|
|
|
[Security2.IdentityReference2]::op_Equality($null, $null) | Should -BeTrue
|
|
|
|
|
[Security2.IdentityReference2]::op_Equality($identity, $null) | Should -BeFalse
|
|
|
|
|
[Security2.IdentityReference2]::op_Equality($null, $identity) | Should -BeFalse
|
|
|
|
|
[Security2.IdentityReference2]::op_Equality($identity, $same) | Should -BeTrue
|
|
|
|
|
[Security2.IdentityReference2]::op_Inequality($identity, $identity) | Should -BeFalse
|
|
|
|
|
[Security2.IdentityReference2]::op_Inequality($identity, $null) | Should -BeTrue
|
|
|
|
|
[Security2.IdentityReference2]::op_Inequality($null, $identity) | Should -BeTrue
|
|
|
|
|
[Security2.IdentityReference2]::op_Inequality($identity, $different) | Should -BeTrue
|
|
|
|
|
[Security2.IdentityReference2]::op_Inequality($identity, $same) | Should -BeFalse
|
|
|
|
|
$identity.GetHashCode() | Should -Be $same.GetHashCode()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
It 'Should round-trip native identities and the binary SID without changing the account' {
|
|
|
|
|
$account = $sid.Translate([System.Security.Principal.NTAccount])
|
|
|
|
|
$fromSid = [Security2.IdentityReference2]::op_Explicit($sid)
|
|
|
|
|
$fromAccount = [Security2.IdentityReference2]::op_Explicit($account)
|
|
|
|
|
|
|
|
|
|
$fromSid.Sid | Should -BeExactly 'S-1-1-0'
|
|
|
|
|
$fromAccount.Sid | Should -BeExactly $fromSid.Sid
|
|
|
|
|
([System.Security.Principal.SecurityIdentifier] $fromSid).Value | Should -BeExactly 'S-1-1-0'
|
|
|
|
|
([System.Security.Principal.NTAccount] $fromAccount).Value | Should -BeExactly $account.Value
|
|
|
|
|
$binarySid = New-Object -TypeName 'System.Security.Principal.SecurityIdentifier' -ArgumentList (
|
|
|
|
|
$fromSid.GetBinaryForm(), 0
|
|
|
|
|
)
|
|
|
|
|
$binarySid.Value | Should -BeExactly 'S-1-1-0'
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Describe 'Unrepresentable inheritance flags' {
|
|
|
|
|
It 'Should reject propagation flags without inheritance instead of inventing an AppliesTo value' {
|
|
|
|
|
$failure = $null
|
|
|
|
|
try {
|
|
|
|
|
$null = [Security2.FileSystemSecurity2]::ConvertToApplyTo('None', 'InheritOnly')
|
|
|
|
|
}
|
|
|
|
|
catch {
|
|
|
|
|
$failure = $_.Exception.GetBaseException()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$failure | Should -BeOfType [Security2.RightsConverionException]
|
|
|
|
|
$failure.Message | Should -BeExactly 'The combination of InheritanceFlags and PropagationFlags could not be translated'
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Describe 'Privilege output comparisons and formatting' {
|
|
|
|
|
It 'Should compare boxed and typed privilege values consistently without accepting an attributes enum' {
|
|
|
|
|
$values = @(Get-Privileges)
|
|
|
|
|
$values.Count | Should -BeGreaterThan 0
|
|
|
|
|
$first = $values[0].PSObject.BaseObject
|
|
|
|
|
$copy = $values[0].PSObject.BaseObject
|
|
|
|
|
# PowerShell prefers the typed overload; reflection selects the public boxed-object contract explicitly.
|
|
|
|
|
$equalsObject = [ProcessPrivileges.PrivilegeAndAttributes].GetMethod('Equals', [type[]] @([object]))
|
|
|
|
|
|
|
|
|
|
$equalsObject.Invoke($first, [object[]] @($copy)) | Should -BeTrue
|
|
|
|
|
$first.Equals($copy) | Should -BeTrue
|
|
|
|
|
[ProcessPrivileges.PrivilegeAndAttributes]::op_Equality($first, $copy) | Should -BeTrue
|
|
|
|
|
[ProcessPrivileges.PrivilegeAndAttributes]::op_Inequality($first, $copy) | Should -BeFalse
|
|
|
|
|
$first.GetHashCode() | Should -Be $copy.GetHashCode()
|
|
|
|
|
$equalsObject.Invoke($first, [object[]] @($null)) | Should -BeFalse
|
|
|
|
|
$equalsObject.Invoke($first, [object[]] @('Backup')) | Should -BeFalse
|
|
|
|
|
$equalsObject.Invoke($first, [object[]] @([ProcessPrivileges.PrivilegeAttributes]::Disabled)) | Should -BeFalse
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
It 'Should format the collection with one aligned privilege and attributes row per value' {
|
|
|
|
|
$control = New-Object -TypeName 'Security2.PrivilegeControl'
|
|
|
|
|
$values = $control.GetPrivileges()
|
|
|
|
|
$expectedWidth = ($values | ForEach-Object { $_.Privilege.ToString().Length } | Measure-Object -Maximum).Maximum
|
|
|
|
|
|
|
|
|
|
$text = $values.ToString()
|
|
|
|
|
|
|
|
|
|
$rows = @($text.TrimEnd("`r", "`n") -split '\r?\n')
|
|
|
|
|
$rows | Should -HaveCount $values.Count
|
|
|
|
|
for ($index = 0; $index -lt $values.Count; $index++) {
|
|
|
|
|
$value = $values[$index]
|
|
|
|
|
$rows[$index] | Should -BeExactly ('{0} => {1}' -f $value.Privilege.ToString().PadRight($expectedWidth),
|
|
|
|
|
$value.PrivilegeAttributes)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Describe 'Legacy effective-permission output objects' {
|
|
|
|
|
It 'Should retain a <Mask> mask and report the supplied path and identity without a native access check' -ForEach @(
|
|
|
|
|
@{ Mask = 0; ObjectName = 'Effective.txt' }
|
|
|
|
|
@{ Mask = 1; ObjectName = 'Effective.txt' }
|
|
|
|
|
@{ Mask = 3; ObjectName = $null }
|
|
|
|
|
) {
|
|
|
|
|
$path = if ($ObjectName) { Join-Path -Path $sandbox -ChildPath $ObjectName } else { $null }
|
|
|
|
|
$entry = New-Object -TypeName 'Security2.FileSystemEffectivePermissionEntry' -ArgumentList (
|
|
|
|
|
$identity, [uint32] $Mask, $path
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
$entry.Account.Sid | Should -BeExactly 'S-1-1-0'
|
|
|
|
|
$entry.AccessMask | Should -Be $Mask
|
|
|
|
|
[int] $entry.AccessRights | Should -Be $Mask
|
|
|
|
|
$entry.FullName | Should -BeExactly ([string] $path)
|
|
|
|
|
$entry.Name | Should -BeExactly $ObjectName
|
|
|
|
|
$entry.AccessAsString | Should -Not -BeNullOrEmpty
|
|
|
|
|
if ($Mask -eq 0) {
|
|
|
|
|
$entry.AccessAsString | Should -Be @('None')
|
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
$entry.AccessAsString | Should -Not -Contain 'None'
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Describe 'Simplified entry comparison branches' {
|
|
|
|
|
It 'Should distinguish identities, rights and types in <Kind> entries and keep equal hashes consistent' -ForEach @(
|
|
|
|
|
@{ Kind = 'access' }
|
|
|
|
|
@{ Kind = 'audit' }
|
|
|
|
|
) {
|
|
|
|
|
$typeName = if ($Kind -eq 'access') { 'Security2.SimpleFileSystemAccessRule' } else { 'Security2.SimpleFileSystemAuditRule' }
|
|
|
|
|
$arguments = @($objectPath, $identity, [Security2.FileSystemRights2]::Read)
|
|
|
|
|
if ($Kind -eq 'access') { $arguments += [System.Security.AccessControl.AccessControlType]::Allow }
|
|
|
|
|
$first = New-Object -TypeName $typeName -ArgumentList $arguments
|
|
|
|
|
$equal = New-Object -TypeName $typeName -ArgumentList $arguments
|
|
|
|
|
$arguments[1] = [Security2.IdentityReference2] 'S-1-5-32-546'
|
|
|
|
|
$differentIdentity = New-Object -TypeName $typeName -ArgumentList $arguments
|
|
|
|
|
$arguments[1] = $identity
|
|
|
|
|
$arguments[2] = [Security2.FileSystemRights2]::Delete
|
|
|
|
|
$differentRights = New-Object -TypeName $typeName -ArgumentList $arguments
|
|
|
|
|
|
|
|
|
|
$first.Equals($equal) | Should -BeTrue
|
|
|
|
|
$first.GetHashCode() | Should -Be $equal.GetHashCode()
|
|
|
|
|
$first.Equals($differentIdentity) | Should -BeFalse
|
|
|
|
|
$first.Equals($differentRights) | Should -BeFalse
|
|
|
|
|
$first.Equals($null) | Should -BeFalse
|
|
|
|
|
$equal.AccessControlType = 'Deny'
|
|
|
|
|
$first.Equals($equal) | Should -BeFalse
|
|
|
|
|
$first.Name | Should -BeExactly 'Rule.txt'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
It 'Should reduce the generic <Rights> mask in access entries' -ForEach @(
|
|
|
|
|
@{ Rights = 'GenericRead'; Expected = 'Read' }
|
|
|
|
|
@{ Rights = 'GenericWrite'; Expected = 'Write' }
|
|
|
|
|
@{ Rights = 'GenericExecute'; Expected = 'Read' }
|
|
|
|
|
@{ Rights = 'GenericAll'; Expected = 'Read, Write, Delete' }
|
|
|
|
|
) {
|
|
|
|
|
$entry = New-Object -TypeName 'Security2.SimpleFileSystemAccessRule' -ArgumentList (
|
|
|
|
|
$objectPath, $identity, [Security2.FileSystemRights2] $Rights,
|
|
|
|
|
[System.Security.AccessControl.AccessControlType]::Allow
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
$entry.AccessRights | Should -Be ([Security2.SimpleFileSystemAccessRights] $Expected)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
It 'Should convert an identity implicitly to its resolved display name and reject an unresolved name' {
|
|
|
|
|
$conversion = [Security2.IdentityReference2].GetMethods([Reflection.BindingFlags] 'Public, Static') |
|
|
|
|
|
Where-Object { $_.Name -eq 'op_Implicit' -and $_.ReturnType -eq [string] }
|
|
|
|
|
$conversion.Invoke($null, [object[]] @($identity.PSObject.BaseObject)) | Should -BeExactly $identity.ToString()
|
|
|
|
|
$unresolved = [Security2.IdentityReference2] 'S-1-5-21-1-2-3-1001'
|
|
|
|
|
$unresolved.Equals('Not-resolved') | Should -BeFalse
|
|
|
|
|
$identity.Equals($identity.AccountName) | Should -BeTrue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
It 'Should compare different privilege values as unequal' {
|
|
|
|
|
$constructor = [ProcessPrivileges.PrivilegeAndAttributes].GetConstructor(
|
|
|
|
|
[Reflection.BindingFlags] 'NonPublic, Instance', $null,
|
|
|
|
|
[type[]] @([ProcessPrivileges.Privilege], [ProcessPrivileges.PrivilegeAttributes]), $null
|
|
|
|
|
)
|
|
|
|
|
$first = $constructor.Invoke(@([ProcessPrivileges.Privilege]::Backup, [ProcessPrivileges.PrivilegeAttributes]::Disabled))
|
|
|
|
|
$different = $constructor.Invoke(@([ProcessPrivileges.Privilege]::Restore, [ProcessPrivileges.PrivilegeAttributes]::Disabled))
|
|
|
|
|
|
|
|
|
|
$first.Equals($different) | Should -BeFalse
|
|
|
|
|
[ProcessPrivileges.PrivilegeAndAttributes]::op_Inequality($first, $different) | Should -BeTrue
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Describe 'Access rule helpers that take a path' {
|
|
|
|
|
BeforeAll {
|
|
|
|
|
$allow = [System.Security.AccessControl.AccessControlType]::Allow
|
|
|
|
|
$noInheritance = [System.Security.AccessControl.InheritanceFlags]::None
|
|
|
|
|
$noPropagation = [System.Security.AccessControl.PropagationFlags]::None
|
|
|
|
|
$users = [Security2.IdentityReference2] 'S-1-5-32-545'
|
|
|
|
|
|
|
|
|
|
function Get-ExplicitEntries {
|
|
|
|
|
[Diagnostics.CodeAnalysis.SuppressMessageAttribute(
|
|
|
|
|
'PSUseSingularNouns', '', Justification = 'The helper returns the explicit entries of an item.'
|
|
|
|
|
)]
|
|
|
|
|
param ([string] $Path, [string] $Account = 'S-1-1-0')
|
|
|
|
|
|
|
|
|
|
$acl = Get-Acl -LiteralPath $Path
|
|
|
|
|
@($acl.GetAccessRules($true, $false, [System.Security.Principal.SecurityIdentifier]) |
|
|
|
|
|
Where-Object -FilterScript { $_.IdentityReference.Value -eq $Account })
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function New-AccountList {
|
|
|
|
|
[Diagnostics.CodeAnalysis.SuppressMessageAttribute(
|
|
|
|
|
'PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Test helper that only creates a list.'
|
|
|
|
|
)]
|
|
|
|
|
param ()
|
|
|
|
|
|
|
|
|
|
$accounts = New-Object -TypeName 'System.Collections.Generic.List[Security2.IdentityReference2]'
|
|
|
|
|
$accounts.Add($identity)
|
|
|
|
|
$accounts.Add($users)
|
|
|
|
|
, $accounts
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
It 'Should add an allow entry with Synchronize to a <Kind> by its path' -ForEach @(
|
|
|
|
|
@{ Kind = 'file'; Directory = $false }
|
|
|
|
|
@{ Kind = 'folder'; Directory = $true }
|
|
|
|
|
) {
|
|
|
|
|
$path = New-TestSandboxItem -Sandbox $sandbox -Name 'AddByPath' -Directory:$Directory
|
|
|
|
|
|
|
|
|
|
$rule = [Security2.FileSystemAccessRule2]::AddFileSystemAccessRule(
|
|
|
|
|
$path, $identity, [Security2.FileSystemRights2]::ReadData, $allow, $noInheritance, $noPropagation
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
$rule.Account.Sid | Should -BeExactly 'S-1-1-0'
|
|
|
|
|
$entries = @(Get-ExplicitEntries -Path $path)
|
|
|
|
|
$entries | Should -HaveCount 1
|
|
|
|
|
$entries[0].AccessControlType | Should -Be 'Allow'
|
|
|
|
|
$entries[0].FileSystemRights | Should -Be ([System.Security.AccessControl.FileSystemRights] 'ReadData, Synchronize')
|
|
|
|
|
}
|
|
|
|
|
|
fix(security2): make the path overloads of AddFileSystemAccessRule and AddFileSystemAuditRule write at once
AddFileSystemAccessRule(string, List<IdentityReference2>, ...) and
AddFileSystemAuditRule(string, List<IdentityReference2>, ...) were
iterator methods (yield return), so nothing happened until the caller
enumerated the result: called as a statement, or assigned to a variable
that the caller never reads, they changed nothing. Their siblings, the
overloads for FileSystemInfo and FileSystemSecurity2 with a list, and
the single-account overloads, already built a list and wrote at once.
Both overloads now build the list and add the entries before returning,
like their siblings; a call that is never enumerated now adds the
entries just the same. An exception that adding an entry raises changes
only in timing: it is now thrown by the call itself, not by the later
enumeration of its result. The returned collection still holds one rule
per account, in the order of the account list, for both a file path and
a folder path.
Public signatures are unchanged.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
2 weeks ago
|
|
|
# Before 5.0.0, the overload that takes a path was an iterator (yield return) that changed nothing until the
|
|
|
|
|
# caller enumerated the result, so a call as a statement, or an assignment that the caller never reads, wrote
|
|
|
|
|
# nothing; the overloads that take an item or a FileSystemSecurity2 were never lazy. It now writes at once, like
|
|
|
|
|
# its siblings, so the entries exist before $pending is ever touched.
|
|
|
|
|
It 'Should add the entries of several accounts to a <Kind> by its path at once, before the result is ever enumerated' -ForEach @(
|
|
|
|
|
@{ Kind = 'file'; Directory = $false }
|
|
|
|
|
@{ Kind = 'folder'; Directory = $true }
|
|
|
|
|
) {
|
|
|
|
|
$path = New-TestSandboxItem -Sandbox $sandbox -Name 'AddListByPath' -Directory:$Directory
|
|
|
|
|
$accounts = New-AccountList
|
|
|
|
|
|
|
|
|
|
$pending = [Security2.FileSystemAccessRule2]::AddFileSystemAccessRule(
|
|
|
|
|
$path, $accounts, [Security2.FileSystemRights2]::ReadData, $allow, $noInheritance, $noPropagation
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
@(Get-ExplicitEntries -Path $path) | Should -HaveCount 1
|
|
|
|
|
@(Get-ExplicitEntries -Path $path -Account 'S-1-5-32-545') | Should -HaveCount 1
|
fix(security2): make the path overloads of AddFileSystemAccessRule and AddFileSystemAuditRule write at once
AddFileSystemAccessRule(string, List<IdentityReference2>, ...) and
AddFileSystemAuditRule(string, List<IdentityReference2>, ...) were
iterator methods (yield return), so nothing happened until the caller
enumerated the result: called as a statement, or assigned to a variable
that the caller never reads, they changed nothing. Their siblings, the
overloads for FileSystemInfo and FileSystemSecurity2 with a list, and
the single-account overloads, already built a list and wrote at once.
Both overloads now build the list and add the entries before returning,
like their siblings; a call that is never enumerated now adds the
entries just the same. An exception that adding an entry raises changes
only in timing: it is now thrown by the call itself, not by the later
enumeration of its result. The returned collection still holds one rule
per account, in the order of the account list, for both a file path and
a folder path.
Public signatures are unchanged.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
2 weeks ago
|
|
|
|
|
|
|
|
$added = @($pending)
|
|
|
|
|
$added | Should -HaveCount 2
|
|
|
|
|
$added[0].Account.Sid | Should -BeExactly $identity.Sid
|
|
|
|
|
$added[1].Account.Sid | Should -BeExactly $users.Sid
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
It 'Should add the deny entries of several accounts without Synchronize when the result is enumerated' {
|
|
|
|
|
$path = New-TestSandboxItem -Sandbox $sandbox -Name 'DenyListByPath'
|
|
|
|
|
$accounts = New-AccountList
|
|
|
|
|
$deny = [System.Security.AccessControl.AccessControlType]::Deny
|
|
|
|
|
|
|
|
|
|
@([Security2.FileSystemAccessRule2]::AddFileSystemAccessRule(
|
|
|
|
|
$path, $accounts, [Security2.FileSystemRights2]::ReadData, $deny, $noInheritance, $noPropagation
|
|
|
|
|
)) | Should -HaveCount 2
|
|
|
|
|
|
|
|
|
|
foreach ($account in 'S-1-1-0', 'S-1-5-32-545') {
|
|
|
|
|
$entries = @(Get-ExplicitEntries -Path $path -Account $account)
|
|
|
|
|
$entries | Should -HaveCount 1
|
|
|
|
|
$entries[0].AccessControlType | Should -Be 'Deny'
|
|
|
|
|
$entries[0].FileSystemRights | Should -Be ([System.Security.AccessControl.FileSystemRights]::ReadData)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
It 'Should add and remove a deny entry by its path without adding Synchronize' {
|
|
|
|
|
$path = New-TestSandboxItem -Sandbox $sandbox -Name 'DenyByPath'
|
|
|
|
|
$deny = [System.Security.AccessControl.AccessControlType]::Deny
|
|
|
|
|
|
|
|
|
|
[void] [Security2.FileSystemAccessRule2]::AddFileSystemAccessRule(
|
|
|
|
|
$path, $identity, [Security2.FileSystemRights2]::ReadData, $deny, $noInheritance, $noPropagation
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
$entries = @(Get-ExplicitEntries -Path $path)
|
|
|
|
|
$entries | Should -HaveCount 1
|
|
|
|
|
$entries[0].AccessControlType | Should -Be 'Deny'
|
|
|
|
|
$entries[0].FileSystemRights | Should -Be ([System.Security.AccessControl.FileSystemRights]::ReadData)
|
|
|
|
|
|
|
|
|
|
[Security2.FileSystemAccessRule2]::RemoveFileSystemAccessRule(
|
|
|
|
|
$path, $identity, [Security2.FileSystemRights2]::ReadData, $deny, $noInheritance, $noPropagation
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
@(Get-ExplicitEntries -Path $path) | Should -BeNullOrEmpty
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
It 'Should return no entries for an empty DACL when the sources of inherited entries are requested' {
|
|
|
|
|
$path = New-TestSandboxItem -Sandbox $sandbox -Name 'EmptyDacl'
|
|
|
|
|
Clear-NTFSAccess -Path $path -DisableInheritance -ErrorAction Stop
|
|
|
|
|
|
|
|
|
|
$rules = @([Security2.FileSystemAccessRule2]::GetFileSystemAccessRules($path, $true, $true, $true))
|
|
|
|
|
|
|
|
|
|
$rules | Should -BeNullOrEmpty
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
It 'Should add the entry of a rule that carries its path' {
|
|
|
|
|
$path = New-TestSandboxItem -Sandbox $sandbox -Name 'AddRule'
|
|
|
|
|
$raw = New-Object -TypeName 'System.Security.AccessControl.FileSystemAccessRule' -ArgumentList (
|
|
|
|
|
$sid, [System.Security.AccessControl.FileSystemRights]::ReadData, $allow
|
|
|
|
|
)
|
|
|
|
|
$rule = New-Object -TypeName 'Security2.FileSystemAccessRule2' -ArgumentList $raw, $path
|
|
|
|
|
|
|
|
|
|
[Security2.FileSystemAccessRule2]::AddFileSystemAccessRule($rule)
|
|
|
|
|
|
|
|
|
|
@(Get-ExplicitEntries -Path $path) | Should -HaveCount 1
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# RemoveSpecific removes only an entry that matches exactly; without it, Windows removes the named rights from the
|
|
|
|
|
# matching entry.
|
|
|
|
|
It 'Should remove only an exactly matching entry of a <Kind> with removeSpecific and the named rights without it' -ForEach @(
|
|
|
|
|
@{ Kind = 'file'; Directory = $false }
|
|
|
|
|
@{ Kind = 'folder'; Directory = $true }
|
|
|
|
|
) {
|
|
|
|
|
$path = New-TestSandboxItem -Sandbox $sandbox -Name 'RemoveByPath' -Directory:$Directory
|
|
|
|
|
[void] [Security2.FileSystemAccessRule2]::AddFileSystemAccessRule(
|
|
|
|
|
$path, $identity, [Security2.FileSystemRights2] 'ReadData, WriteData', $allow, $noInheritance, $noPropagation
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
[Security2.FileSystemAccessRule2]::RemoveFileSystemAccessRule(
|
|
|
|
|
$path, $identity, [Security2.FileSystemRights2]::ReadData, $allow, $noInheritance, $noPropagation, $true
|
|
|
|
|
)
|
|
|
|
|
@(Get-ExplicitEntries -Path $path)[0].FileSystemRights |
|
|
|
|
|
Should -Be ([System.Security.AccessControl.FileSystemRights] 'ReadData, WriteData, Synchronize')
|
|
|
|
|
|
|
|
|
|
[Security2.FileSystemAccessRule2]::RemoveFileSystemAccessRule(
|
|
|
|
|
$path, $identity, [Security2.FileSystemRights2]::ReadData, $allow, $noInheritance, $noPropagation, $false
|
|
|
|
|
)
|
|
|
|
|
@(Get-ExplicitEntries -Path $path)[0].FileSystemRights |
|
|
|
|
|
Should -Be ([System.Security.AccessControl.FileSystemRights] 'WriteData, Synchronize')
|
|
|
|
|
|
|
|
|
|
[Security2.FileSystemAccessRule2]::RemoveFileSystemAccessRule(
|
|
|
|
|
$path, $identity, [Security2.FileSystemRights2]::WriteData, $allow, $noInheritance, $noPropagation, $true
|
|
|
|
|
)
|
|
|
|
|
@(Get-ExplicitEntries -Path $path) | Should -BeNullOrEmpty
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
It 'Should remove the entries of several accounts of a <Kind> by path' -ForEach @(
|
|
|
|
|
@{ Kind = 'file'; Directory = $false }
|
|
|
|
|
@{ Kind = 'folder'; Directory = $true }
|
|
|
|
|
) {
|
|
|
|
|
$path = New-TestSandboxItem -Sandbox $sandbox -Name 'RemoveListByPath' -Directory:$Directory
|
|
|
|
|
$accounts = New-AccountList
|
|
|
|
|
@([Security2.FileSystemAccessRule2]::AddFileSystemAccessRule(
|
|
|
|
|
$path, $accounts, [Security2.FileSystemRights2]::ReadData, $allow, $noInheritance, $noPropagation
|
|
|
|
|
)) | Should -HaveCount 2
|
|
|
|
|
|
|
|
|
|
[Security2.FileSystemAccessRule2]::RemoveFileSystemAccessRule(
|
|
|
|
|
$path, $accounts, [Security2.FileSystemRights2]::ReadData, $allow, $noInheritance, $noPropagation, $false
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
@(Get-ExplicitEntries -Path $path) | Should -BeNullOrEmpty
|
|
|
|
|
@(Get-ExplicitEntries -Path $path -Account 'S-1-5-32-545') | Should -BeNullOrEmpty
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
It 'Should remove the entry that a rule object describes from an item' {
|
|
|
|
|
$path = New-TestSandboxItem -Sandbox $sandbox -Name 'RemoveRuleObject'
|
|
|
|
|
[void] [Security2.FileSystemAccessRule2]::AddFileSystemAccessRule(
|
|
|
|
|
$path, $identity, [Security2.FileSystemRights2]::ReadData, $allow, $noInheritance, $noPropagation
|
|
|
|
|
)
|
|
|
|
|
$raw = New-Object -TypeName 'System.Security.AccessControl.FileSystemAccessRule' -ArgumentList (
|
|
|
|
|
$sid, [System.Security.AccessControl.FileSystemRights]::ReadData, $allow
|
|
|
|
|
)
|
|
|
|
|
$item = New-Object -TypeName 'Alphaleonis.Win32.Filesystem.FileInfo' -ArgumentList $path
|
|
|
|
|
|
|
|
|
|
[Security2.FileSystemAccessRule2]::RemoveFileSystemAccessRule($item, $raw, $false)
|
|
|
|
|
|
|
|
|
|
@(Get-ExplicitEntries -Path $path) | Should -BeNullOrEmpty
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
It 'Should return the explicit entries of a folder, and its inherited ones when asked, by its path' {
|
|
|
|
|
$path = New-TestSandboxItem -Sandbox $sandbox -Name 'GetByPath' -Directory
|
|
|
|
|
[void] [Security2.FileSystemAccessRule2]::AddFileSystemAccessRule(
|
|
|
|
|
$path, $identity, [Security2.FileSystemRights2]::ReadData, $allow, $noInheritance, $noPropagation
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
$explicit = @([Security2.FileSystemAccessRule2]::GetFileSystemAccessRules($path, $true, $false, $false))
|
|
|
|
|
$all = @([Security2.FileSystemAccessRule2]::GetFileSystemAccessRules($path, $true, $true, $true))
|
|
|
|
|
|
|
|
|
|
$explicit | Should -HaveCount 1
|
|
|
|
|
$explicit[0].Account.Sid | Should -BeExactly 'S-1-1-0'
|
|
|
|
|
$all.Count | Should -BeGreaterThan 1
|
|
|
|
|
@($all | Where-Object -FilterScript { $_.Account.Sid -eq 'S-1-1-0' }) | Should -HaveCount 1
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Describe 'Audit rule helpers that take a path' -Skip:(-not $holdsSecurityPrivilege) {
|
|
|
|
|
BeforeAll {
|
|
|
|
|
$success = [System.Security.AccessControl.AuditFlags]::Success
|
|
|
|
|
$noInheritance = [System.Security.AccessControl.InheritanceFlags]::None
|
|
|
|
|
$noPropagation = [System.Security.AccessControl.PropagationFlags]::None
|
|
|
|
|
$users = [Security2.IdentityReference2] 'S-1-5-32-545'
|
|
|
|
|
|
|
|
|
|
function Get-AuditEntries {
|
|
|
|
|
[Diagnostics.CodeAnalysis.SuppressMessageAttribute(
|
|
|
|
|
'PSUseSingularNouns', '', Justification = 'The helper returns the audit entries of an item.'
|
|
|
|
|
)]
|
|
|
|
|
param ([string] $Path, [string] $Account = 'S-1-1-0')
|
|
|
|
|
|
|
|
|
|
$descriptor = Get-NTFSSecurityDescriptor -Path $Path
|
|
|
|
|
@($descriptor.SecurityDescriptor.GetAuditRules($true, $false, [System.Security.Principal.SecurityIdentifier]) |
|
|
|
|
|
Where-Object -FilterScript { $_.IdentityReference.Value -eq $Account })
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
It 'Should add an audit entry to a <Kind> by its path' -ForEach @(
|
|
|
|
|
@{ Kind = 'file'; Directory = $false }
|
|
|
|
|
@{ Kind = 'folder'; Directory = $true }
|
|
|
|
|
) {
|
|
|
|
|
$path = New-TestSandboxItem -Sandbox $sandbox -Name 'AuditByPath' -Directory:$Directory
|
|
|
|
|
|
|
|
|
|
$rule = [Security2.FileSystemAuditRule2]::AddFileSystemAuditRule(
|
|
|
|
|
$path, $identity, [Security2.FileSystemRights2]::Delete, $success, $noInheritance, $noPropagation
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
$rule.Account.Sid | Should -BeExactly 'S-1-1-0'
|
|
|
|
|
$entries = @(Get-AuditEntries -Path $path)
|
|
|
|
|
$entries | Should -HaveCount 1
|
|
|
|
|
$entries[0].AuditFlags | Should -Be 'Success'
|
|
|
|
|
$entries[0].FileSystemRights | Should -Be ([System.Security.AccessControl.FileSystemRights]::Delete)
|
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>
3 weeks ago
|
|
|
$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
|
|
|
|
|
}
|
|
|
|
|
|
fix(security2): make the path overloads of AddFileSystemAccessRule and AddFileSystemAuditRule write at once
AddFileSystemAccessRule(string, List<IdentityReference2>, ...) and
AddFileSystemAuditRule(string, List<IdentityReference2>, ...) were
iterator methods (yield return), so nothing happened until the caller
enumerated the result: called as a statement, or assigned to a variable
that the caller never reads, they changed nothing. Their siblings, the
overloads for FileSystemInfo and FileSystemSecurity2 with a list, and
the single-account overloads, already built a list and wrote at once.
Both overloads now build the list and add the entries before returning,
like their siblings; a call that is never enumerated now adds the
entries just the same. An exception that adding an entry raises changes
only in timing: it is now thrown by the call itself, not by the later
enumeration of its result. The returned collection still holds one rule
per account, in the order of the account list, for both a file path and
a folder path.
Public signatures are unchanged.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
2 weeks ago
|
|
|
# Before 5.0.0, the overload that takes a path was an iterator (yield return) that changed nothing until the
|
|
|
|
|
# caller enumerated the result; it now writes at once, like its siblings, so the entries exist before $pending is
|
|
|
|
|
# ever touched. This overload needs the Security privilege like its neighbors, so the whole Describe skips without it.
|
|
|
|
|
It 'Should add the entries of several accounts to a <Kind> by its path at once, before the result is ever enumerated, and remove them again' -ForEach @(
|
|
|
|
|
@{ Kind = 'file'; Directory = $false }
|
|
|
|
|
@{ Kind = 'folder'; Directory = $true }
|
|
|
|
|
) {
|
|
|
|
|
$path = New-TestSandboxItem -Sandbox $sandbox -Name 'AuditListByPath' -Directory:$Directory
|
|
|
|
|
$accounts = New-Object -TypeName 'System.Collections.Generic.List[Security2.IdentityReference2]'
|
|
|
|
|
$accounts.Add($identity)
|
|
|
|
|
$accounts.Add($users)
|
|
|
|
|
|
|
|
|
|
$pending = [Security2.FileSystemAuditRule2]::AddFileSystemAuditRule(
|
|
|
|
|
$path, $accounts, [Security2.FileSystemRights2]::Delete, $success, $noInheritance, $noPropagation
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
@(Get-AuditEntries -Path $path) | Should -HaveCount 1
|
|
|
|
|
@(Get-AuditEntries -Path $path -Account 'S-1-5-32-545') | Should -HaveCount 1
|
fix(security2): make the path overloads of AddFileSystemAccessRule and AddFileSystemAuditRule write at once
AddFileSystemAccessRule(string, List<IdentityReference2>, ...) and
AddFileSystemAuditRule(string, List<IdentityReference2>, ...) were
iterator methods (yield return), so nothing happened until the caller
enumerated the result: called as a statement, or assigned to a variable
that the caller never reads, they changed nothing. Their siblings, the
overloads for FileSystemInfo and FileSystemSecurity2 with a list, and
the single-account overloads, already built a list and wrote at once.
Both overloads now build the list and add the entries before returning,
like their siblings; a call that is never enumerated now adds the
entries just the same. An exception that adding an entry raises changes
only in timing: it is now thrown by the call itself, not by the later
enumeration of its result. The returned collection still holds one rule
per account, in the order of the account list, for both a file path and
a folder path.
Public signatures are unchanged.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
2 weeks ago
|
|
|
$added = @($pending)
|
|
|
|
|
$added | Should -HaveCount 2
|
|
|
|
|
$added[0].Account.Sid | Should -BeExactly $identity.Sid
|
|
|
|
|
$added[1].Account.Sid | Should -BeExactly $users.Sid
|
|
|
|
|
|
|
|
|
|
[Security2.FileSystemAuditRule2]::RemoveFileSystemAuditRule(
|
|
|
|
|
$path, $identity, [Security2.FileSystemRights2]::Delete, $success, $noInheritance, $noPropagation, $true
|
|
|
|
|
)
|
|
|
|
|
@(Get-AuditEntries -Path $path) | Should -BeNullOrEmpty
|
|
|
|
|
@(Get-AuditEntries -Path $path -Account 'S-1-5-32-545') | Should -HaveCount 1
|
|
|
|
|
[Security2.FileSystemAuditRule2]::RemoveFileSystemAuditRule(
|
|
|
|
|
$path, $users, [Security2.FileSystemRights2]::Delete, $success, $noInheritance, $noPropagation, $false
|
|
|
|
|
)
|
|
|
|
|
@(Get-AuditEntries -Path $path -Account 'S-1-5-32-545') | Should -BeNullOrEmpty
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
It 'Should name the item of a rule, replay it, read it by path, and remove it by its rule object' {
|
|
|
|
|
$path = New-TestSandboxItem -Sandbox $sandbox -Name 'AuditReplay'
|
|
|
|
|
$raw = New-Object -TypeName 'System.Security.AccessControl.FileSystemAuditRule' -ArgumentList (
|
|
|
|
|
$sid, [System.Security.AccessControl.FileSystemRights]::Delete, $success
|
|
|
|
|
)
|
|
|
|
|
$item = New-Object -TypeName 'Alphaleonis.Win32.Filesystem.FileInfo' -ArgumentList $path
|
|
|
|
|
$rule = New-Object -TypeName 'Security2.FileSystemAuditRule2' -ArgumentList $raw, $item
|
|
|
|
|
$rule.FullName | Should -BeExactly $path
|
|
|
|
|
$rule.Name | Should -BeExactly (Split-Path -Path $path -Leaf)
|
|
|
|
|
|
|
|
|
|
[Security2.FileSystemAuditRule2]::AddFileSystemAuditRule($rule)
|
|
|
|
|
|
|
|
|
|
@(Get-AuditEntries -Path $path) | Should -HaveCount 1
|
|
|
|
|
$found = @([Security2.FileSystemAuditRule2]::GetFileSystemAuditRules($path, $true, $true))
|
|
|
|
|
$found | Should -HaveCount 1
|
|
|
|
|
$found[0].Account.Sid | Should -BeExactly 'S-1-1-0'
|
|
|
|
|
[Security2.FileSystemAuditRule2]::RemoveFileSystemAuditRule($item, $raw)
|
|
|
|
|
@(Get-AuditEntries -Path $path) | Should -BeNullOrEmpty
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
It 'Should remove a rule object from an item without audit entries and change nothing' {
|
|
|
|
|
$path = New-TestSandboxItem -Sandbox $sandbox -Name 'AuditNothing'
|
|
|
|
|
$raw = New-Object -TypeName 'System.Security.AccessControl.FileSystemAuditRule' -ArgumentList (
|
|
|
|
|
$sid, [System.Security.AccessControl.FileSystemRights]::Delete, $success
|
|
|
|
|
)
|
|
|
|
|
$item = New-Object -TypeName 'Alphaleonis.Win32.Filesystem.FileInfo' -ArgumentList $path
|
|
|
|
|
$before = (Get-Acl -LiteralPath $path).Sddl
|
|
|
|
|
|
|
|
|
|
[Security2.FileSystemAuditRule2]::RemoveFileSystemAuditRule($item, $raw)
|
|
|
|
|
|
|
|
|
|
(Get-Acl -LiteralPath $path).Sddl | Should -BeExactly $before
|
|
|
|
|
@(Get-AuditEntries -Path $path) | Should -BeNullOrEmpty
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Describe 'Inheritance helpers that take a path' {
|
|
|
|
|
It 'Should block and restore the access inheritance of a <Kind> by its path' -ForEach @(
|
|
|
|
|
@{ Kind = 'file'; Directory = $false; Remove = $false }
|
|
|
|
|
@{ Kind = 'folder'; Directory = $true; Remove = $true }
|
|
|
|
|
) {
|
|
|
|
|
$path = New-TestSandboxItem -Sandbox $sandbox -Name 'InheritanceByPath' -Directory:$Directory
|
|
|
|
|
$inherited = @((Get-Acl -LiteralPath $path).GetAccessRules($false, $true, [System.Security.Principal.SecurityIdentifier])).Count
|
|
|
|
|
$inherited | Should -BeGreaterThan 0
|
|
|
|
|
|
|
|
|
|
[Security2.FileSystemInheritanceInfo]::DisableAccessInheritance($path, $Remove)
|
|
|
|
|
|
|
|
|
|
$acl = Get-Acl -LiteralPath $path
|
|
|
|
|
$acl.AreAccessRulesProtected | Should -BeTrue
|
|
|
|
|
@($acl.GetAccessRules($true, $true, [System.Security.Principal.SecurityIdentifier])).Count |
|
|
|
|
|
Should -Be $(if ($Remove) { 0 } else { $inherited })
|
|
|
|
|
|
|
|
|
|
[Security2.FileSystemInheritanceInfo]::EnableAccessInheritance($path, $Remove)
|
|
|
|
|
|
|
|
|
|
$acl = Get-Acl -LiteralPath $path
|
|
|
|
|
$acl.AreAccessRulesProtected | Should -BeFalse
|
|
|
|
|
@($acl.GetAccessRules($false, $true, [System.Security.Principal.SecurityIdentifier])).Count | Should -Be $inherited
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
It 'Should read the access inheritance of a file by its path and keep what the caller sets on the result' {
|
|
|
|
|
$path = New-TestSandboxItem -Sandbox $sandbox -Name 'InheritanceInfo'
|
|
|
|
|
|
|
|
|
|
$info = [Security2.FileSystemInheritanceInfo]::GetFileSystemInheritanceInfo($path)
|
|
|
|
|
|
|
|
|
|
$info.AccessInheritanceEnabled | Should -BeTrue
|
|
|
|
|
$info.Item.FullName | Should -BeExactly $path
|
|
|
|
|
$info.AccessInheritanceEnabled = $false
|
|
|
|
|
$info.AuditInheritanceEnabled = $true
|
|
|
|
|
$info.Item = New-Object -TypeName 'Alphaleonis.Win32.Filesystem.FileInfo' -ArgumentList $path
|
|
|
|
|
$info.AccessInheritanceEnabled | Should -BeFalse
|
|
|
|
|
$info.AuditInheritanceEnabled | Should -BeTrue
|
|
|
|
|
(Get-Acl -LiteralPath $path).AreAccessRulesProtected | Should -BeFalse
|
|
|
|
|
}
|
|
|
|
|
|
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>
3 weeks ago
|
|
|
# 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 }
|
|
|
|
|
) {
|
|
|
|
|
$path = New-TestSandboxItem -Sandbox $sandbox -Name 'AuditInheritanceByPath' -Directory:$Directory
|
|
|
|
|
(Get-NTFSInheritance -Path $path).AuditInheritanceEnabled | Should -BeTrue
|
|
|
|
|
|
|
|
|
|
[Security2.FileSystemInheritanceInfo]::DisableAuditInheritance($path, $false)
|
|
|
|
|
|
|
|
|
|
(Get-NTFSInheritance -Path $path).AuditInheritanceEnabled | Should -BeFalse
|
|
|
|
|
|
|
|
|
|
[Security2.FileSystemInheritanceInfo]::EnableAuditInheritance($path, $false)
|
|
|
|
|
|
|
|
|
|
(Get-NTFSInheritance -Path $path).AuditInheritanceEnabled | Should -BeTrue
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Describe 'Owner and descriptor objects' {
|
|
|
|
|
It 'Should name the item and the account of an owner object' {
|
|
|
|
|
$path = New-TestSandboxItem -Sandbox $sandbox -Name 'OwnerObject'
|
|
|
|
|
|
|
|
|
|
$owner = Get-NTFSOwner -Path $path
|
|
|
|
|
|
|
|
|
|
$owner.Item.FullName | Should -BeExactly $path
|
|
|
|
|
$owner.FullName | Should -BeExactly $path
|
|
|
|
|
$owner.Account.Sid | Should -BeExactly $owner.Owner.Sid
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
It 'Should read the owner of a drive root also for a lowercase drive letter' {
|
|
|
|
|
$root = [IO.Path]::GetPathRoot($sandbox)
|
|
|
|
|
$expected = (Get-NTFSOwner -Path $root).Owner.Sid
|
|
|
|
|
|
|
|
|
|
$owner = Get-NTFSOwner -Path $root.ToLowerInvariant()
|
|
|
|
|
|
|
|
|
|
$owner.Owner.Sid | Should -BeExactly $expected
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
It 'Should name the item of a descriptor and write it to a folder by its path' {
|
|
|
|
|
$source = New-TestSandboxItem -Sandbox $sandbox -Name 'DescriptorSource' -Directory
|
|
|
|
|
$target = New-TestSandboxItem -Sandbox $sandbox -Name 'DescriptorTarget' -Directory
|
|
|
|
|
Add-NTFSAccess -Path $source -Account 'S-1-1-0' -AccessRights ReadData -AppliesTo ThisFolderOnly
|
|
|
|
|
$descriptor = Get-NTFSSecurityDescriptor -Path $source
|
|
|
|
|
|
|
|
|
|
$descriptor.Name | Should -BeExactly (Split-Path -Path $source -Leaf)
|
|
|
|
|
$descriptor.Write([string] $target)
|
|
|
|
|
|
|
|
|
|
@((Get-Acl -LiteralPath $target).GetAccessRules($true, $false, [System.Security.Principal.SecurityIdentifier]) |
|
|
|
|
|
Where-Object -FilterScript { $_.IdentityReference.Value -eq 'S-1-1-0' }) | Should -HaveCount 1
|
|
|
|
|
}
|
|
|
|
|
|
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>
3 weeks ago
|
|
|
# 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'))
|
|
|
|
|
Assert-TestSandboxPath -Sandbox $sandbox -Path $missing
|
|
|
|
|
$descriptor = Get-NTFSSecurityDescriptor -Path $source
|
|
|
|
|
|
|
|
|
|
$failure = { $descriptor.Write($missing) } | Should -Throw -PassThru
|
|
|
|
|
|
|
|
|
|
$failure.Exception.GetBaseException() | Should -BeOfType [System.IO.FileNotFoundException]
|
|
|
|
|
$failure.Exception.GetBaseException().FileName | Should -BeExactly $missing
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
It 'Should leave both flags unset for an AppliesTo value that no case names' {
|
|
|
|
|
$inheritance = [System.Security.AccessControl.InheritanceFlags]::ContainerInherit
|
|
|
|
|
$propagation = [System.Security.AccessControl.PropagationFlags]::InheritOnly
|
|
|
|
|
|
|
|
|
|
[Security2.FileSystemSecurity2]::ConvertToFileSystemFlags(
|
|
|
|
|
[Enum]::ToObject([Security2.ApplyTo], 99), [ref] $inheritance, [ref] $propagation
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
$inheritance | Should -Be 'None'
|
|
|
|
|
$propagation | Should -Be 'None'
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Before 5.0.0, both conversions ignored the descriptor that was passed and returned a FileSystemSecurity2 that wrapped
|
|
|
|
|
# a FileInfo or a DirectoryInfo built from an empty path, which AlphaFS rejects, so the conversion always threw. The
|
|
|
|
|
# object that the fixed conversion returns has no item: FullName and Name, which have no path to report, are null,
|
|
|
|
|
# which reads most naturally as "nothing to name".
|
|
|
|
|
Describe 'The FileSystemSecurity2 conversions from FileSecurity and DirectorySecurity' {
|
|
|
|
|
BeforeAll {
|
|
|
|
|
function New-RawFileSystemSecurity {
|
|
|
|
|
[Diagnostics.CodeAnalysis.SuppressMessageAttribute(
|
|
|
|
|
'PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Builds an in-memory descriptor only.'
|
|
|
|
|
)]
|
|
|
|
|
param ([string] $Kind)
|
|
|
|
|
|
|
|
|
|
$raw = New-Object -TypeName "System.Security.AccessControl.$Kind"
|
|
|
|
|
$rule = New-Object -TypeName 'System.Security.AccessControl.FileSystemAccessRule' -ArgumentList (
|
|
|
|
|
$sid, [System.Security.AccessControl.FileSystemRights]::ReadData,
|
|
|
|
|
[System.Security.AccessControl.AccessControlType]::Allow
|
|
|
|
|
)
|
|
|
|
|
$raw.AddAccessRule($rule)
|
|
|
|
|
$raw
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
It 'Should wrap the <Kind> descriptor that was passed, report IsFile as <IsFile>, and round-trip the same instance' -ForEach @(
|
|
|
|
|
@{ Kind = 'FileSecurity'; IsFile = $true }
|
|
|
|
|
@{ Kind = 'DirectorySecurity'; IsFile = $false }
|
|
|
|
|
) {
|
|
|
|
|
$raw = New-RawFileSystemSecurity -Kind $Kind
|
|
|
|
|
$targetType = [type] "System.Security.AccessControl.$Kind"
|
|
|
|
|
|
|
|
|
|
$wrapped = [Security2.FileSystemSecurity2] $raw
|
|
|
|
|
|
|
|
|
|
[object]::ReferenceEquals($wrapped.SecurityDescriptor, $raw) | Should -BeTrue
|
|
|
|
|
$wrapped.IsFile | Should -Be $IsFile
|
|
|
|
|
[object]::ReferenceEquals(($wrapped -as $targetType), $raw) | Should -BeTrue
|
|
|
|
|
$wrapped.FullName | Should -Be $null
|
|
|
|
|
$wrapped.Name | Should -Be $null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
It 'Should compare two <Kind> conversions of the same descriptor as equal, and of different descriptors as unequal' -ForEach @(
|
|
|
|
|
@{ Kind = 'FileSecurity' }
|
|
|
|
|
@{ Kind = 'DirectorySecurity' }
|
|
|
|
|
) {
|
|
|
|
|
$raw = New-RawFileSystemSecurity -Kind $Kind
|
|
|
|
|
|
|
|
|
|
$first = [Security2.FileSystemSecurity2] $raw
|
|
|
|
|
$second = [Security2.FileSystemSecurity2] $raw
|
|
|
|
|
$other = [Security2.FileSystemSecurity2] (New-RawFileSystemSecurity -Kind $Kind)
|
|
|
|
|
|
|
|
|
|
$first.Equals($second) | Should -BeTrue
|
|
|
|
|
$first.GetHashCode() | Should -Be $second.GetHashCode()
|
|
|
|
|
$first.Equals($other) | Should -BeFalse
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
It 'Should throw an InvalidOperationException, not a NullReferenceException, when a <Kind> without an item is written' -ForEach @(
|
|
|
|
|
@{ Kind = 'FileSecurity' }
|
|
|
|
|
@{ Kind = 'DirectorySecurity' }
|
|
|
|
|
) {
|
|
|
|
|
$wrapped = [Security2.FileSystemSecurity2] (New-RawFileSystemSecurity -Kind $Kind)
|
|
|
|
|
|
|
|
|
|
$failure = { $wrapped.Write() } | Should -Throw -PassThru
|
|
|
|
|
|
|
|
|
|
$failure.Exception.GetBaseException() | Should -BeOfType [System.InvalidOperationException]
|
|
|
|
|
$failure.Exception.GetBaseException().Message | Should -BeLike '*item*'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
It 'Should write a <Kind> without an item once the caller sets Item' -ForEach @(
|
|
|
|
|
@{ Kind = 'FileSecurity'; Directory = $false }
|
|
|
|
|
@{ Kind = 'DirectorySecurity'; Directory = $true }
|
|
|
|
|
) {
|
|
|
|
|
$path = New-TestSandboxItem -Sandbox $sandbox -Name 'ConvertedWriteItem' -Directory:$Directory
|
|
|
|
|
$wrapped = [Security2.FileSystemSecurity2] (New-RawFileSystemSecurity -Kind $Kind)
|
|
|
|
|
$wrapped.Item = Get-Item2 -Path $path
|
|
|
|
|
|
|
|
|
|
$wrapped.Write()
|
|
|
|
|
|
|
|
|
|
@((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 write a <Kind> without an item directly to a path' -ForEach @(
|
|
|
|
|
@{ Kind = 'FileSecurity'; Directory = $false }
|
|
|
|
|
@{ Kind = 'DirectorySecurity'; Directory = $true }
|
|
|
|
|
) {
|
|
|
|
|
$path = New-TestSandboxItem -Sandbox $sandbox -Name 'ConvertedWritePath' -Directory:$Directory
|
|
|
|
|
$wrapped = [Security2.FileSystemSecurity2] (New-RawFileSystemSecurity -Kind $Kind)
|
|
|
|
|
|
|
|
|
|
$wrapped.Write([string] $path)
|
|
|
|
|
|
|
|
|
|
@((Get-Acl -LiteralPath $path).GetAccessRules($true, $false, [System.Security.Principal.SecurityIdentifier]) |
|
|
|
|
|
Where-Object -FilterScript { $_.IdentityReference.Value -eq 'S-1-1-0' }) | Should -HaveCount 1
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Before 5.0.0, the account list of both methods was built but its filter result was never assigned anywhere, so a
|
|
|
|
|
# non-null list removed every explicit entry regardless of what it named; only a .NET caller that passes a list is
|
|
|
|
|
# affected, since the cmdlets never do.
|
|
|
|
|
Describe 'RemoveFileSystemAccessRuleAll and RemoveFileSystemAuditRuleAll honor the account list' {
|
|
|
|
|
BeforeAll {
|
|
|
|
|
$allow = [System.Security.AccessControl.AccessControlType]::Allow
|
|
|
|
|
$success = [System.Security.AccessControl.AuditFlags]::Success
|
|
|
|
|
$accountOne = $identity
|
|
|
|
|
$accountTwo = [Security2.IdentityReference2] 'S-1-5-32-545'
|
|
|
|
|
$accountThree = [Security2.IdentityReference2] 'S-1-5-32-544'
|
|
|
|
|
$accountsByLabel = @{ One = $accountOne; Two = $accountTwo; Three = $accountThree }
|
|
|
|
|
|
|
|
|
|
function New-AccessSecurityFor {
|
|
|
|
|
[Diagnostics.CodeAnalysis.SuppressMessageAttribute(
|
|
|
|
|
'PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Builds an in-memory descriptor only.'
|
|
|
|
|
)]
|
|
|
|
|
param ([Security2.IdentityReference2[]] $Accounts)
|
|
|
|
|
|
|
|
|
|
$raw = New-Object -TypeName 'System.Security.AccessControl.FileSecurity'
|
|
|
|
|
foreach ($account in $Accounts) {
|
|
|
|
|
$raw.AddAccessRule((New-Object -TypeName 'System.Security.AccessControl.FileSystemAccessRule' -ArgumentList (
|
|
|
|
|
[System.Security.Principal.SecurityIdentifier] $account,
|
|
|
|
|
[System.Security.AccessControl.FileSystemRights]::ReadData, $allow
|
|
|
|
|
)))
|
|
|
|
|
}
|
|
|
|
|
$raw
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function New-AuditSecurityFor {
|
|
|
|
|
[Diagnostics.CodeAnalysis.SuppressMessageAttribute(
|
|
|
|
|
'PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Builds an in-memory descriptor only.'
|
|
|
|
|
)]
|
|
|
|
|
param ([Security2.IdentityReference2[]] $Accounts)
|
|
|
|
|
|
|
|
|
|
$raw = New-Object -TypeName 'System.Security.AccessControl.FileSecurity'
|
|
|
|
|
foreach ($account in $Accounts) {
|
|
|
|
|
$raw.AddAuditRule((New-Object -TypeName 'System.Security.AccessControl.FileSystemAuditRule' -ArgumentList (
|
|
|
|
|
[System.Security.Principal.SecurityIdentifier] $account,
|
|
|
|
|
[System.Security.AccessControl.FileSystemRights]::ReadData, $success
|
|
|
|
|
)))
|
|
|
|
|
}
|
|
|
|
|
$raw
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function New-IdentityList {
|
|
|
|
|
[Diagnostics.CodeAnalysis.SuppressMessageAttribute(
|
|
|
|
|
'PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Test helper that only creates a list.'
|
|
|
|
|
)]
|
|
|
|
|
param ([Security2.IdentityReference2[]] $Accounts)
|
|
|
|
|
|
|
|
|
|
$list = New-Object -TypeName 'System.Collections.Generic.List[Security2.IdentityReference2]'
|
|
|
|
|
foreach ($account in $Accounts) {
|
|
|
|
|
$list.Add($account)
|
|
|
|
|
}
|
|
|
|
|
, $list
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function Get-AccessSddl {
|
|
|
|
|
param ([System.Security.AccessControl.FileSecurity] $Raw)
|
|
|
|
|
|
|
|
|
|
$Raw.GetSecurityDescriptorSddlForm([System.Security.AccessControl.AccessControlSections]::Access)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function Get-AuditSddl {
|
|
|
|
|
param ([System.Security.AccessControl.FileSecurity] $Raw)
|
|
|
|
|
|
|
|
|
|
$Raw.GetSecurityDescriptorSddlForm([System.Security.AccessControl.AccessControlSections]::Audit)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Builds the expected result by removing the given accounts with the .NET method directly, instead of with
|
|
|
|
|
# a freshly built descriptor that only holds the remaining accounts: once an entry was added and removed
|
|
|
|
|
# again, the SACL stays present but empty, while a SACL that never held an entry is absent, and the two give
|
|
|
|
|
# a different SDDL for the same set of remaining entries.
|
|
|
|
|
function Get-ExpectedAccessSddl {
|
|
|
|
|
param ([Security2.IdentityReference2[]] $AccountsToRemove)
|
|
|
|
|
|
|
|
|
|
$raw = New-AccessSecurityFor -Accounts $accountOne, $accountTwo, $accountThree
|
|
|
|
|
foreach ($account in $AccountsToRemove) {
|
|
|
|
|
$raw.RemoveAccessRuleSpecific((New-Object -TypeName 'System.Security.AccessControl.FileSystemAccessRule' -ArgumentList (
|
|
|
|
|
[System.Security.Principal.SecurityIdentifier] $account,
|
|
|
|
|
[System.Security.AccessControl.FileSystemRights]::ReadData, $allow
|
|
|
|
|
)))
|
|
|
|
|
}
|
|
|
|
|
Get-AccessSddl $raw
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function Get-ExpectedAuditSddl {
|
|
|
|
|
param ([Security2.IdentityReference2[]] $AccountsToRemove)
|
|
|
|
|
|
|
|
|
|
$raw = New-AuditSecurityFor -Accounts $accountOne, $accountTwo, $accountThree
|
|
|
|
|
foreach ($account in $AccountsToRemove) {
|
|
|
|
|
$raw.RemoveAuditRuleSpecific((New-Object -TypeName 'System.Security.AccessControl.FileSystemAuditRule' -ArgumentList (
|
|
|
|
|
[System.Security.Principal.SecurityIdentifier] $account,
|
|
|
|
|
[System.Security.AccessControl.FileSystemRights]::ReadData, $success
|
|
|
|
|
)))
|
|
|
|
|
}
|
|
|
|
|
Get-AuditSddl $raw
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
It 'Should honor the account list when it removes access entries from a descriptor with three accounts, in memory, for <Scenario>' -ForEach @(
|
|
|
|
|
@{ Scenario = 'a list with one account'; AccountsToRemove = @('One') }
|
|
|
|
|
@{ Scenario = 'a list with two accounts'; AccountsToRemove = @('One', 'Two') }
|
|
|
|
|
@{ Scenario = 'a null list'; AccountsToRemove = $null }
|
|
|
|
|
@{ Scenario = 'an empty list'; AccountsToRemove = @() }
|
|
|
|
|
) {
|
|
|
|
|
$raw = New-AccessSecurityFor -Accounts $accountOne, $accountTwo, $accountThree
|
|
|
|
|
$toRemove = if ($null -eq $AccountsToRemove) { $accountOne, $accountTwo, $accountThree } else { $AccountsToRemove.ForEach({ $accountsByLabel[$_] }) }
|
|
|
|
|
$expected = Get-ExpectedAccessSddl -AccountsToRemove $toRemove
|
|
|
|
|
$accounts = if ($null -eq $AccountsToRemove) { $null } else { New-IdentityList -Accounts $AccountsToRemove.ForEach({ $accountsByLabel[$_] }) }
|
|
|
|
|
|
|
|
|
|
[Security2.FileSystemAccessRule2]::RemoveFileSystemAccessRuleAll(([Security2.FileSystemSecurity2] $raw), $accounts)
|
|
|
|
|
|
|
|
|
|
Get-AccessSddl $raw | Should -BeExactly $expected
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
It 'Should honor the account list when it removes audit entries from a descriptor with three accounts, in memory, for <Scenario>' -ForEach @(
|
|
|
|
|
@{ Scenario = 'a list with one account'; AccountsToRemove = @('One') }
|
|
|
|
|
@{ Scenario = 'a list with two accounts'; AccountsToRemove = @('One', 'Two') }
|
|
|
|
|
@{ Scenario = 'a null list'; AccountsToRemove = $null }
|
|
|
|
|
@{ Scenario = 'an empty list'; AccountsToRemove = @() }
|
|
|
|
|
) {
|
|
|
|
|
$raw = New-AuditSecurityFor -Accounts $accountOne, $accountTwo, $accountThree
|
|
|
|
|
$toRemove = if ($null -eq $AccountsToRemove) { $accountOne, $accountTwo, $accountThree } else { $AccountsToRemove.ForEach({ $accountsByLabel[$_] }) }
|
|
|
|
|
$expected = Get-ExpectedAuditSddl -AccountsToRemove $toRemove
|
|
|
|
|
$accounts = if ($null -eq $AccountsToRemove) { $null } else { New-IdentityList -Accounts $AccountsToRemove.ForEach({ $accountsByLabel[$_] }) }
|
|
|
|
|
|
|
|
|
|
[Security2.FileSystemAuditRule2]::RemoveFileSystemAuditRuleAll(([Security2.FileSystemSecurity2] $raw), $accounts)
|
|
|
|
|
|
|
|
|
|
Get-AuditSddl $raw | Should -BeExactly $expected
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
It 'Should remove only the listed account''s explicit access entries of a sandbox item, read back with .NET' {
|
|
|
|
|
$path = New-TestSandboxItem -Sandbox $sandbox -Name 'RemoveAllAccessByItem'
|
|
|
|
|
$item = New-Object -TypeName 'Alphaleonis.Win32.Filesystem.FileInfo' -ArgumentList $path
|
|
|
|
|
foreach ($account in $accountOne, $accountTwo, $accountThree) {
|
|
|
|
|
[void] [Security2.FileSystemAccessRule2]::AddFileSystemAccessRule(
|
|
|
|
|
$item, $account, [Security2.FileSystemRights2]::ReadData, $allow,
|
|
|
|
|
[System.Security.AccessControl.InheritanceFlags]::None, [System.Security.AccessControl.PropagationFlags]::None
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Security2.FileSystemAccessRule2]::RemoveFileSystemAccessRuleAll($item, (New-IdentityList -Accounts $accountOne))
|
|
|
|
|
|
|
|
|
|
$remaining = @((Get-Acl -LiteralPath $path).GetAccessRules($true, $false, [System.Security.Principal.SecurityIdentifier]))
|
|
|
|
|
@($remaining.IdentityReference.Value | Sort-Object) | Should -Be @(($accountTwo.Sid, $accountThree.Sid) | Sort-Object)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
It 'Should remove only the listed account''s explicit audit entries of a sandbox item, read back with .NET' -Skip:(-not $holdsSecurityPrivilege) {
|
|
|
|
|
$path = New-TestSandboxItem -Sandbox $sandbox -Name 'RemoveAllAuditByItem'
|
|
|
|
|
$item = New-Object -TypeName 'Alphaleonis.Win32.Filesystem.FileInfo' -ArgumentList $path
|
|
|
|
|
foreach ($account in $accountOne, $accountTwo, $accountThree) {
|
|
|
|
|
[void] [Security2.FileSystemAuditRule2]::AddFileSystemAuditRule(
|
|
|
|
|
$item, $account, [Security2.FileSystemRights2]::ReadData, $success,
|
|
|
|
|
[System.Security.AccessControl.InheritanceFlags]::None, [System.Security.AccessControl.PropagationFlags]::None
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Security2.FileSystemAuditRule2]::RemoveFileSystemAuditRuleAll($item, (New-IdentityList -Accounts $accountOne))
|
|
|
|
|
|
|
|
|
|
$remaining = @((Get-Acl -LiteralPath $path -Audit).GetAuditRules($true, $false, [System.Security.Principal.SecurityIdentifier]))
|
|
|
|
|
@($remaining.IdentityReference.Value | Sort-Object) | Should -Be @(($accountTwo.Sid, $accountThree.Sid) | Sort-Object)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Describe 'Generic access rights and identity errors' {
|
|
|
|
|
It 'Should map the generic mask <Mask> to the file system rights <Expected>' -ForEach @(
|
|
|
|
|
@{ Mask = '80000000'; Expected = '00120089' }
|
|
|
|
|
@{ Mask = '40000000'; Expected = '00120116' }
|
|
|
|
|
@{ Mask = '20000000'; Expected = '001200A0' }
|
|
|
|
|
@{ Mask = '10000000'; Expected = '001F01FF' }
|
|
|
|
|
@{ Mask = 'C0000000'; Expected = '0012019F' }
|
|
|
|
|
@{ Mask = '80010000'; Expected = '00130089' }
|
|
|
|
|
@{ Mask = '001F01FF'; Expected = '001F01FF' }
|
|
|
|
|
@{ Mask = '00120089'; Expected = '00120089' }
|
|
|
|
|
@{ Mask = '02000000'; Expected = '02000000' }
|
|
|
|
|
@{ Mask = '82000000'; Expected = '02120089' }
|
|
|
|
|
@{ Mask = '00000000'; Expected = '00000000' }
|
|
|
|
|
) {
|
|
|
|
|
$rights = [Security2.FileSystemSecurity2]::MapGenericRightsToFileSystemRights([Convert]::ToUInt32($Mask, 16))
|
|
|
|
|
|
|
|
|
|
[int] $rights | Should -Be ([Convert]::ToInt32($Expected, 16))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
It 'Should reject <Case> when it creates an identity' -ForEach @(
|
|
|
|
|
@{ Case = 'an empty value'; Value = ''; Expected = [System.ArgumentException] }
|
|
|
|
|
@{ Case = 'a SID with too many sub authorities'; Value = ('S-1-' + (('1-' * 20) + '1')); Expected = [System.InvalidCastException] }
|
|
|
|
|
@{ Case = 'an account that does not exist'; Value = 'NTFSSecurityNoSuchAccount'; Expected = [System.Security.Principal.IdentityNotMappedException] }
|
|
|
|
|
) {
|
|
|
|
|
$failure = { [Security2.IdentityReference2]::new($Value) } | Should -Throw -PassThru
|
|
|
|
|
|
|
|
|
|
# PowerShell wraps the exception of a constructor, which here wraps the cause of an invalid SID in turn.
|
|
|
|
|
$failure.Exception.InnerException | Should -BeOfType $Expected
|
|
|
|
|
}
|
|
|
|
|
}
|
test: cover deny entries without rights, a NULL DACL, hard links with read rights denied, and the extension methods
Add-NTFSAccess, Remove-NTFSAccess, and Add-NTFSAudit report an AddAceError or RemoveAceError for each item when .NET refuses an entry without rights, change nothing, and return nothing with -PassThru. Get-NTFSAccess returns the one entry that .NET reports for a NULL DACL without a source; the new helper Set-TestNullDacl writes it through SetNamedSecurityInfo inside the sandbox guard. Get-NTFSHardLink lists the names of a file whose read rights are denied, which is why its UnauthorizedAccessException handler has no trigger. The public Extensions.ForEach and GetParent helpers, which a static scan listed as unused although cmdlets call them, are tested directly.
All of these characterize behavior that was already correct, so none was red before; the mutations that prove their detection run against the frozen measurement commit.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
3 weeks ago
|
|
|
|
|
|
|
|
# The helpers of the cmdlets are public extension methods, so a script can call them. The cmdlets pass what the guards
|
|
|
|
|
# of ForEach refuse and a parent that is a folder, never a file; the generic method is invoked through reflection,
|
|
|
|
|
# because Windows PowerShell can't name the type argument of a call.
|
|
|
|
|
Describe 'The extension methods of the cmdlets' {
|
|
|
|
|
BeforeAll {
|
|
|
|
|
$forEachMethod = [NTFSSecurity.Extensions].GetMethod('ForEach').MakeGenericMethod([string])
|
|
|
|
|
|
|
|
|
|
function New-ItemObject {
|
|
|
|
|
[Diagnostics.CodeAnalysis.SuppressMessageAttribute(
|
|
|
|
|
'PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Test helper that only creates an object.'
|
|
|
|
|
)]
|
|
|
|
|
param ([string] $Library, [string] $Path)
|
|
|
|
|
|
|
|
|
|
if ($Library -eq 'AlphaFS') {
|
|
|
|
|
[Alphaleonis.Win32.Filesystem.FileInfo]::new($Path)
|
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
[System.IO.FileInfo]::new($Path)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
It 'ForEach should reject <Case>' -ForEach @(
|
|
|
|
|
@{ Case = 'a source that is null'; NullSource = $true }
|
|
|
|
|
@{ Case = 'an action that is null'; NullSource = $false }
|
|
|
|
|
) {
|
|
|
|
|
$arguments = [object[]]::new(2)
|
|
|
|
|
if ($NullSource) {
|
|
|
|
|
$arguments[1] = [System.Action[string]] { param ($Element) $null = $Element }
|
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
$arguments[0] = [string[]] @('One')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$failure = { $forEachMethod.Invoke($null, $arguments) } | Should -Throw -PassThru
|
|
|
|
|
|
|
|
|
|
# Reflection wraps the exception of the method, and PowerShell wraps that in turn.
|
|
|
|
|
$failure.Exception.GetBaseException() | Should -BeOfType [System.ArgumentException]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
It 'ForEach should run the action for each element in order' {
|
|
|
|
|
$seen = New-Object -TypeName 'System.Collections.Generic.List[string]'
|
|
|
|
|
$arguments = [object[]]::new(2)
|
|
|
|
|
$arguments[0] = [string[]] @('One', 'Two', 'Three')
|
|
|
|
|
$arguments[1] = [System.Action[string]] { param ($Element) $seen.Add($Element) }
|
|
|
|
|
|
|
|
|
|
$forEachMethod.Invoke($null, $arguments) | Out-Null
|
|
|
|
|
|
|
|
|
|
$seen -join ',' | Should -BeExactly 'One,Two,Three'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Context 'GetParent' {
|
|
|
|
|
BeforeAll {
|
|
|
|
|
$folder = New-TestSandboxItem -Sandbox $sandbox -Name 'ParentFolder' -Directory
|
|
|
|
|
$file = New-TestSandboxItem -Sandbox $sandbox -Name 'ParentFile'
|
|
|
|
|
$inFolder = Join-Path -Path $folder -ChildPath 'Child.txt'
|
|
|
|
|
$belowFile = Join-Path -Path $file -ChildPath 'Child.txt'
|
|
|
|
|
$belowMissing = Join-Path -Path $sandbox -ChildPath 'Missing\Child.txt'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
It 'Should return a folder as a DirectoryInfo of <Library>' -ForEach @(
|
|
|
|
|
@{ Library = 'AlphaFS'; TypeName = 'Alphaleonis.Win32.Filesystem.DirectoryInfo' }
|
|
|
|
|
@{ Library = 'System.IO'; TypeName = 'System.IO.DirectoryInfo' }
|
|
|
|
|
) {
|
|
|
|
|
$parent = [NTFSSecurity.Extensions]::GetParent((New-ItemObject -Library $Library -Path $inFolder))
|
|
|
|
|
|
|
|
|
|
$parent.GetType().FullName | Should -BeExactly $TypeName
|
|
|
|
|
$parent.FullName | Should -BeExactly $folder
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# A FileInfo can name a path below a file, which no file system holds, so the parent path names a file.
|
|
|
|
|
It 'Should return a parent path that names a file as a FileInfo of <Library>' -ForEach @(
|
|
|
|
|
@{ Library = 'AlphaFS'; TypeName = 'Alphaleonis.Win32.Filesystem.FileInfo' }
|
|
|
|
|
@{ Library = 'System.IO'; TypeName = 'System.IO.FileInfo' }
|
|
|
|
|
) {
|
|
|
|
|
$parent = [NTFSSecurity.Extensions]::GetParent((New-ItemObject -Library $Library -Path $belowFile))
|
|
|
|
|
|
|
|
|
|
$parent.GetType().FullName | Should -BeExactly $TypeName
|
|
|
|
|
$parent.FullName | Should -BeExactly $file
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
It 'Should throw a FileNotFoundException for a parent that does not exist, for <Library>' -ForEach @(
|
|
|
|
|
@{ Library = 'AlphaFS' }
|
|
|
|
|
@{ Library = 'System.IO' }
|
|
|
|
|
) {
|
|
|
|
|
$failure = { [NTFSSecurity.Extensions]::GetParent((New-ItemObject -Library $Library -Path $belowMissing)) } |
|
|
|
|
|
Should -Throw -PassThru
|
|
|
|
|
|
|
|
|
|
$failure.Exception.GetBaseException() | Should -BeOfType [System.IO.FileNotFoundException]
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|