Carbon.FileSystem.psm1


using namespace System.Security.AccessControl
using namespace System.Collections

# Copyright WebMD Health Services
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License

#Requires -Version 5.1
Set-StrictMode -Version 'Latest'

# Functions should use $moduleRoot as the relative root from which to find
# things. A published module has its function appended to this file, while a
# module in development has its functions in the Functions directory.
$moduleRoot = $PSScriptRoot

Import-Module -Name (Join-Path -Path $moduleRoot -ChildPath 'Modules\Carbon.Security' -Resolve) `
              -Function @('Get-CPermission', 'Grant-CPermission', 'Revoke-CPermission', 'Test-CPermission') `
              -Verbose:$false

Add-Type @'
using System;
using System.Text;
using System.Collections.Generic;
using System.Runtime.InteropServices;
 
namespace Carbon.FileSystem
{
  public static class Kernel32
  {
 
    #region WinAPI P/Invoke declarations
    [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
    public static extern IntPtr FindFirstFileNameW(string lpFileName, uint dwFlags, ref uint StringLength, StringBuilder LinkName);
 
    [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
    public static extern bool FindNextFileNameW(IntPtr hFindStream, ref uint StringLength, StringBuilder LinkName);
 
    [DllImport("kernel32.dll", SetLastError = true)]
    public static extern bool FindClose(IntPtr hFindFile);
 
    [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
    public static extern bool GetVolumePathName(string lpszFileName, [Out] StringBuilder lpszVolumePathName, uint cchBufferLength);
 
    public static readonly IntPtr INVALID_HANDLE_VALUE = (IntPtr)(-1); // 0xffffffff;
    public const int MAX_PATH = 65535; // Max. NTFS path length.
    #endregion
   }
}
'@


# Store each of your module's functions in its own file in the Functions
# directory. On the build server, your module's functions will be appended to
# this file, so only dot-source files that exist on the file system. This allows
# developers to work on a module without having to build it first. Grab all the
# functions that are in their own files.
$functionsPath = Join-Path -Path $moduleRoot -ChildPath 'Functions\*.ps1'
if( (Test-Path -Path $functionsPath) )
{
    foreach( $functionPath in (Get-Item $functionsPath) )
    {
        . $functionPath.FullName
    }
}



function ConvertTo-CarbonSecurityApplyTo
{
    [CmdletBinding()]
    param(
        [Parameter(Mandatory, ValueFromPipeline)]
        [AllowEmptyString()]
        [AllowNull()]
        [ValidateSet('FolderOnly', 'FolderSubfoldersAndFiles', 'FolderAndSubfolders', 'FolderAndFiles',
            'SubfoldersAndFilesOnly', 'SubfoldersOnly', 'FilesOnly')]
        [String] $ApplyTo
    )

    process
    {
        $map = @{
            'FolderOnly' = 'ContainerOnly';
            'FolderSubfoldersAndFiles' = 'ContainerSubcontainersAndLeaves';
            'FolderAndSubfolders' = 'ContainerAndSubcontainers';
            'FolderAndFiles' = 'ContainerAndLeaves';
            'SubfoldersAndFilesOnly' = 'SubcontainersAndLeavesOnly';
            'SubfoldersOnly' = 'SubcontainersOnly';
            'FilesOnly' = 'LeavesOnly';
        }

        if (-not $ApplyTo)
        {
            return
        }

        return $map[$ApplyTo]
    }
}


function Get-CNtfsHardLink
{
    <#
    .SYNOPSIS
    Retrieves hard link targets from a file.
 
    .DESCRIPTION
    Get-CNtfsHardLink retrieves hard link targets from a file given a file path. This fixes compatibility issues between
    Windows PowerShell and PowerShell Core when retrieving targets from a hard link.
 
    .EXAMPLE
    Get-CNtfsHardLink -Path $Path
 
    Demonstrates how to retrieve a hard link given a file path.
    #>

    [CmdletBinding()]
    param(
        # The path whose hard links to get/return. Must exist.
        [Parameter(Mandatory)]
        [String] $Path
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

    if( -not (Resolve-Path -LiteralPath $Path) )
    {
        return
    }

    try
    {
        $sbPath = [Text.StringBuilder]::New([Carbon.FileSystem.Kernel32]::MAX_PATH)
        $charCount = [uint32]$sbPath.Capacity; # in/out character-count variable for the WinAPI calls.
        # Get the volume (drive) part of the target file's full path (e.g., @"C:\")
        [void][Carbon.FileSystem.Kernel32]::GetVolumePathName($Path, $sbPath, $charCount)
        $volume = $sbPath.ToString();
        # Trim the trailing "\" from the volume path, to enable simple concatenation
        # with the volume-relative paths returned by the FindFirstFileNameW() and FindFirstFileNameW() functions,
        # which have a leading "\"
        $volume = $volume.Substring(0, $volume.Length - 1);
        # Loop over and collect all hard links as their full paths.
        [IntPtr]$findHandle = [IntPtr]::Zero
        $findHandle = [Carbon.FileSystem.Kernel32]::FindFirstFileNameW($Path, 0, [ref]$charCount, $sbPath)
        if( [Carbon.FileSystem.Kernel32]::INVALID_HANDLE_VALUE -eq $findHandle)
        {
            $errorCode = [Runtime.InteropServices.Marshal]::GetLastWin32Error()
            $msg = "Failed to find hard links to path ""$($Path | Split-Path -Relative)"": the system error code is ""$($errorCode)""."
            Write-Error $msg -ErrorAction $ErrorActionPreference
            return
        }

        do
        {
            Join-Path -Path $volume -ChildPath $sbPath.ToString() | Write-Output # Add the full path to the result list.
            $charCount = [uint32]$sbPath.Capacity; # Prepare for the next FindNextFileNameW() call.
        }
        while( [Carbon.FileSystem.Kernel32]::FindNextFileNameW($findHandle, [ref]$charCount, $sbPath) )
        [void][Carbon.FileSystem.Kernel32]::FindClose($findHandle);
    }
    catch
    {
        Write-Error -Message $_ -ErrorAction $ErrorActionPreference
    }
}

function Get-FileHardLink
{
    <#
    .SYNOPSIS
    ***OBSOLETE.*** Use Get-CNtfsHardLink instead.
 
    .DESCRIPTION
    ***OBSOLETE.*** Use Get-CNtfsHardLink instead.
 
    .EXAMPLE
    Get-CNtfsHardLink -Path $Path
 
    Demonstrates that you should use `Get-CNtfsHardLink` instead.
    #>

    [CmdletBinding()]
    param(
        # The path whose hard links to get/return. Must exist.
        [Parameter(Mandatory)]
        [String] $Path
    )

    $msg = 'The Get-FileHardLink function is obsolete and will removed in the next major version of ' +
           'Carbon.FileSystem. Please use Get-CNtfsHardLink instead.'
    Write-Warning -Message $msg

    Get-CNtfsHardLink @PSBoundParameters
}


function Get-CNtfsPermission
{
    <#
    .SYNOPSIS
    Gets the permissions (access control rules) for a file or directory.
 
    .DESCRIPTION
    The `Get-CNtfsPermission` function gets permissions on a file or directory. Permissions returned are the
    `[Security.AccessControl.FileSystemAccessRule]` objects from the file/directory's ACL. By default, all non-inherited
    permissions are returned. Pass the path to the file/directory whose permissions to get to the `Path` parameter. To
    also get inherited permissions, use the `Inherited` switch.
 
    To get the permissions a specific identity has on the file/directory, pass that username/group name to the
    `Identity` parameter. If the identity doesn't exist, or it doesn't have any permissions, no error is written and
    nothing is returned.
 
    .OUTPUTS
    System.Security.AccessControl.FileSystemAccessRule.
 
    .LINK
    Get-CNtfsPermission
 
    .LINK
    Grant-CNtfsPermission
 
    .LINK
    Revoke-CNtfsPermission
 
    .LINK
    Test-CNtfsPermission
 
    .EXAMPLE
    Get-CNtfsPermission -Path 'C:\Windows'
 
    Returns `System.Security.AccessControl.FileSystemAccessRule` objects for all the non-inherited rules on
    `C:\windows`.
 
    .EXAMPLE
    Get-CNtfsPermission -Path 'C:\Windows' -Inherited
 
    Returns `System.Security.AccessControl.RegistryAccessRule` objects for all the inherited and non-inherited rules on
    `hklm:\software`.
 
    .EXAMPLE
    Get-CNtfsPermission -Path 'C:\Windows' -Idenity Administrators
 
    Returns `System.Security.AccessControl.FileSystemAccessRule` objects for all the `Administrators'` rules on
    `C:\windows`.
    #>

    [CmdletBinding()]
    [OutputType([Security.AccessControl.FileSystemAccessRule])]
    param(
        # The path to the file/directory whose permissions (i.e. access control rules) to return. Wildcards supported.
        [Parameter(Mandatory, ValueFromPipeline)]
        [String] $Path,

        # The identity whose permissiosn (i.e. access control rules) to return. By default, all non-inherited
        # permissions are returned.
        [String] $Identity,

        # Return inherited permissions in addition to explicit permissions.
        [switch] $Inherited
    )

    process
    {
        Set-StrictMode -Version 'Latest'
        Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState

        Get-CPermission @PSBoundParameters
    }
}


function Grant-CNtfsPermission
{
    <#
    .SYNOPSIS
    Grants permission on folders and files.
 
    .DESCRIPTION
    The `Grant-CNtfsPermission` functions grants permissions to folders and files. Pass the folder/file path to the
    `Path` parameter, the user/group name to the `Identity` parameter, and the permissions to the `Permission`
    parameter. By default, the permissions are applied to the folder and inherited to all its subfolders and files. To
    control how the permissions are applied, use the `ApplyTo` parameter. If you want permissions to only apply to child
    files and folders, use the `OnlyApplyToChildFilesAndFolders` switch.
 
    By default, an "Allow" permission is granted. To add a "Deny" permission, set the value of the `Type` parameter to
    `Deny`.
 
    All existing, non-inherited permissions for the given identity are removed first. If you want to preserve a
    user/group's existing permissions, use the `Append` switch.
 
    To remove *all* non-inherited permissions except the permission being granted, use the `Clear` switch.
 
    The permission is only granted if it doesn't exist. To always grant the permission, use the `Force` switch.
 
    To get the permission back as a `[System.Security.AccessControl.FileSystemAccessRule]` object, use the `PassThru`
    switch.
 
    .OUTPUTS
    System.Security.AccessControl.FileSystemAccessRule.
 
    .LINK
    Get-CNtfsPermission
 
    .LINK
    Revoke-CNtfsPermission
 
    .LINK
    Test-CNtfsPermission
 
    .LINK
    http://msdn.microsoft.com/en-us/library/system.security.accesscontrol.filesystemrights.aspx
 
    .LINK
    http://msdn.microsoft.com/en-us/magazine/cc163885.aspx#S3
 
    .EXAMPLE
    Grant-CNtfsPermission -Identity ENTERPRISE\Engineers -Permission FullControl -Path C:\EngineRoom
 
    Grants the Enterprise's engineering group full control on the engine room. Very important if you want to get
    anywhere.
 
    .EXAMPLE
    Grant-CNtfsPermission -Identity ENTERPRISE\Engineers -Permission FullControl -Path C:\EngineRoom -Clear
 
    Grants the Enterprise's engineering group full control on the engine room. Any non-inherited, existing access rules
    are removed from `C:\EngineRoom`.
 
    .EXAMPLE
    Grant-CNtfsPermission -Identity BORG\Locutus -Permission FullControl -Path 'C:\EngineRoom' -Type Deny
 
    Demonstrates how to grant deny permissions on an objecy with the `Type` parameter.
 
    .EXAMPLE
    Grant-CNtfsPermission -Path C:\Bridge -Identity ENTERPRISE\Wesley -Permission 'Read' -ApplyTo ContainerAndSubContainersAndLeaves -Append
    Grant-CNtfsPermission -Path C:\Bridge -Identity ENTERPRISE\Wesley -Permission 'Write' -ApplyTo ContainerAndLeaves
    -Append
 
    Demonstrates how to grant multiple access rules to a single identity with the `Append` switch. In this case,
    `ENTERPRISE\Wesley` will be able to read everything in `C:\Bridge` and write only in the `C:\Bridge` directory, not
    to any sub-directory.
    #>

    [Diagnostics.CodeAnalysis.SuppressMessage('PSShouldProcess', '')]
    [CmdletBinding(SupportsShouldProcess, DefaultParameterSetName='DefaultAppliesToFlags')]
    [OutputType([Security.AccessControl.FileSystemAccessRule])]
    param(
        # The folder/file path on which the permissions should be granted.
        [Parameter(Mandatory)]
        [String] $Path,

        # The user or group getting the permissions.
        [Parameter(Mandatory)]
        [String] $Identity,

        # The permissions to grant. See
        # [System.Security.AccessControl.FileSystemRights](http://msdn.microsoft.com/en-us/library/system.security.accesscontrol.filesystemrights.aspx)
        # for the list of rights with descriptions.
        [Parameter(Mandatory)]
        [FileSystemRights[]] $Permission,

        # How to apply the permissions. The default is `FolderSubfoldersAndFiles`. Valid values are:
        #
        # * FolderOnly
        # * FolderSubfoldersAndFiles
        # * FolderAndSubfolders
        # * FolderAndFiles
        # * SubfoldersAndFilesOnly
        # * SubfoldersOnly
        # * FilesOnly
        [Parameter(Mandatory, ParameterSetName='SetAppliesToFlags')]
        [ValidateSet('FolderOnly', 'FolderSubfoldersAndFiles', 'FolderAndSubfolders', 'FolderAndFiles',
            'SubfoldersAndFilesOnly', 'SubfoldersOnly', 'FilesOnly')]
        [String] $ApplyTo,

        # Only apply the permissions to files and/or folders within the folder. Don't set this if the Path parameter is
        # to a file.
        [Parameter(ParameterSetName='SetAppliesToFlags')]
        [switch] $OnlyApplyToChildFilesAndFolders,

        # The type of rule to apply, either `Allow` or `Deny`. The default is `Allow`, which will allow access to the
        # item. The other option is `Deny`, which will deny access to the item.
        [AccessControlType] $Type = [AccessControlType]::Allow,

        # Removes all non-inherited permissions on the item.
        [switch] $Clear,

        # Returns an object representing the permission created or set on the `Path`. The returned object will have a
        # `Path` propery added to it so it can be piped to any cmdlet that uses a path.
        [switch] $PassThru,

        # Grants permissions, even if they are already present.
        [switch] $Force,

        # When set, adds the permissions as a new access rule instead of replacing any existing access rules.
        [switch] $Append
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState

    if (-not $ApplyTo)
    {
        $ApplyTo = 'FolderSubfoldersAndFiles'
    }

    $PSBoundParameters['ApplyTo'] = $ApplyTo | ConvertTo-CarbonSecurityApplyTo

    if ($PSBoundParameters.ContainsKey('OnlyApplyToChildFilesAndFolders'))
    {
        $PSBoundParameters.Remove('OnlyApplyToChildFilesAndFolders')
        $PSBoundParameters['OnlyApplyToChildren'] = $OnlyApplyToChildFilesAndFolders
    }

    Grant-CPermission @PSBoundParameters
}



function Revoke-CNtfsPermission
{
    <#
    .SYNOPSIS
    Revokes *explicit* permissions on folders and files.
 
    .DESCRIPTION
    Revokes all of user/group's *explicit* permissions on a folder or file. Only explicit permissions are considered;
    inherited permissions are ignored.
 
    If the identity doesn't have permission, nothing happens, not even errors written out.
 
    .LINK
    Get-CNtfsPermission
 
    .LINK
    Grant-CNtfsPermission
 
    .LINK
    Test-CNtfsPermission
 
    .EXAMPLE
    Revoke-CNtfsPermission -Identity ENTERPRISE\Engineers -Path 'C:\EngineRoom'
 
    Demonstrates how to revoke all of the 'Engineers' permissions on the `C:\EngineRoom` directory.
    #>

    [Diagnostics.CodeAnalysis.SuppressMessage('PSShouldProcess', '')]
    [CmdletBinding(SupportsShouldProcess)]
    param(
        # The folder or file path on which the permissions should be revoked.
        [Parameter(Mandatory)]
        [String] $Path,

        # The identity losing permissions.
        [Parameter(Mandatory)]
        [String] $Identity
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState

    Revoke-CPermission @PSBoundParameters
}



function Test-CNtfsPermission
{
    <#
    .SYNOPSIS
    Tests if permissions are set on a folder or file.
 
    .DESCRIPTION
    The `Test-CNtfsPermission` function tests if an identity has a permission on a file/folder. Pass the path to check
    to the `Path` parameter, the user/group name to the `Identity` parameter, and the permission to check for to the
    `Permission` parameter. If the user/group has the given permission on the given path, the function returns `$true`,
    otherwise it returns `$false`.
 
    Inherited permissions are *not* checked by default. To check inherited permission, use the `-Inherited` switch.
 
    By default, the permission check is not exact, i.e. the user may have additional permissions to what you're
    checking. If you want to make sure the user has *exactly* the permission you want, use the `-Strict` switch.
    Please note that by default, NTFS will automatically add/grant `Synchronize` permission on an item, which is handled
    by this function.
 
    You can also test how the permission is inherited by using the `ApplyTo` and `OnlyApplyToChildFilesAndFolders`
    parameters.
 
    .OUTPUTS
    System.Boolean.
 
    .LINK
    Get-CNtfsPermission
 
    .LINK
    Grant-CNtfsPermission
 
    .LINK
    Revoke-CNtfsPermission
 
    .LINK
    http://msdn.microsoft.com/en-us/library/system.security.accesscontrol.filesystemrights.aspx
 
    .EXAMPLE
    Test-CNtfsPermission -Identity 'STARFLEET\JLPicard' -Permission 'FullControl' -Path 'C:\Enterprise\Bridge'
 
    Demonstrates how to check that Jean-Luc Picard has `FullControl` permission on the `C:\Enterprise\Bridge`.
 
    .EXAMPLE
    Test-CNtfsPermission -Identity 'STARFLEET\Worf' -Permission 'Write' -ApplyTo 'FolderOnly' -Path 'C:\Enterprise\Brig'
 
    Demonstrates how to test for inheritance/propogation flags, in addition to permissions.
    #>

    [CmdletBinding(DefaultParameterSetName='SkipAppliesToFlags')]
    param(
        # The path to a folder/file on which the permissions should be checked.
        [Parameter(Mandatory)]
        [String] $Path,

        # The user or group name whose permissions to check.
        [Parameter(Mandatory)]
        [String] $Identity,

        # The permission to test for: e.g. FullControl, Read, etc. See
        # [System.Security.AccessControl.FileSystemRights](http://msdn.microsoft.com/en-us/library/system.security.accesscontrol.filesystemrights.aspx)
        # for the list of rights with descriptions.
        [Parameter(Mandatory)]
        [FileSystemRights[]] $Permission,

        # Checks how the permission is inherited. By default, the permission's inheritance is ignored.
        #
        # Valid values are:
        #
        # * FolderOnly
        # * FolderSubfoldersAndFiles
        # * FolderAndSubfolders
        # * FolderAndFiles
        # * SubfoldersAndFilesOnly
        # * SubfoldersOnly
        # * FilesOnly
        [Parameter(Mandatory, ParameterSetName='TestAppliesToFlags')]
        [ValidateSet('FolderOnly', 'FolderSubfoldersAndFiles', 'FolderAndSubfolders', 'FolderAndFiles',
            'SubfoldersAndFilesOnly', 'SubfoldersOnly', 'FilesOnly')]
        [String] $ApplyTo,

        # Checks that the permissions are only applied to child files and folders. By default, the permission's
        # inheritnace is ignored.
        [Parameter(ParameterSetName='TestAppliesToFlags')]
        [switch] $OnlyApplyToChildFilesAndFolders,

        # Include inherited permissions in the check.
        [switch] $Inherited,

        # Check for the exact permissions and how the permission is applied, i.e. make sure the identity has
        # *only* the permissions you specify.
        [switch] $Strict
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState

    if ($PSCmdlet.ParameterSetName -eq 'TestAppliesToFlags')
    {
        if ($ApplyTo)
        {
            $PSBoundParameters['ApplyTo'] = $ApplyTo | ConvertTo-CarbonSecurityApplyTo
        }
        $PSBoundParameters.Remove('OnlyApplyToChildFilesAndFolders') | Out-Null
        $PSBoundParameters['OnlyApplyToChildren'] = $OnlyApplyToChildFilesAndFolders
    }

    Test-CPermission @PSBoundParameters
}




function Use-CallerPreference
{
    <#
    .SYNOPSIS
    Sets the PowerShell preference variables in a module's function based on the callers preferences.
 
    .DESCRIPTION
    Script module functions do not automatically inherit their caller's variables, including preferences set by common
    parameters. This means if you call a script with switches like `-Verbose` or `-WhatIf`, those that parameter don't
    get passed into any function that belongs to a module.
 
    When used in a module function, `Use-CallerPreference` will grab the value of these common parameters used by the
    function's caller:
 
     * ErrorAction
     * Debug
     * Confirm
     * InformationAction
     * Verbose
     * WarningAction
     * WhatIf
     
    This function should be used in a module's function to grab the caller's preference variables so the caller doesn't
    have to explicitly pass common parameters to the module function.
 
    This function is adapted from the [`Get-CallerPreference` function written by David Wyatt](https://gallery.technet.microsoft.com/scriptcenter/Inherit-Preference-82343b9d).
 
    There is currently a [bug in PowerShell](https://connect.microsoft.com/PowerShell/Feedback/Details/763621) that
    causes an error when `ErrorAction` is implicitly set to `Ignore`. If you use this function, you'll need to add
    explicit `-ErrorAction $ErrorActionPreference` to every `Write-Error` call. Please vote up this issue so it can get
    fixed.
 
    .LINK
    about_Preference_Variables
 
    .LINK
    about_CommonParameters
 
    .LINK
    https://gallery.technet.microsoft.com/scriptcenter/Inherit-Preference-82343b9d
 
    .LINK
    http://powershell.org/wp/2014/01/13/getting-your-script-module-functions-to-inherit-preference-variables-from-the-caller/
 
    .EXAMPLE
    Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState
 
    Demonstrates how to set the caller's common parameter preference variables in a module function.
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        #[Management.Automation.PSScriptCmdlet]
        # The module function's `$PSCmdlet` object. Requires the function be decorated with the `[CmdletBinding()]`
        # attribute.
        $Cmdlet,

        [Parameter(Mandatory)]
        # The module function's `$ExecutionContext.SessionState` object. Requires the function be decorated with the
        # `[CmdletBinding()]` attribute.
        #
        # Used to set variables in its callers' scope, even if that caller is in a different script module.
        [Management.Automation.SessionState]$SessionState
    )

    Set-StrictMode -Version 'Latest'

    # List of preference variables taken from the about_Preference_Variables and their common parameter name (taken
    # from about_CommonParameters).
    $commonPreferences = @{
                              'ErrorActionPreference' = 'ErrorAction';
                              'DebugPreference' = 'Debug';
                              'ConfirmPreference' = 'Confirm';
                              'InformationPreference' = 'InformationAction';
                              'VerbosePreference' = 'Verbose';
                              'WarningPreference' = 'WarningAction';
                              'WhatIfPreference' = 'WhatIf';
                          }

    foreach( $prefName in $commonPreferences.Keys )
    {
        $parameterName = $commonPreferences[$prefName]

        # Don't do anything if the parameter was passed in.
        if( $Cmdlet.MyInvocation.BoundParameters.ContainsKey($parameterName) )
        {
            continue
        }

        $variable = $Cmdlet.SessionState.PSVariable.Get($prefName)
        # Don't do anything if caller didn't use a common parameter.
        if( -not $variable )
        {
            continue
        }

        if( $SessionState -eq $ExecutionContext.SessionState )
        {
            Set-Variable -Scope 1 -Name $variable.Name -Value $variable.Value -Force -Confirm:$false -WhatIf:$false
        }
        else
        {
            $SessionState.PSVariable.Set($variable.Name, $variable.Value)
        }
    }
}