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

if (-not (Test-Path 'variable:IsWindows'))
{
    $script:IsWindows = $true
    $script:IsLinux = $false
    $script:IsMacOS = $false
}

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

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

Import-Module -Name (Join-Path -Path $psModulesPath -ChildPath 'Carbon.Accounts\Carbon.Accounts.psm1' -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 $IsWindows)
    {
        $msg = 'The Get-CNtfsHardLink function is only supported on Windows.'
        Write-Error -Message $msg -ErrorAction $ErrorActionPreference
        return
    }

    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

        if (-not $IsWindows)
        {
            $msg = 'The Get-CNtfsPermission function is only supported on Windows.'
            Write-Error -Message $msg -ErrorAction $ErrorActionPreference
            return
        }

        Get-CPermission @PSBoundParameters
    }
}



function Get-CTempPath
{
    <#
    .SYNOPSIS
    Gets the path to the current user's temporary directory.
 
    .DESCRIPTION
    The `Get-CTempPath` function gets the path to the current user's temporary directory, as returned by
    `[IO.Path]::GetTempPath()`, which works across operating systems. The path is not guaranteed to actually exist. To
    ensure the temp path exists, use the `-Create` switch, and `Get-CTempPath` will ensure the path it returns exists.
 
    On Windows, the `System.IO.Path.GetTempPath()` function uses the [GetTempPath2
    function](https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-gettemppath2a), which, for
    non-system processes, checks for the existence of environment variables in the following order and uses the first
    path found:
 
    * `TMP`
    * `TEMP`
    * `USERPROFILE`
    * The Windows directory
 
    For system processes, returns the value of the `SystemTemp` environment variable if it is set, or
    `C:\Windows\SystemTemp` if it is not.
 
    On Linux and macOS, returns the value of the `TMPDIR` environment variable if it is set, or `/tmp/` if it is not.
 
    .EXAMPLE
    Get-CTempPath
 
    Demonstrates how to get the path to the current user's temporary directory.
    #>

    [CmdletBinding()]
    param(
        # If set, and the temp path does not exist, it is created.
        [switch] $Create
    )

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

    $path = [IO.Path]::GetTempPath()

    if ($Create)
    {
        Install-CDirectory -Path $path
    }

    return $path
}


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 $IsWindows)
    {
        $msg = 'The Grant-CNtfsPermission function is only supported on Windows.'
        Write-Error -Message $msg -ErrorAction $ErrorActionPreference
        return
    }

    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 Install-CDirectory
{
    <#
    .SYNOPSIS
    Creates a directory, if it doesn't exist.
 
    .DESCRIPTION
    The `Install-CDirectory` function creates a directory. If the directory already exists, it does nothing. If any
    parent directories don't exist, they are created, too.
 
    To return a `DirectoryInfo` object for the directory, use the `-PassThru` switch.
 
    If the path exists and is a file, writes an error. Use the `-Force` switch to delete the file and create the
    directory in its place.
 
    .EXAMPLE
    Install-CDirectory -Path 'C:\Projects\Carbon'
 
    Demonstrates how to use create a directory. In this case, the directories `C:\Projects` and `C:\Projects\Carbon`
    will be created if they don't exist.
    #>

    [CmdletBinding(SupportsShouldProcess)]
    param(
        # The path to the directory to create.
        [Parameter(Mandatory, ValueFromPipeline)]
        [String] $Path,

        # If set, returns a `DirectoryInfo` object for the directory.
        [switch] $PassThru,

        # If set and the target path exists and is a file, deletes the file and creates the directory in its place.
        # Otherwise, writes an error that the path exists and is a file.
        [switch] $Force
    )

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

        if ((Test-Path -LiteralPath $Path -PathType Leaf))
        {
            if (-not $Force)
            {
                $msg = "Failed to install directory ""${Path}"" because that path exists and is a file. Use the -Force " +
                       'switch to replace the file with a directory.'
                Write-Error $msg -ErrorAction $ErrorActionPreference
                return
            }

            Write-Information "Deleting file ""${Path}""." -InformationAction $InformationPreference
            Remove-Item -LiteralPath $Path -Force
        }

        if (-not (Test-Path -LiteralPath $Path -PathType Container))
        {
            Write-Information "Creating directory ""${Path}""." -InformationAction $InformationPreference
            New-Item -Path $Path -ItemType 'Directory' -Force | Out-String | Write-Verbose
        }

        if ($PassThru -and (Test-Path -LiteralPath $Path))
        {
            Get-Item -LiteralPath $Path
        }
    }
}



