diff --git a/.github/scripts/Invoke-TestsAsBasicUser.ps1 b/.github/scripts/Invoke-TestsAsBasicUser.ps1 index 93d1d82..60d33c6 100644 --- a/.github/scripts/Invoke-TestsAsBasicUser.ps1 +++ b/.github/scripts/Invoke-TestsAsBasicUser.ps1 @@ -14,8 +14,8 @@ Specifies the path of the result file in the NUnit format. .PARAMETER Title - Specifies the heading of the test results in the job summary. It can't contain a double quote or a percent sign, - which would change the command line of cmd.exe. + Specifies the heading of the test results in the job summary. It can't contain a double quote, a percent sign, or a + line break, which would change the command line of cmd.exe. .EXAMPLE .\.github\scripts\Invoke-TestsAsBasicUser.ps1 -ResultPath TestResults\WindowsPowerShell-BasicUser.xml -Title 'Windows PowerShell 5.1 as a basic user' @@ -31,8 +31,9 @@ param ( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] - # cmd.exe gets the title in a quoted argument and expands environment variables also inside quotes. - [ValidatePattern('^[^"%]+$')] + # cmd.exe gets the title in a quoted argument, expands environment variables also inside quotes, and ends the + # command at a line break. \z, unlike $, doesn't match before a final line feed. + [ValidatePattern('\A[^"%\r\n]+\z')] [string] $Title ) diff --git a/CHANGELOG.md b/CHANGELOG.md index 137c644..9659ecb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -321,7 +321,8 @@ The format is based on `Select-Object -Unique` and `Compare-Object`, stopped with an `InvalidCastException`, and a security descriptor as the key of a hashtable with a `NullReferenceException`. Two objects are now equal when - they hold the same entry or descriptor, as in .NET + they hold the same entry or descriptor, as in .NET. Converting a security + descriptor to `FileSecurity` or `DirectorySecurity` returned `$null` - Fix `InheritedFrom` of `Get-NTFSAccess` and `Get-NTFSAudit`: with `-ExcludeExplicit`, each inherited entry showed the source of another entry, and `Get-NTFSAccess -SecurityDescriptor` stopped with an diff --git a/Security2/FileSystem/FileSystemAccessRule2 Class/FileSystemAccessRule2.cs b/Security2/FileSystem/FileSystemAccessRule2 Class/FileSystemAccessRule2.cs index 7cf84a4..bcba319 100644 --- a/Security2/FileSystem/FileSystemAccessRule2 Class/FileSystemAccessRule2.cs +++ b/Security2/FileSystem/FileSystemAccessRule2 Class/FileSystemAccessRule2.cs @@ -48,13 +48,12 @@ namespace Security2 return new FileSystemAccessRule2(ace); } - // Like the entries of .NET, two objects are equal when they wrap the same entry. A cast instead of "as" - // threw an InvalidCastException for any other type, also for -eq in PowerShell. + // Like the entries of .NET, two objects are equal when they wrap the same entry; an entry of .NET is never equal, + // so that equality is the same in both directions. A cast instead of "as" threw an InvalidCastException. public override bool Equals(object obj) { var other = obj as FileSystemAccessRule2; - var rule = other != null ? other.fileSystemAccessRule : obj as FileSystemAccessRule; - return rule != null && ReferenceEquals(fileSystemAccessRule, rule); + return other != null && ReferenceEquals(fileSystemAccessRule, other.fileSystemAccessRule); } public override int GetHashCode() { diff --git a/Security2/FileSystem/FileSystemAuditRule2 Class/FileSystemAuditRule2.cs b/Security2/FileSystem/FileSystemAuditRule2 Class/FileSystemAuditRule2.cs index 1253d90..f8c729f 100644 --- a/Security2/FileSystem/FileSystemAuditRule2 Class/FileSystemAuditRule2.cs +++ b/Security2/FileSystem/FileSystemAuditRule2 Class/FileSystemAuditRule2.cs @@ -48,13 +48,12 @@ namespace Security2 { return new FileSystemAuditRule2(ace); } - // Like the entries of .NET, two objects are equal when they wrap the same entry. A cast instead of "as" - // threw an InvalidCastException for any other type, also for -eq in PowerShell. + // Like the entries of .NET, two objects are equal when they wrap the same entry; an entry of .NET is never equal, + // so that equality is the same in both directions. A cast instead of "as" threw an InvalidCastException. public override bool Equals(object obj) { var other = obj as FileSystemAuditRule2; - var rule = other != null ? other.fileSystemAuditRule : obj as FileSystemAuditRule; - return rule != null && ReferenceEquals(fileSystemAuditRule, rule); + return other != null && ReferenceEquals(fileSystemAuditRule, other.fileSystemAuditRule); } public override int GetHashCode() { diff --git a/Security2/FileSystem/FileSystemSecurity2.cs b/Security2/FileSystem/FileSystemSecurity2.cs index 39e5c8c..b984d7d 100644 --- a/Security2/FileSystem/FileSystemSecurity2.cs +++ b/Security2/FileSystem/FileSystemSecurity2.cs @@ -7,8 +7,6 @@ namespace Security2 { public class FileSystemSecurity2 { - protected FileSecurity fileSecurityDescriptor; - protected DirectorySecurity directorySecurityDescriptor; protected FileSystemInfo item; protected FileSystemSecurity sd; protected AccessControlSections sections; @@ -236,9 +234,11 @@ namespace Security2 } #region Conversion + // The descriptor is in sd. Before 5.0.0-rc6, these conversions returned fields that were never set, so they + // gave null, and the hash code threw a NullReferenceException. public static implicit operator FileSecurity(FileSystemSecurity2 fs2) { - return fs2.fileSecurityDescriptor; + return fs2.sd as FileSecurity; } public static implicit operator FileSystemSecurity2(FileSecurity fs) { @@ -247,21 +247,20 @@ namespace Security2 public static implicit operator DirectorySecurity(FileSystemSecurity2 fs2) { - return fs2.directorySecurityDescriptor; + return fs2.sd as DirectorySecurity; } public static implicit operator FileSystemSecurity2(DirectorySecurity fs) { return new FileSystemSecurity2(new DirectoryInfo("")); } - // Like the descriptors of .NET, two objects are equal when they wrap the same descriptor. The descriptor is in - // sd; fileSecurityDescriptor is never set, so its hash code threw a NullReferenceException, and the cast an - // InvalidCastException for any other type. + // Like the descriptors of .NET, two objects are equal when they wrap the same descriptor; a descriptor of .NET + // is never equal, so that equality is the same in both directions. A cast instead of "as" threw an + // InvalidCastException. public override bool Equals(object obj) { var other = obj as FileSystemSecurity2; - var descriptor = other != null ? other.sd : obj as FileSystemSecurity; - return descriptor != null && ReferenceEquals(sd, descriptor); + return other != null && ReferenceEquals(sd, other.sd); } public override int GetHashCode() { diff --git a/Tests/Access.Tests.ps1 b/Tests/Access.Tests.ps1 index deafdc6..7431465 100644 --- a/Tests/Access.Tests.ps1 +++ b/Tests/Access.Tests.ps1 @@ -809,6 +809,15 @@ Describe 'Comparing access entries' { @(@($entries) + $entries[0] | Select-Object -Unique) | Should -HaveCount $entries.Count Compare-Object -ReferenceObject $entries -DifferenceObject $entries | Should -BeNullOrEmpty } + + # The entry of .NET doesn't know the object of the module, so equality in one direction only would make a hashtable + # lookup depend on which of the two is the key. + It 'Should be equal only to an entry of the module, in both directions' { + $raw = [System.Security.AccessControl.FileSystemAccessRule] $entries[0] + + $entries[0].Equals($raw) | Should -BeFalse + $raw.Equals($entries[0]) | Should -BeFalse + } } # Before 5.0.0-rc6, -ExcludeExplicit gave each inherited entry the source of another entry, and Get-NTFSAccess stopped @@ -840,6 +849,25 @@ Describe 'InheritedFrom of access entries' { } } + # Two explicit entries before the inherited ones; before 5.0.0-rc6, -ExcludeExplicit shifted the sources by two. + It 'Should name the folder of an inheritable entry, also with -ExcludeExplicit' { + $parent = New-TestSandboxItem -Sandbox $sandbox -Name 'InheritedFromParent' -Directory + Add-NTFSAccess -Path $parent -Account 'S-1-5-32-546' -AccessRights ReadData -AppliesTo ThisFolderSubfoldersAndFiles + $child = Join-Path -Path $parent -ChildPath 'Child.txt' + Assert-TestSandboxPath -Sandbox $sandbox -Path $child + Set-Content -LiteralPath $child -Value 'Child' + Add-NTFSAccess -Path $child -Account 'S-1-1-0' -AccessRights ReadData + Add-NTFSAccess -Path $child -Account 'S-1-5-32-545' -AccessRights ReadData + + $all = @(Get-NTFSAccess -Path $child | Where-Object -FilterScript { $_.IsInherited -and "$($_.Account.Sid)" -eq 'S-1-5-32-546' }) + $inherited = @(Get-NTFSAccess -Path $child -ExcludeExplicit | Where-Object -FilterScript { "$($_.Account.Sid)" -eq 'S-1-5-32-546' }) + + $all | Should -HaveCount 1 + $all[0].InheritedFrom | Should -Be $parent + $inherited | Should -HaveCount 1 + $inherited[0].InheritedFrom | Should -Be $parent + } + It 'Should read a security descriptor with audit entries and name the same folders' -Skip:(-not $holdsSecurityPrivilege) { $file = New-TestSandboxItem -Sandbox $sandbox -Name 'InheritedFromAudit' Add-NTFSAccess -Path $file -Account 'S-1-1-0' -AccessRights ReadData diff --git a/Tests/Repository.Tests.ps1 b/Tests/Repository.Tests.ps1 index 66ce3f1..d13c8cd 100644 --- a/Tests/Repository.Tests.ps1 +++ b/Tests/Repository.Tests.ps1 @@ -117,9 +117,20 @@ Describe 'Invoke-TestsAsBasicUser.ps1' { $bindParameters = [scriptblock]::Create($ast.ParamBlock.Extent.Text) } - # The title goes into a quoted argument of cmd.exe, which expands environment variables also inside quotes. - It 'Should refuse a title with the character <_>, which would change the command line of cmd.exe' -ForEach @('%', '"') { - { & $bindParameters -ResultPath 'TestResults\Refused.xml' -Title "CI run $_ 1" } | + # The title goes into a quoted argument of cmd.exe, which expands environment variables also inside quotes and ends + # the command at a line break. + It 'Should refuse a title with the character , which would change the command line of cmd.exe' -ForEach @( + @{ Name = '%'; Character = '%' } + @{ Name = 'double quote'; Character = '"' } + @{ Name = 'line feed'; Character = "`n" } + @{ Name = 'carriage return'; Character = "`r" } + ) { + { & $bindParameters -ResultPath 'TestResults\Refused.xml' -Title "CI run $Character 1" } | + Should -Throw -ExpectedMessage "*'Title'*" + } + + It 'Should refuse a title that ends with a line break' { + { & $bindParameters -ResultPath 'TestResults\Refused.xml' -Title "CI run`n" } | Should -Throw -ExpectedMessage "*'Title'*" } diff --git a/Tests/SecurityDescriptor.Tests.ps1 b/Tests/SecurityDescriptor.Tests.ps1 index 5fad078..6be3eeb 100644 --- a/Tests/SecurityDescriptor.Tests.ps1 +++ b/Tests/SecurityDescriptor.Tests.ps1 @@ -311,4 +311,24 @@ Describe 'Comparing security descriptors' { $table[$sd] | Should -Be 'first' $table.Count | Should -Be 2 } + + # Before 5.0.0-rc6, the conversion returned a field that was never set, so it gave $null. + It 'Should convert to the security object of .NET that it holds' { + $file = New-TestSandboxItem -Sandbox $sandbox -Name 'ConvertFile' + $folder = New-TestSandboxItem -Sandbox $sandbox -Name 'ConvertFolder' -Directory + $fileSd = Get-NTFSSecurityDescriptor -Path $file + $folderSd = Get-NTFSSecurityDescriptor -Path $folder + + [object]::ReferenceEquals([System.Security.AccessControl.FileSecurity] $fileSd, $fileSd.SecurityDescriptor) | Should -BeTrue + [object]::ReferenceEquals([System.Security.AccessControl.DirectorySecurity] $folderSd, $folderSd.SecurityDescriptor) | Should -BeTrue + } + + It 'Should be equal only to a descriptor of the module, in both directions' { + $file = New-TestSandboxItem -Sandbox $sandbox -Name 'Symmetric' + $sd = Get-NTFSSecurityDescriptor -Path $file + $raw = $sd.SecurityDescriptor + + $sd.Equals($raw) | Should -BeFalse + $raw.Equals($sd) | Should -BeFalse + } }