diff --git a/Tests/ObjectApis.Tests.ps1 b/Tests/ObjectApis.Tests.ps1 index bf508ca..84a8030 100644 --- a/Tests/ObjectApis.Tests.ps1 +++ b/Tests/ObjectApis.Tests.ps1 @@ -6,6 +6,11 @@ )] 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' @@ -322,3 +327,386 @@ Describe 'Simplified entry comparison branches' { [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 { + 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 { + $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 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') + } + + # The overload for several accounts is an iterator, so it writes nothing until the caller enumerates the result. + It 'Should add the entries of several accounts to a by its path only when the result is 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 -BeNullOrEmpty + @(Get-ExplicitEntries -Path $path -Account 'S-1-5-32-545') | Should -BeNullOrEmpty + @($pending) | Should -HaveCount 2 + @(Get-ExplicitEntries -Path $path) | Should -HaveCount 1 + @(Get-ExplicitEntries -Path $path -Account 'S-1-5-32-545') | Should -HaveCount 1 + } + + 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 with removeSpecific and the named rights without it' { + $path = New-TestSandboxItem -Sandbox $sandbox -Name 'RemoveByPath' + [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 by path' { + $path = New-TestSandboxItem -Sandbox $sandbox -Name 'RemoveListByPath' + $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 { + 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 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) + } + + It 'Should add the entries of several accounts to a by its path only when the result is 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 -BeNullOrEmpty + @($pending) | Should -HaveCount 2 + @(Get-AuditEntries -Path $path) | Should -HaveCount 1 + @(Get-AuditEntries -Path $path -Account 'S-1-5-32-545') | Should -HaveCount 1 + [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 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 + } + + It 'Should block and restore the audit inheritance of a 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 + } + + 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' + } +} + +Describe 'Generic access rights and identity errors' { + It 'Should map the generic mask to the file system rights ' -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 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 + } +} diff --git a/Tests/Privileges.Tests.ps1 b/Tests/Privileges.Tests.ps1 index e75e18f..bc311f8 100644 --- a/Tests/Privileges.Tests.ps1 +++ b/Tests/Privileges.Tests.ps1 @@ -262,3 +262,121 @@ Describe 'Privileges that another command in the pipeline changes' { Get-EnabledFileSystemPrivilege | Should -BeNullOrEmpty } } + +# The library class of the module that the cmdlets leave unused; the tests change only the privileges of the test process. +Describe 'The PrivilegeEnabler class' { + BeforeAll { + $privateData['EnablePrivileges'] = $false + $backup = [ProcessPrivileges.Privilege]::Backup + $currentProcess = [System.Diagnostics.Process]::GetCurrentProcess() + } + + AfterAll { + $privateData['EnablePrivileges'] = $enablePrivileges + } + + BeforeEach { + Disable-Privileges -ErrorAction SilentlyContinue -WarningAction SilentlyContinue + } + + AfterEach { + Disable-Privileges -ErrorAction SilentlyContinue -WarningAction SilentlyContinue + } + + It 'Should enable a disabled privilege until it is disposed' -Skip:(-not $holdsPrivileges) { + Get-BackupPrivilegeState | Should -Be 'Disabled' + + $enabler = New-Object -TypeName 'ProcessPrivileges.PrivilegeEnabler' -ArgumentList $currentProcess, $backup + try { + Get-BackupPrivilegeState | Should -Be 'Enabled' + } + finally { + $enabler.Dispose() + } + + Get-BackupPrivilegeState | Should -Be 'Disabled' + $enabler.Dispose() + Get-BackupPrivilegeState | Should -Be 'Disabled' + } + + It 'Should report a privilege that it modified once and leave it to the instance that enabled it' -Skip:(-not $holdsPrivileges) { + $first = New-Object -TypeName 'ProcessPrivileges.PrivilegeEnabler' -ArgumentList $currentProcess + $second = New-Object -TypeName 'ProcessPrivileges.PrivilegeEnabler' -ArgumentList $currentProcess + try { + $first.EnablePrivilege($backup) | Should -Be 'PrivilegeModified' + Get-BackupPrivilegeState | Should -Be 'Enabled' + $first.EnablePrivilege($backup) | Should -Be 'None' + $second.EnablePrivilege($backup) | Should -Be 'None' + $second.Dispose() + Get-BackupPrivilegeState | Should -Be 'Enabled' + } + finally { + $first.Dispose() + $second.Dispose() + } + + Get-BackupPrivilegeState | Should -Be 'Disabled' + } + + It 'Should not disable a privilege that was enabled before' -Skip:(-not $holdsPrivileges) { + $null = [ProcessPrivileges.ProcessExtensions]::EnablePrivilege($currentProcess, $backup) + + $enabler = New-Object -TypeName 'ProcessPrivileges.PrivilegeEnabler' -ArgumentList $currentProcess, $backup + try { + $enabler.EnablePrivilege($backup) | Should -Be 'None' + } + finally { + $enabler.Dispose() + } + + Get-BackupPrivilegeState | Should -Be 'Enabled' + } + + It 'Should enable a privilege through an access token handle that the caller owns' -Skip:(-not $holdsPrivileges) { + $rights = [ProcessPrivileges.TokenAccessRights]::AdjustPrivileges -bor [ProcessPrivileges.TokenAccessRights]::Query + $handle = [ProcessPrivileges.ProcessExtensions]::GetAccessTokenHandle($currentProcess, $rights) + try { + $enabler = New-Object -TypeName 'ProcessPrivileges.PrivilegeEnabler' -ArgumentList $handle, $backup + Get-BackupPrivilegeState | Should -Be 'Enabled' + $enabler.Dispose() + + Get-BackupPrivilegeState | Should -Be 'Disabled' + $handle.IsClosed | Should -BeFalse + } + finally { + $handle.Dispose() + } + } + + # The access tokens of administrators don't hold the privilege to create a token, and those of basic users don't hold + # most of the others. + It 'Should leave a privilege that the access token does not hold alone' { + $removed = [ProcessPrivileges.Privilege]::CreateToken + [ProcessPrivileges.ProcessExtensions]::GetPrivilegeState($currentProcess, $removed) | Should -Be 'Removed' + + $enabler = New-Object -TypeName 'ProcessPrivileges.PrivilegeEnabler' -ArgumentList $currentProcess + try { + $enabler.EnablePrivilege($removed) | Should -Be 'None' + } + finally { + $enabler.Dispose() + } + + [ProcessPrivileges.ProcessExtensions]::GetPrivilegeState($currentProcess, $removed) | Should -Be 'Removed' + } + + # The enabled flag decides first, then the removed flag; the attributes are not a flags enumeration in .NET. + It 'Should derive the state from the attribute value ' -ForEach @( + @{ Value = 0; Expected = 'Disabled' } + @{ Value = 1; Expected = 'Disabled' } + @{ Value = 2; Expected = 'Enabled' } + @{ Value = 3; Expected = 'Enabled' } + @{ Value = 4; Expected = 'Removed' } + @{ Value = 6; Expected = 'Enabled' } + @{ Value = -2147483648; Expected = 'Disabled' } + ) { + $attributes = [Enum]::ToObject([ProcessPrivileges.PrivilegeAttributes], $Value) + + [ProcessPrivileges.ProcessExtensions]::GetPrivilegeState($attributes) | Should -Be $Expected + } +}