function New-CTempDirectory
{
    <#
    .SYNOPSIS
    Creates a new temporary directory with a random name.
 
    .DESCRIPTION
    A new temporary directory is created in the current user's temp directory, as returned by
    `[IO.Path]::GetTempPath()`. The directory's name is created using the `Path` class's [GetRandomFileName
    method](http://msdn.microsoft.com/en-us/library/system.io.path.getrandomfilename.aspx).
 
    To add a custom prefix to the directory name, use the `Prefix` parameter. If you pass in a path, only its name will
    be used. In this way, you can pass `$MyInvocation.MyCommand.Definition` (PowerShell 2) or `$PSCommandPath`
    (PowerShell 3+), which will help you identify what scripts are leaving cruft around in the temp directory.
 
    .LINK
    http://msdn.microsoft.com/en-us/library/system.io.path.getrandomfilename.aspx
 
    .EXAMPLE
    New-CTempDirectory
 
    Demonstrates how to create a new temporary directory, e.g. `C:\Users\ajensen\AppData\Local\Temp\5pobd3tu.5rn`.
 
    .EXAMPLE
    New-CTempDirectory -Prefix 'Carbon'
 
    Demonstrates how to create a new temporary directory with a custom prefix for its name, e.g.
    `C:\Users\ajensen\AppData\Local\Temp\Carbon5pobd3tu.5rn`.
 
    .EXAMPLE
    New-CTempDirectory -Prefix $MyInvocation.MyCommand.Definition
 
    Demonstrates how you can use `$MyInvocation.MyCommand.Definition` in PowerShell 2 to create a new, temporary
    directory, named after the currently executing scripts, e.g.
    `C:\Users\ajensen\AppData\Local\Temp\New-CTempDirectory.ps15pobd3tu.5rn`.
 
    .EXAMPLE
    New-CTempDirectory -Prefix $PSCommandPath
 
    Demonstrates how you can use `$PSCommandPath` in PowerShell 3+ to create a new, temporary directory, named after the
    currently executing scripts, e.g. `C:\Users\ajensen\AppData\Local\Temp\New-CTempDirectory.ps15pobd3tu.5rn`.
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')]
    [CmdletBinding(SupportsShouldProcess)]
    [OutputType([IO.DirectoryInfo])]
    param(
        # A prefix to use, so you can more easily identify *what* created the temporary directory. If you pass in a
        # path, its name will be used as the prefix.
        [String] $Prefix
    )

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

    $tempDir = [IO.Path]::GetRandomFileName()
    if( $Prefix )
    {
        $Prefix = Split-Path -Leaf -Path $Prefix
        $tempDir = '{0}{1}' -f $Prefix,$tempDir
    }

    $tempDir = Join-Path -Path (Get-CTempPath) -ChildPath $tempDir
    Install-CDirectory -Path $tempDir -PassThru
}



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

    if (-not $IsWindows)
    {
        $msg = 'The Revoke-CNtfsPermission function is only supported on Windows.'
        Write-Error -Message $msg -ErrorAction $ErrorActionPreference
        return
    }

    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
    )

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

        if (-not $IsWindows)
        {
            $msg = 'The Set-CNtfsOwner function is only supported on Windows.'
            Write-Error -Message $msg -ErrorAction $ErrorActionPreference
            return
        }
    }

    process
    {
        if (-not $IsWindows)
        {
            return
        }

        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 Set-CTempPath
{
    <#
    .SYNOPSIS
    Sets the path to the current user's temp directory.
 
    .DESCRIPTION
    The `Set-CTempPath` function sets the path to the current user's temp directory. Pass the path for the temp
    directory to the `Path` parameter. On Windows, for non-system processes, sets the `TMP` environment variable to the
    path. For system processes, sets the `SystemTemp` environment variable. On Linux and macOS, sets the `TMPDIR`
    environment variable. If path is a relative path, it will be assumed to be relative to PowerShell's current
    directory (i.e. the return value of `Get-Location`), and converted to an absolute path.
 
    Use the `-Create` switch to create the temp path if it doesn't exist.
 
    Note that on Windows, if setting the system user's temp path, the directory should only be accessible to the system
    user: ACL inheritance should be turned off and the only permission granted should be full control to the SYSTEM
    account. Otherwise, unprivileged users may be able to view sensitive files.
 
    .EXAMPLE
    Set-CTempPath -Path 'C:\MyTemp'
 
    Demonstrates how to set the current user's temp path by passing the path to the `Path` parameter.
 
    .EXAMPLE
    Set-CTempPath -Path 'C:\MyTemp' -Create
 
    Demonstrates how to ensure the temp path exists by using the `-Create` switch.
 
    .EXAMPLE
    'C:\MyTemp' | Set-CTempPath
 
    Demonstrates that you can pipe the path to `Set-CTempPath`.
    #>

    [CmdletBinding(SupportsShouldProcess)]
    param(
        # The path to set as the current user's temp path. Can be piped in or passed as an argument. If a relative path
        # is passed, it is assumed to be relative to PowerShell's current directory (i.e. the return value of
        # `Get-Location`), and converted to an absolute path.
        [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [string] $Path,

        # If set, creates the temp path if it doesn't exist.
        [switch] $Create
    )

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

        if (-not [IO.Path]::IsPathRooted($Path))
        {
            $Path = Join-Path -Path (Get-Location) -ChildPath $Path
        }

        $Path = [IO.Path]::GetFullPath($Path)

        $Path = Join-Path -Path $Path -ChildPath ([IO.Path]::DirectorySeparatorChar)

        if ($Create)
        {
            Install-CDirectory -Path $Path
        }

        $envVarName  = 'TMPDIR'

        if ($IsWindows)
        {
            if ([Security.Principal.WindowsIdentity]::GetCurrent().IsSystem)
            {
                $envVarName = 'SystemTemp'
            }
            else
            {
                $envVarName = 'TMP'
            }
        }

        $currentTempPath = Get-CTempPath
        if ($currentTempPath -eq $Path)
        {
            Write-Verbose "Temp path environment variable ${envVarName} already set to ""${Path}""."
            return
        }

        $action = "Creating"
        if ((Test-Path -Path "env:${envVarName}"))
        {
            $action = "Setting"
        }

        $target = "${envVarName} environment variable"
        $actionMsg = "$($action.ToLowerInvariant()) to ""${Path}"""
        if ($PSCmdlet.ShouldProcess($target, $actionMsg))
        {
            $msg = "${action} temp path environment variable ${envVarName} to ""${Path}""."
            Write-Information $msg -InformationAction $InformationPreference
            [Environment]::SetEnvironmentVariable($envVarName, $Path, [EnvironmentVariableTarget]::Process)
        }
    }
}


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 (-not $IsWindows)
    {
        $msg = 'The Test-CNtfsPermission function is only supported on Windows.'
        Write-Error -Message $msg -ErrorAction $ErrorActionPreference
        return
    }

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

    Test-CPermission @PSBoundParameters
}




