PowerShell Problem Solver: Copy Files with Alternate Credentials

Not too long ago, I chimed in on a PowerShell problem about copying files on some social media platform. An IT pro was facing a challenge using PowerShell to get his job done and wasn’t sure what to do. The task at hand was to copy files to a network location using alternate credentials. Naturally, he wanted this accomplished using PowerShell. At first glance, you might think this should be pretty easy. If you look at help for Copy-Item, you will see a credential parameter. However, if you look at help for this parameter, then you will come across a rather stunning pronouncement.


The credential parameter is not supported in Windows PowerShell. (Image Credit: Jeffery Hicks)
The credential parameter is not supported in Windows PowerShell. (Image Credit: Jeffery Hicks)

Yes. This parameter is not used out-of-the-box. This is why it is important that you be in the habit of reading full help in PowerShell. More than likely this parameter exists for future providers or future versions of existing providers that will support it. But for right now in PowerShell 4.0, this is not an option. So if we can’t use an alternate credential, how can we copy files?
My solution is to leverage the New-PSDrive cmdlet. This cmdlet has a credential parameter that will work. I should be able to create a new drive to a network share using an alternate credential.

new-psdrive -Name IT -PSProvider FileSystem -Root \chi-fp02IT -Credential globomanticsadministrator

Now I can copy files to this destination.

dir c:work*.xml | copy -Destination IT: -PassThru

When I’m finished copying I can remove the PSDrive.

Remove-PSDrive IT

But it would sure be nice to make this process more transparent. So I created a proxy function for Copy-Item. A proxy function is wrapper for a PowerShell command that you can customize. Often proxy commands add or remove parameters to an underlying cmdlet. I used a function that I published on my blog to create a proxy version of Copy-Item. Here is a version that I call Copy-Item2 because I didn’t want to overwrite the original Copy-Item2, although you certainly could.

