|
|
|
|
<#
|
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
|
|
|
|
|
}
|
fix(security2): let the readers handle a descriptor that has no item
3.1 made a FileSystemSecurity2 without an item reachable from a .NET
caller (the conversions from FileSecurity and DirectorySecurity), but
six sibling call sites still dereferenced the item without checking it
first, so a caller who converted a FileSecurity or DirectorySecurity
and then read its entries, owner, inheritance, or effective access met
a NullReferenceException instead of a usable result.
GetFileSystemAccessRules and GetFileSystemAuditRules (the
FileSystemSecurity2 overload) now give each returned entry the same
null FullName that the descriptor itself reports; asking for the
source of an inherited entry (getInheritedFrom) needs the path of the
item, so it now throws one clear InvalidOperationException instead,
naming the reason, when the descriptor has none. FileSystemOwner and
FileSystemInheritanceInfo give a null FullName (and Name) the same
way; FileSystemOwner.ToString, which must never return null, gives an
empty string instead, since there is no other representation to fall
back to. The FileSystemAccessRule2(rule, item) constructor, which
EffectiveAccess.GetEffectiveAccess uses to build its result, now
accepts a null item the same way, so effective access can be
calculated for such a descriptor and the result names no path.
Every existing behavior for an item that is not null is unchanged;
only a .NET caller that reads such a descriptor can notice the
difference, and only for the better: a usable result or a clear error
instead of an unrelated NullReferenceException. Public signatures are
unchanged.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
1 week ago
|
|
|
|
|
|
|
|
# PowerShell silently turns an exception that a property getter throws into $null, without even recording a
|
|
|
|
|
# non-terminating error, so a plain "$object.Property" cannot tell a getter that throws from one that
|
|
|
|
|
# legitimately returns null. The getter is invoked through reflection instead, so that it throws normally.
|
|
|
|
|
function Invoke-PropertyGetter {
|
|
|
|
|
[Diagnostics.CodeAnalysis.SuppressMessageAttribute(
|
|
|
|
|
'PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Test helper that only reads a property through reflection.'
|
|
|
|
|
)]
|
|
|
|
|
param ([object] $InputObject, [string] $PropertyName)
|
|
|
|
|
|
|
|
|
|
$InputObject.GetType().GetProperty($PropertyName).GetGetMethod().Invoke($InputObject, $null)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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
|
fix(security2): keep a descriptor without an item usable by the cmdlets that take a security descriptor
PowerShell's parameter binder uses the conversion that 3.1 fixed for
every -SecurityDescriptor parameter, so a descriptor that Get-Acl
returns, never meant for these cmdlets, now binds where it used to be
a binding error. Two more consequences of that surfaced:
With the default module setting GetInheritedFrom ($true), Get-NTFSAccess,
Get-NTFSAudit, Get-NTFSOrphanedAccess, and Get-NTFSOrphanedAudit stopped
with the InvalidOperationException that a previous fix added for a
descriptor without an item; a cmdlet user has no parameter to avoid it.
GetFileSystemAccessRules and GetFileSystemAuditRules now treat
getInheritedFrom as false for such a descriptor instead of throwing: the
entries come back with no source, the same as an entry whose source
Windows cannot name.
The private constructor that wraps a descriptor directly marked every
section, including Audit, as read; Get-Acl never reads Audit unless
-Audit is given. This made HasAuditSection wrongly report true, so the
existing #109 check let Get-NTFSAudit go on with no audit data instead
of reporting that the descriptor has none, and let Get-NTFSInheritance
report AuditInheritanceEnabled as a real value instead of null
(unknown). The constructor now includes Audit in the sections only when
the descriptor's binary form actually carries a SACL, the same test
HasSystemAcl already uses.
Get-NTFSOwner, Get-NTFSSimpleAccess, Get-NTFSOrphanedAccess,
Get-NTFSEffectiveAccess, and Add-/Remove-NTFSAccess with such a
descriptor already worked; Set-NTFSSecurityDescriptor already reported
a clear, non-terminating error. A review finding on the earlier fix is
also addressed: two assertions read a property through reflection
instead of a plain PowerShell property access, which silently turns a
throwing getter into $null without a trace and so could not tell a
getter that throws from one that legitimately returns null.
Every existing behavior for an item that is not null is unchanged.
Public signatures are unchanged.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
1 week ago
|
|
|
# A plain property read turns a throwing getter into $null (see Invoke-PropertyGetter above), so these two
|
|
|
|
|
# getters are invoked through reflection: a null FullName/Name is pinned as the getter's own return value,
|
|
|
|
|
# not as whatever a getter that happened to throw would also look like from here.
|
|
|
|
|
{ Invoke-PropertyGetter -InputObject $wrapped -PropertyName 'FullName' } | Should -Not -Throw
|
|
|
|
|
{ Invoke-PropertyGetter -InputObject $wrapped -PropertyName 'Name' } | Should -Not -Throw
|
|
|
|
|
(Invoke-PropertyGetter -InputObject $wrapped -PropertyName 'FullName') | Should -Be $null
|
|
|
|
|
(Invoke-PropertyGetter -InputObject $wrapped -PropertyName '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
|
|
|
|
|
}
|
fix(security2): let the readers handle a descriptor that has no item
3.1 made a FileSystemSecurity2 without an item reachable from a .NET
caller (the conversions from FileSecurity and DirectorySecurity), but
six sibling call sites still dereferenced the item without checking it
first, so a caller who converted a FileSecurity or DirectorySecurity
and then read its entries, owner, inheritance, or effective access met
a NullReferenceException instead of a usable result.
GetFileSystemAccessRules and GetFileSystemAuditRules (the
FileSystemSecurity2 overload) now give each returned entry the same
null FullName that the descriptor itself reports; asking for the
source of an inherited entry (getInheritedFrom) needs the path of the
item, so it now throws one clear InvalidOperationException instead,
naming the reason, when the descriptor has none. FileSystemOwner and
FileSystemInheritanceInfo give a null FullName (and Name) the same
way; FileSystemOwner.ToString, which must never return null, gives an
empty string instead, since there is no other representation to fall
back to. The FileSystemAccessRule2(rule, item) constructor, which
EffectiveAccess.GetEffectiveAccess uses to build its result, now
accepts a null item the same way, so effective access can be
calculated for such a descriptor and the result names no path.
Every existing behavior for an item that is not null is unchanged;
only a .NET caller that reads such a descriptor can notice the
difference, and only for the better: a usable result or a clear error
instead of an unrelated NullReferenceException. Public signatures are
unchanged.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
1 week ago
|
|
|
|
|
|
|
|
# 3.1 makes a FileSystemSecurity2 without an item reachable from a .NET caller; the readers below must not meet a
|
|
|
|
|
# NullReferenceException from it. Built in memory, so no privilege is needed to add the audit rule.
|
|
|
|
|
It 'Should return the <RuleType> entries of a <Kind> without an item, with a null FullName' -ForEach @(
|
|
|
|
|
@{ Kind = 'FileSecurity'; RuleType = 'access' }
|
|
|
|
|
@{ Kind = 'DirectorySecurity'; RuleType = 'access' }
|
|
|
|
|
@{ Kind = 'FileSecurity'; RuleType = 'audit' }
|
|
|
|
|
@{ Kind = 'DirectorySecurity'; RuleType = 'audit' }
|
|
|
|
|
) {
|
|
|
|
|
if ($RuleType -eq 'access') {
|
|
|
|
|
$sd = [Security2.FileSystemSecurity2] (New-RawFileSystemSecurity -Kind $Kind)
|
|
|
|
|
$found = @([Security2.FileSystemAccessRule2]::GetFileSystemAccessRules($sd, $true, $true, $false))
|
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
$raw = New-RawFileSystemSecurity -Kind $Kind
|
|
|
|
|
$raw.AddAuditRule((New-Object -TypeName 'System.Security.AccessControl.FileSystemAuditRule' -ArgumentList (
|
|
|
|
|
$sid, [System.Security.AccessControl.FileSystemRights]::ReadData,
|
|
|
|
|
[System.Security.AccessControl.AuditFlags]::Success
|
|
|
|
|
)))
|
|
|
|
|
$sd = [Security2.FileSystemSecurity2] $raw
|
|
|
|
|
$found = @([Security2.FileSystemAuditRule2]::GetFileSystemAuditRules($sd, $true, $true, $false))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$found | Should -HaveCount 1
|
|
|
|
|
$found[0].FullName | Should -Be $null
|
|
|
|
|
$found[0].Account.Sid | Should -BeExactly 'S-1-1-0'
|
|
|
|
|
}
|
|
|
|
|
|
fix(security2): keep a descriptor without an item usable by the cmdlets that take a security descriptor
PowerShell's parameter binder uses the conversion that 3.1 fixed for
every -SecurityDescriptor parameter, so a descriptor that Get-Acl
returns, never meant for these cmdlets, now binds where it used to be
a binding error. Two more consequences of that surfaced:
With the default module setting GetInheritedFrom ($true), Get-NTFSAccess,
Get-NTFSAudit, Get-NTFSOrphanedAccess, and Get-NTFSOrphanedAudit stopped
with the InvalidOperationException that a previous fix added for a
descriptor without an item; a cmdlet user has no parameter to avoid it.
GetFileSystemAccessRules and GetFileSystemAuditRules now treat
getInheritedFrom as false for such a descriptor instead of throwing: the
entries come back with no source, the same as an entry whose source
Windows cannot name.
The private constructor that wraps a descriptor directly marked every
section, including Audit, as read; Get-Acl never reads Audit unless
-Audit is given. This made HasAuditSection wrongly report true, so the
existing #109 check let Get-NTFSAudit go on with no audit data instead
of reporting that the descriptor has none, and let Get-NTFSInheritance
report AuditInheritanceEnabled as a real value instead of null
(unknown). The constructor now includes Audit in the sections only when
the descriptor's binary form actually carries a SACL, the same test
HasSystemAcl already uses.
Get-NTFSOwner, Get-NTFSSimpleAccess, Get-NTFSOrphanedAccess,
Get-NTFSEffectiveAccess, and Add-/Remove-NTFSAccess with such a
descriptor already worked; Set-NTFSSecurityDescriptor already reported
a clear, non-terminating error. A review finding on the earlier fix is
also addressed: two assertions read a property through reflection
instead of a plain PowerShell property access, which silently turns a
throwing getter into $null without a trace and so could not tell a
getter that throws from one that legitimately returns null.
Every existing behavior for an item that is not null is unchanged.
Public signatures are unchanged.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
1 week ago
|
|
|
# Before this fix, a descriptor without an item made getInheritedFrom throw an InvalidOperationException, which a
|
|
|
|
|
# cmdlet caller has no parameter to avoid (the module setting GetInheritedFrom is $true by default): getInheritedFrom
|
|
|
|
|
# is now treated as false for such a descriptor instead, the same as an entry whose source Windows cannot name.
|
|
|
|
|
It 'Should return the <RuleType> entries with no source, not throw, when getInheritedFrom is requested for a descriptor without an item' -ForEach @(
|
fix(security2): let the readers handle a descriptor that has no item
3.1 made a FileSystemSecurity2 without an item reachable from a .NET
caller (the conversions from FileSecurity and DirectorySecurity), but
six sibling call sites still dereferenced the item without checking it
first, so a caller who converted a FileSecurity or DirectorySecurity
and then read its entries, owner, inheritance, or effective access met
a NullReferenceException instead of a usable result.
GetFileSystemAccessRules and GetFileSystemAuditRules (the
FileSystemSecurity2 overload) now give each returned entry the same
null FullName that the descriptor itself reports; asking for the
source of an inherited entry (getInheritedFrom) needs the path of the
item, so it now throws one clear InvalidOperationException instead,
naming the reason, when the descriptor has none. FileSystemOwner and
FileSystemInheritanceInfo give a null FullName (and Name) the same
way; FileSystemOwner.ToString, which must never return null, gives an
empty string instead, since there is no other representation to fall
back to. The FileSystemAccessRule2(rule, item) constructor, which
EffectiveAccess.GetEffectiveAccess uses to build its result, now
accepts a null item the same way, so effective access can be
calculated for such a descriptor and the result names no path.
Every existing behavior for an item that is not null is unchanged;
only a .NET caller that reads such a descriptor can notice the
difference, and only for the better: a usable result or a clear error
instead of an unrelated NullReferenceException. Public signatures are
unchanged.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
1 week ago
|
|
|
@{ RuleType = 'access' }
|
|
|
|
|
@{ RuleType = 'audit' }
|
|
|
|
|
) {
|
|
|
|
|
if ($RuleType -eq 'access') {
|
|
|
|
|
$sd = [Security2.FileSystemSecurity2] (New-RawFileSystemSecurity -Kind 'FileSecurity')
|
fix(security2): keep a descriptor without an item usable by the cmdlets that take a security descriptor
PowerShell's parameter binder uses the conversion that 3.1 fixed for
every -SecurityDescriptor parameter, so a descriptor that Get-Acl
returns, never meant for these cmdlets, now binds where it used to be
a binding error. Two more consequences of that surfaced:
With the default module setting GetInheritedFrom ($true), Get-NTFSAccess,
Get-NTFSAudit, Get-NTFSOrphanedAccess, and Get-NTFSOrphanedAudit stopped
with the InvalidOperationException that a previous fix added for a
descriptor without an item; a cmdlet user has no parameter to avoid it.
GetFileSystemAccessRules and GetFileSystemAuditRules now treat
getInheritedFrom as false for such a descriptor instead of throwing: the
entries come back with no source, the same as an entry whose source
Windows cannot name.
The private constructor that wraps a descriptor directly marked every
section, including Audit, as read; Get-Acl never reads Audit unless
-Audit is given. This made HasAuditSection wrongly report true, so the
existing #109 check let Get-NTFSAudit go on with no audit data instead
of reporting that the descriptor has none, and let Get-NTFSInheritance
report AuditInheritanceEnabled as a real value instead of null
(unknown). The constructor now includes Audit in the sections only when
the descriptor's binary form actually carries a SACL, the same test
HasSystemAcl already uses.
Get-NTFSOwner, Get-NTFSSimpleAccess, Get-NTFSOrphanedAccess,
Get-NTFSEffectiveAccess, and Add-/Remove-NTFSAccess with such a
descriptor already worked; Set-NTFSSecurityDescriptor already reported
a clear, non-terminating error. A review finding on the earlier fix is
also addressed: two assertions read a property through reflection
instead of a plain PowerShell property access, which silently turns a
throwing getter into $null without a trace and so could not tell a
getter that throws from one that legitimately returns null.
Every existing behavior for an item that is not null is unchanged.
Public signatures are unchanged.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
1 week ago
|
|
|
{ [Security2.FileSystemAccessRule2]::GetFileSystemAccessRules($sd, $true, $true, $true) } | Should -Not -Throw
|
|
|
|
|
$found = @([Security2.FileSystemAccessRule2]::GetFileSystemAccessRules($sd, $true, $true, $true))
|
fix(security2): let the readers handle a descriptor that has no item
3.1 made a FileSystemSecurity2 without an item reachable from a .NET
caller (the conversions from FileSecurity and DirectorySecurity), but
six sibling call sites still dereferenced the item without checking it
first, so a caller who converted a FileSecurity or DirectorySecurity
and then read its entries, owner, inheritance, or effective access met
a NullReferenceException instead of a usable result.
GetFileSystemAccessRules and GetFileSystemAuditRules (the
FileSystemSecurity2 overload) now give each returned entry the same
null FullName that the descriptor itself reports; asking for the
source of an inherited entry (getInheritedFrom) needs the path of the
item, so it now throws one clear InvalidOperationException instead,
naming the reason, when the descriptor has none. FileSystemOwner and
FileSystemInheritanceInfo give a null FullName (and Name) the same
way; FileSystemOwner.ToString, which must never return null, gives an
empty string instead, since there is no other representation to fall
back to. The FileSystemAccessRule2(rule, item) constructor, which
EffectiveAccess.GetEffectiveAccess uses to build its result, now
accepts a null item the same way, so effective access can be
calculated for such a descriptor and the result names no path.
Every existing behavior for an item that is not null is unchanged;
only a .NET caller that reads such a descriptor can notice the
difference, and only for the better: a usable result or a clear error
instead of an unrelated NullReferenceException. Public signatures are
unchanged.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
1 week ago
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
$raw = New-RawFileSystemSecurity -Kind 'FileSecurity'
|
|
|
|
|
$raw.AddAuditRule((New-Object -TypeName 'System.Security.AccessControl.FileSystemAuditRule' -ArgumentList (
|
|
|
|
|
$sid, [System.Security.AccessControl.FileSystemRights]::ReadData,
|
|
|
|
|
[System.Security.AccessControl.AuditFlags]::Success
|
|
|
|
|
)))
|
|
|
|
|
$sd = [Security2.FileSystemSecurity2] $raw
|
fix(security2): keep a descriptor without an item usable by the cmdlets that take a security descriptor
PowerShell's parameter binder uses the conversion that 3.1 fixed for
every -SecurityDescriptor parameter, so a descriptor that Get-Acl
returns, never meant for these cmdlets, now binds where it used to be
a binding error. Two more consequences of that surfaced:
With the default module setting GetInheritedFrom ($true), Get-NTFSAccess,
Get-NTFSAudit, Get-NTFSOrphanedAccess, and Get-NTFSOrphanedAudit stopped
with the InvalidOperationException that a previous fix added for a
descriptor without an item; a cmdlet user has no parameter to avoid it.
GetFileSystemAccessRules and GetFileSystemAuditRules now treat
getInheritedFrom as false for such a descriptor instead of throwing: the
entries come back with no source, the same as an entry whose source
Windows cannot name.
The private constructor that wraps a descriptor directly marked every
section, including Audit, as read; Get-Acl never reads Audit unless
-Audit is given. This made HasAuditSection wrongly report true, so the
existing #109 check let Get-NTFSAudit go on with no audit data instead
of reporting that the descriptor has none, and let Get-NTFSInheritance
report AuditInheritanceEnabled as a real value instead of null
(unknown). The constructor now includes Audit in the sections only when
the descriptor's binary form actually carries a SACL, the same test
HasSystemAcl already uses.
Get-NTFSOwner, Get-NTFSSimpleAccess, Get-NTFSOrphanedAccess,
Get-NTFSEffectiveAccess, and Add-/Remove-NTFSAccess with such a
descriptor already worked; Set-NTFSSecurityDescriptor already reported
a clear, non-terminating error. A review finding on the earlier fix is
also addressed: two assertions read a property through reflection
instead of a plain PowerShell property access, which silently turns a
throwing getter into $null without a trace and so could not tell a
getter that throws from one that legitimately returns null.
Every existing behavior for an item that is not null is unchanged.
Public signatures are unchanged.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
1 week ago
|
|
|
{ [Security2.FileSystemAuditRule2]::GetFileSystemAuditRules($sd, $true, $true, $true) } | Should -Not -Throw
|
|
|
|
|
$found = @([Security2.FileSystemAuditRule2]::GetFileSystemAuditRules($sd, $true, $true, $true))
|
fix(security2): let the readers handle a descriptor that has no item
3.1 made a FileSystemSecurity2 without an item reachable from a .NET
caller (the conversions from FileSecurity and DirectorySecurity), but
six sibling call sites still dereferenced the item without checking it
first, so a caller who converted a FileSecurity or DirectorySecurity
and then read its entries, owner, inheritance, or effective access met
a NullReferenceException instead of a usable result.
GetFileSystemAccessRules and GetFileSystemAuditRules (the
FileSystemSecurity2 overload) now give each returned entry the same
null FullName that the descriptor itself reports; asking for the
source of an inherited entry (getInheritedFrom) needs the path of the
item, so it now throws one clear InvalidOperationException instead,
naming the reason, when the descriptor has none. FileSystemOwner and
FileSystemInheritanceInfo give a null FullName (and Name) the same
way; FileSystemOwner.ToString, which must never return null, gives an
empty string instead, since there is no other representation to fall
back to. The FileSystemAccessRule2(rule, item) constructor, which
EffectiveAccess.GetEffectiveAccess uses to build its result, now
accepts a null item the same way, so effective access can be
calculated for such a descriptor and the result names no path.
Every existing behavior for an item that is not null is unchanged;
only a .NET caller that reads such a descriptor can notice the
difference, and only for the better: a usable result or a clear error
instead of an unrelated NullReferenceException. Public signatures are
unchanged.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
1 week ago
|
|
|
}
|
|
|
|
|
|
fix(security2): keep a descriptor without an item usable by the cmdlets that take a security descriptor
PowerShell's parameter binder uses the conversion that 3.1 fixed for
every -SecurityDescriptor parameter, so a descriptor that Get-Acl
returns, never meant for these cmdlets, now binds where it used to be
a binding error. Two more consequences of that surfaced:
With the default module setting GetInheritedFrom ($true), Get-NTFSAccess,
Get-NTFSAudit, Get-NTFSOrphanedAccess, and Get-NTFSOrphanedAudit stopped
with the InvalidOperationException that a previous fix added for a
descriptor without an item; a cmdlet user has no parameter to avoid it.
GetFileSystemAccessRules and GetFileSystemAuditRules now treat
getInheritedFrom as false for such a descriptor instead of throwing: the
entries come back with no source, the same as an entry whose source
Windows cannot name.
The private constructor that wraps a descriptor directly marked every
section, including Audit, as read; Get-Acl never reads Audit unless
-Audit is given. This made HasAuditSection wrongly report true, so the
existing #109 check let Get-NTFSAudit go on with no audit data instead
of reporting that the descriptor has none, and let Get-NTFSInheritance
report AuditInheritanceEnabled as a real value instead of null
(unknown). The constructor now includes Audit in the sections only when
the descriptor's binary form actually carries a SACL, the same test
HasSystemAcl already uses.
Get-NTFSOwner, Get-NTFSSimpleAccess, Get-NTFSOrphanedAccess,
Get-NTFSEffectiveAccess, and Add-/Remove-NTFSAccess with such a
descriptor already worked; Set-NTFSSecurityDescriptor already reported
a clear, non-terminating error. A review finding on the earlier fix is
also addressed: two assertions read a property through reflection
instead of a plain PowerShell property access, which silently turns a
throwing getter into $null without a trace and so could not tell a
getter that throws from one that legitimately returns null.
Every existing behavior for an item that is not null is unchanged.
Public signatures are unchanged.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
1 week ago
|
|
|
$found | Should -HaveCount 1
|
|
|
|
|
$found[0].InheritedFrom | Should -Be $null
|
fix(security2): let the readers handle a descriptor that has no item
3.1 made a FileSystemSecurity2 without an item reachable from a .NET
caller (the conversions from FileSecurity and DirectorySecurity), but
six sibling call sites still dereferenced the item without checking it
first, so a caller who converted a FileSecurity or DirectorySecurity
and then read its entries, owner, inheritance, or effective access met
a NullReferenceException instead of a usable result.
GetFileSystemAccessRules and GetFileSystemAuditRules (the
FileSystemSecurity2 overload) now give each returned entry the same
null FullName that the descriptor itself reports; asking for the
source of an inherited entry (getInheritedFrom) needs the path of the
item, so it now throws one clear InvalidOperationException instead,
naming the reason, when the descriptor has none. FileSystemOwner and
FileSystemInheritanceInfo give a null FullName (and Name) the same
way; FileSystemOwner.ToString, which must never return null, gives an
empty string instead, since there is no other representation to fall
back to. The FileSystemAccessRule2(rule, item) constructor, which
EffectiveAccess.GetEffectiveAccess uses to build its result, now
accepts a null item the same way, so effective access can be
calculated for such a descriptor and the result names no path.
Every existing behavior for an item that is not null is unchanged;
only a .NET caller that reads such a descriptor can notice the
difference, and only for the better: a usable result or a clear error
instead of an unrelated NullReferenceException. Public signatures are
unchanged.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
1 week ago
|
|
|
}
|
|
|
|
|
|
|
|
|
|
It 'Should return an owner object with a null FullName and an empty ToString for a <Kind> without an item' -ForEach @(
|
|
|
|
|
@{ Kind = 'FileSecurity' }
|
|
|
|
|
@{ Kind = 'DirectorySecurity' }
|
|
|
|
|
) {
|
|
|
|
|
$raw = New-RawFileSystemSecurity -Kind $Kind
|
|
|
|
|
$raw.SetOwner($sid)
|
|
|
|
|
$sd = [Security2.FileSystemSecurity2] $raw
|
|
|
|
|
|
|
|
|
|
$owner = [Security2.FileSystemOwner]::GetOwner($sd)
|
|
|
|
|
|
|
|
|
|
$owner.Owner.Sid | Should -BeExactly 'S-1-1-0'
|
|
|
|
|
{ Invoke-PropertyGetter -InputObject $owner -PropertyName 'FullName' } | Should -Not -Throw
|
|
|
|
|
(Invoke-PropertyGetter -InputObject $owner -PropertyName 'FullName') | Should -Be $null
|
|
|
|
|
$owner.ToString() | Should -BeExactly ''
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
It 'Should return inheritance info with a null FullName and Name for a <Kind> without an item' -ForEach @(
|
|
|
|
|
@{ Kind = 'FileSecurity' }
|
|
|
|
|
@{ Kind = 'DirectorySecurity' }
|
|
|
|
|
) {
|
|
|
|
|
$sd = [Security2.FileSystemSecurity2] (New-RawFileSystemSecurity -Kind $Kind)
|
|
|
|
|
|
|
|
|
|
$info = [Security2.FileSystemInheritanceInfo]::GetFileSystemInheritanceInfo($sd)
|
|
|
|
|
|
|
|
|
|
{ Invoke-PropertyGetter -InputObject $info -PropertyName 'FullName' } | Should -Not -Throw
|
|
|
|
|
{ Invoke-PropertyGetter -InputObject $info -PropertyName 'Name' } | Should -Not -Throw
|
|
|
|
|
(Invoke-PropertyGetter -InputObject $info -PropertyName 'FullName') | Should -Be $null
|
|
|
|
|
(Invoke-PropertyGetter -InputObject $info -PropertyName 'Name') | Should -Be $null
|
|
|
|
|
$info.AccessInheritanceEnabled | Should -BeTrue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# The Authz access check needs a descriptor with an owner and a group, like any real file has; the minimal
|
|
|
|
|
# descriptor of the other tests has neither, and the call fails with "The parameter is incorrect" for either.
|
|
|
|
|
It 'Should return the effective access of a <Kind> without an item, with a null FullName on the result' -ForEach @(
|
|
|
|
|
@{ Kind = 'FileSecurity' }
|
|
|
|
|
@{ Kind = 'DirectorySecurity' }
|
|
|
|
|
) {
|
|
|
|
|
$raw = New-RawFileSystemSecurity -Kind $Kind
|
|
|
|
|
$raw.SetOwner($sid)
|
|
|
|
|
$raw.SetGroup($sid)
|
|
|
|
|
$sd = [Security2.FileSystemSecurity2] $raw
|
|
|
|
|
|
|
|
|
|
$result = [Security2.EffectiveAccess]::GetEffectiveAccess($sd, $identity, $null)
|
|
|
|
|
|
|
|
|
|
if ($result.OperationFailed) {
|
|
|
|
|
Set-ItResult -Skipped -Because "the Authz call could not run in this configuration: $($result.AuthzException.Message)"
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$result.Ace.FullName | Should -Be $null
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
fix(security2): keep a descriptor without an item usable by the cmdlets that take a security descriptor
PowerShell's parameter binder uses the conversion that 3.1 fixed for
every -SecurityDescriptor parameter, so a descriptor that Get-Acl
returns, never meant for these cmdlets, now binds where it used to be
a binding error. Two more consequences of that surfaced:
With the default module setting GetInheritedFrom ($true), Get-NTFSAccess,
Get-NTFSAudit, Get-NTFSOrphanedAccess, and Get-NTFSOrphanedAudit stopped
with the InvalidOperationException that a previous fix added for a
descriptor without an item; a cmdlet user has no parameter to avoid it.
GetFileSystemAccessRules and GetFileSystemAuditRules now treat
getInheritedFrom as false for such a descriptor instead of throwing: the
entries come back with no source, the same as an entry whose source
Windows cannot name.
The private constructor that wraps a descriptor directly marked every
section, including Audit, as read; Get-Acl never reads Audit unless
-Audit is given. This made HasAuditSection wrongly report true, so the
existing #109 check let Get-NTFSAudit go on with no audit data instead
of reporting that the descriptor has none, and let Get-NTFSInheritance
report AuditInheritanceEnabled as a real value instead of null
(unknown). The constructor now includes Audit in the sections only when
the descriptor's binary form actually carries a SACL, the same test
HasSystemAcl already uses.
Get-NTFSOwner, Get-NTFSSimpleAccess, Get-NTFSOrphanedAccess,
Get-NTFSEffectiveAccess, and Add-/Remove-NTFSAccess with such a
descriptor already worked; Set-NTFSSecurityDescriptor already reported
a clear, non-terminating error. A review finding on the earlier fix is
also addressed: two assertions read a property through reflection
instead of a plain PowerShell property access, which silently turns a
throwing getter into $null without a trace and so could not tell a
getter that throws from one that legitimately returns null.
Every existing behavior for an item that is not null is unchanged.
Public signatures are unchanged.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
1 week ago
|
|
|
# Review finding: PowerShell's binder uses the fixed conversion of 3.1 for every -SecurityDescriptor parameter, so a
|
|
|
|
|
# descriptor that Get-Acl returns, never meant for these cmdlets, now binds where it used to be a binding error. These
|
|
|
|
|
# pin the four cmdlets the review named; the one-line probes of the others are in the final report, not here.
|
|
|
|
|
Describe 'Cmdlets with a security descriptor that Get-Acl returns' {
|
|
|
|
|
BeforeAll {
|
|
|
|
|
$plainFile = New-TestSandboxItem -Sandbox $sandbox -Name 'GetAclPlain'
|
|
|
|
|
Add-NTFSAccess -Path $plainFile -Account 'S-1-1-0' -AccessRights ReadData -AppliesTo ThisFolderOnly
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
It 'Should return the explicit access entries and no error for Get-NTFSAccess -SecurityDescriptor (Get-Acl ...)' {
|
|
|
|
|
$sd = Get-Acl -LiteralPath $plainFile
|
|
|
|
|
$expected = @($sd.GetAccessRules($true, $false, [System.Security.Principal.SecurityIdentifier]))
|
|
|
|
|
|
|
|
|
|
$result = @(Get-NTFSAccess -SecurityDescriptor $sd -ExcludeInherited -ErrorVariable accessErrors -ErrorAction SilentlyContinue)
|
|
|
|
|
|
|
|
|
|
$accessErrors | Should -BeNullOrEmpty
|
|
|
|
|
($result.Account.Sid | Sort-Object) | Should -Be ($expected.IdentityReference.Value | Sort-Object)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
It 'Should write the #109 ReadSecurityError, not an empty result, for Get-NTFSAudit -SecurityDescriptor (Get-Acl ...) without -Audit' {
|
|
|
|
|
$sd = Get-Acl -LiteralPath $plainFile
|
|
|
|
|
|
|
|
|
|
$result = @(Get-NTFSAudit -SecurityDescriptor $sd -ErrorVariable auditErrors -ErrorAction SilentlyContinue)
|
|
|
|
|
|
|
|
|
|
$result | Should -BeNullOrEmpty
|
|
|
|
|
$auditErrors | Should -HaveCount 1
|
|
|
|
|
$auditErrors[0].FullyQualifiedErrorId | Should -BeLike 'ReadSecurityError,*'
|
|
|
|
|
$auditErrors[0].CategoryInfo.Category | Should -Be 'InvalidData'
|
|
|
|
|
$auditErrors[0].Exception.Message | Should -BeLike "*doesn't contain the audit entries*"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
It 'Should give a null AuditInheritanceEnabled and the right AccessInheritanceEnabled for Get-NTFSInheritance -SecurityDescriptor (Get-Acl ...)' {
|
|
|
|
|
$sd = Get-Acl -LiteralPath $plainFile
|
|
|
|
|
|
|
|
|
|
$result = Get-NTFSInheritance -SecurityDescriptor $sd -ErrorAction Stop
|
|
|
|
|
|
|
|
|
|
$result.AuditInheritanceEnabled | Should -Be $null
|
|
|
|
|
$result.AccessInheritanceEnabled | Should -Be (-not $sd.AreAccessRulesProtected)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
It 'Should write a non-terminating error naming the missing item for Set-NTFSSecurityDescriptor -SecurityDescriptor (Get-Acl ...), and change nothing' {
|
|
|
|
|
$sd = Get-Acl -LiteralPath $plainFile
|
|
|
|
|
$sd.AddAccessRule((New-Object -TypeName 'System.Security.AccessControl.FileSystemAccessRule' -ArgumentList (
|
|
|
|
|
$sid, [System.Security.AccessControl.FileSystemRights]::WriteData,
|
|
|
|
|
[System.Security.AccessControl.AccessControlType]::Allow
|
|
|
|
|
)))
|
|
|
|
|
$before = (Get-Acl -LiteralPath $plainFile).Sddl
|
|
|
|
|
|
|
|
|
|
$null = Set-NTFSSecurityDescriptor -SecurityDescriptor $sd -ErrorVariable sdErrors -ErrorAction SilentlyContinue
|
|
|
|
|
|
|
|
|
|
$sdErrors | Should -HaveCount 1
|
|
|
|
|
$sdErrors[0].FullyQualifiedErrorId | Should -BeLike 'WriteSdError,*'
|
|
|
|
|
$sdErrors[0].Exception.Message | Should -BeLike '*no item*'
|
|
|
|
|
(Get-Acl -LiteralPath $plainFile).Sddl | Should -BeExactly $before
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
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]
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|