function Uninstall-CDirectory
{
    <#
    .SYNOPSIS
    Removes a directory, if it exists.
 
    .DESCRIPTION
    The `Uninstall-CDirectory` function removes a directory. If the directory doesn't exist, it does nothing. If the
    directory has any files or sub-directories, you will be prompted to confirm the deletion of the directory and all
    its contents. To avoid the prompt, use the `-Recurse` switch.
 
    If the path to delete is to a file, the function writes an error. Use the `-Force` switch to delete the path even
    if it is a file.
 
    .EXAMPLE
    Uninstall-CDirectory -Path 'C:\Projects\Carbon'
 
    Demonstrates how to remove/delete a directory. In this case, the directory `C:\Projects\Carbon` will be deleted, if
    it exists.
 
    .EXAMPLE
    Uninstall-CDirectory -Path 'C:\Projects\Carbon' -Recurse
 
    Demonstrates how to remove/delete a directory that has items in it. In this case, the directory `C:\Projects\Carbon`
    *and all of its files and sub-directories* will be deleted, if the directory exists.
 
    .EXAMPLE
    Get-ChildItem -Path 'C:\Projects' -Directory | Uninstall-CDirectory -Recurse
 
    Demonstrates that you can pipe paths or directory objects to `Uninstall-CDirectory`.
    #>

    [CmdletBinding(SupportsShouldProcess)]
    param(
        # The path to the directory to delete. Wildcards *not* supported.
        [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [Alias('FullName')]
        [String] $Path,

        # Delete the directory *and* everything under it.
        [switch] $Recurse,

        # Delete the directory even if it is a file.
        [switch] $Force
    )

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

        if ((Test-Path -LiteralPath $Path -PathType Leaf))
        {
            if (-not $Force)
            {
                $msg = "Failed to delete directory ""${Path}"" because that path is a file. Use the -Force switch to " +
                       'delete that path even if it is a file.'
                Write-Error $msg -ErrorAction $ErrorActionPreference
                return
            }

            Write-Information "Deleting file ""${Path}""." -InformationAction $InformationPreference
            Remove-Item -LiteralPath $Path -Force
            return
        }

        if ((Test-Path -LiteralPath $Path -PathType Container))
        {
            Write-Information "Deleting directory ""${Path}""." -InformationAction $InformationPreference
            Remove-Item -LiteralPath $Path -Recurse:$Recurse
        }
    }
}



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