#requires -version 2.0
#copy item with credential
Function Copy-Item2 {
<#
.Synopsis
A Copy-Item replacement that supports alternate credentials
.Description
This is a proxy version of Copy-Item that has been modified to support using alternate credentials when copying files to a UNC.
.Parameter Credential
Enter a saved PSCredential object or an account name in the format domainusername or computernameusername. You will be prompted for the password.
This parameter only works if the destination is a UNC.
.Notes
Last Updated:	9/25/2014
.Example
PS C:> dir c:work*.xml | Copy-Item2 -destination \chi-fp02IT -credential globomanticsadministrator -passthru
.Link
Copy-Item
 #>
[CmdletBinding(DefaultParameterSetName='Path', SupportsShouldProcess=$true, ConfirmImpact='Medium', SupportsTransactions=$true, HelpUri='http://go.microsoft.com/fwlink/?LinkID=113292')]
 param(
     [Parameter(ParameterSetName='Path', Mandatory=$true, Position=0, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true)]
     [string[]]$Path,
     [Parameter(ParameterSetName='LiteralPath', Mandatory=$true, ValueFromPipelineByPropertyName=$true)]
     [Alias('PSPath')]
     [string[]]$LiteralPath,
     [Parameter(Position=1, ValueFromPipelineByPropertyName=$true)]
     [string]$Destination,
     [switch]$Container,
     [switch]$Force,
     [string]$Filter,
     [string[]]$Include,
     [string[]]$Exclude,
     [switch]$Recurse,
     [switch]$PassThru,
     [Parameter(ValueFromPipelineByPropertyName=$true)]
     [ValidateScript({
       if ($Destination -match "^\\S+\S+") {
        $True
       }
       else {
        Throw "You can only use the credential parameter with a UNC destination."
       }
     })]
     [System.Management.Automation.CredentialAttribute()]$Credential
    )
 begin
 {
      Write-Verbose -Message "Starting $($MyInvocation.Mycommand)"
     try {
         $outBuffer = $null
         if ($PSBoundParameters.TryGetValue('OutBuffer', [ref]$outBuffer))
         {
             $PSBoundParameters['OutBuffer'] = 1
         }
         if ($Credential) {
             #create a temporary PSDrive with the credential
             Write-Verbose -Message "Creating temporary PSDrive to $Destination"
             Try {
                #NOTE: this code does not always work
                #generate a random name without an extension
                $tmpName = [System.IO.Path]::GetFileNameWithoutExtension([System.IO.path]::GetRandomFileName())
                Write-Verbose "Creating new psdrive $tmpName"
                Write-verbose "Using credential for $($credential.username)"
                $tmpDrive = New-PSDrive -Name $tmpName -PSProvider FileSystem -Root $Destination -Credential $Credential -ErrorAction Stop
                if (-not (Test-Path "$($tmpName):")) {
                    Throw "New-PSDrive command failed mapping $Destination to $tmpName"
                }
             }
             Catch {
                #error creating PSDrive
                Throw
             }
             #Change PSBoundParameter
             Write-Verbose "Setting destination parameter to $($tmpName):"
             $PSBoundParameters.Destination =  "$($tmpName):"
             #remove the credential from PSBound parameters
             Write-Verbose "Removing original credential from PSBoundParameters"
             $PSBoundParameters.Remove("Credential") | Out-Null
         }#if Credential
         $wrappedCmd = $ExecutionContext.InvokeCommand.GetCommand('Copy-Item', [System.Management.Automation.CommandTypes]::Cmdlet)
         $scriptCmd = {& $wrappedCmd @PSBoundParameters }
         $steppablePipeline = $scriptCmd.GetSteppablePipeline($myInvocation.CommandOrigin)
         $steppablePipeline.Begin($PSCmdlet)
     } catch {
         throw
     }
 }
 process
 {
     try {
         $steppablePipeline.Process($_)
     } catch {
         throw
     }
 }
 end
 {
     try {
         $steppablePipeline.End()
     } catch {
         throw
     }
     if (Test-Path "$($tmpName):") {
        Write-Verbose "Removing temporary PSDrive"
        Remove-PSDrive -Name $tmpName
        #might need to clean up a NET USE Drive mapping
        #need to escape any  in the path
        $wmiPath = $Destination.replace("","\")
        $net = Get-WMIObject -Class Win32_networkConnection -filter "RemoteName='$wmiPath'"
        if ($net) {
            net use $Destination /delete | out-Null
        }
     }
     Write-Verbose -Message "Ending $($MyInvocation.Mycommand)"
 }
} #end function Copy-Item2

I modified the credential parameter to include a validation test, verifying that the destination is a UNC. If it isn’t, then PowerShell will throw an exception when you run the command.

[Parameter(ValueFromPipelineByPropertyName=$true)]
[ValidateScript({
if ($Destination -match "^\\S+\S+") {
$True
}
else {
Throw "You can only use the credential parameter with a UNC destination."
}
})]
[System.Management.Automation.CredentialAttribute()]$Credential
)

In the script, if –Credential is specified, I want to create a temporary PSDrive. Because I don’t want to have to guess what might be an available drive letter or name, I’ll have the .NET Framework give me random file name which will be something like ‘3bani1qf.gie’. Then I get the filename only, i.e. ‘3bani1qf’.

All of this done in a one-line command:

$tmpName = [System.IO.Path]::GetFileNameWithoutExtension([System.IO.path]::GetRandomFileName())

Armed with a name, I can create the PSDrive.

$tmpDrive = New-PSDrive -Name $tmpName -PSProvider FileSystem -Root $Destination -Credential $Credential -ErrorAction Stop

Now, because Copy-Item is a wrapped command, it doesn’t really use Copy-Item, I don’t want it to see the credential parameter, so I’ll drop it from the bound parameters.

$PSBoundParameters.Remove("Credential") | Out-Null

I will also redefine the destination parameter to use the newly created PSDrive.

$PSBoundParameters.Destination = "$($tmpName):"


The proxy function will invoke the underlying cmdlet with the defined parameters.

$wrappedCmd = $ExecutionContext.InvokeCommand.GetCommand('Copy-Item', [System.Management.Automation.CommandTypes]::Cmdlet)
$scriptCmd = {& $wrappedCmd @PSBoundParameters }

With this function loaded in my PowerShell session I can use Copy-Item, or at least my version, as you would expect:

dir c:work*.xml | Copy-Item2 -destination \chi-fp02IT -credential globomanticsadministrator -passthru

Hopefully if you use this command it will always work. But in my testing it would never work 100% of the time. You might see an error about multiple connections to a server not being allowed. I’ll have another alternative next week.