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.
$script:moduleRoot = $PSScriptRoot

$psModulesPath = Join-Path -Path $script:moduleRoot -ChildPath 'Modules' -Resolve

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

Import-Module -Name (Join-Path -Path $psModulesPath -ChildPath 'Carbon.Accounts' -Resolve) `
              -Function @('Resolve-CPrincipal', 'Resolve-CPrincipalName') `
              -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 -and (Test-Path -Path $Path -PathType Container))
    {
        $ApplyTo = 'FolderSubfoldersAndFiles'
    }

    if ($ApplyTo)
    {
        $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 Set-CNtfsOwner
{
    <#
    .SYNOPSIS
    Sets the owner of an NTFS file or directory.
 
    .DESCRIPTION
    The `Set-CNtfsOwner` function sets the owner of an NTFS file or directory. Pass the path of the file or directory to
    the `Path` parameter. Pass the new owner to the `Identity` parameter. If the file or directory isn't owned by the
    new owner, its ACL is updated. Otherwise, nothing happens.
 
    You can also pipe file system objects to the function in place of passing a path.
 
    This function requires administrative privileges.
 
    .EXAMPLE
    Set-CNtfsOwner -Path $Path -Identity $username
 
    Demonstrates how to set the owner of a file system object to a specific principal. In this example, the file or
    directory at `$Path` will be owned by `$username`.
 
    .EXAMPLE
    Get-ChildItem -Path $directory | Set-CNtfsOwner -Identity $username
 
    Demonstrates that you can pipe items to `Set-CNtfsOwner` to mass change the owner.
    #>

    [CmdletBinding(SupportsShouldProcess)]
    param(
        [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [Alias('FullName')]
        [String] $Path,

        [Parameter(Mandatory)]
        [String] $Identity
    )

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

        if (-not (Test-Path -Path $Path))
        {
            $msg = "Failed to set owner on ""${Path}"" to ""${Identity}"" because that path does not exist."
            Write-Error -Message $msg -ErrorAction $ErrorActionPreference
            return
        }

        $newOwner = Resolve-CPrincipal -Name $Identity
        if (-not $newOwner)
        {
            Write-Error -Message "Principal ""${Identity}"" not found." -ErrorAction $ErrorActionPreference
            return
        }

        $paths = Resolve-Path -Path $Path

        foreach ($pathItem in $paths)
        {
            $acl = Get-Acl -LiteralPath $pathItem

            $currentOwner = Resolve-CPrincipalName -Name $acl.Owner

            if ($currentOwner -eq $newOwner.FullName)
            {
                Write-Verbose "Principal ""$($newOwner.FullName)}"" already owns ""${pathItem}""."
                return
            }

            Write-Information "Changing owner of ""${pathItem}"" from ""${currentOwner}"" to ""$($newOwner.FullName)""."
            $acl.SetOwner($newOwner.Sid)
            Set-Acl -LiteralPath $pathItem -AclObject $acl
        }
    }
}



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)
        }
    }
}