DSCResources/xFileUpload/xFileUpload.schema.psm1
$errorActionPreference = 'Stop' Set-StrictMode -Version 'Latest' $modulePath = Join-Path -Path (Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent) -ChildPath 'Modules' # Import the shared modules Import-Module -Name (Join-Path -Path $modulePath ` -ChildPath (Join-Path -Path 'xPSDesiredStateConfiguration.Common' ` -ChildPath 'xPSDesiredStateConfiguration.Common.psm1')) <# .SYNOPSIS DSC Composite Resource uploads file or folder to an SMB share. .DESCRIPTION This is a DSC Composite resource that can be used to upload a file or folder into an SMB file share. The SMB file share does not have to be currently mounted. It will be mounted during the upload process using the optional Credential and then dismounted after completion of the upload. .PARAMETER DestinationPath The destination SMB share path to upload the file or folder to. .PARAMETER SourcePath The source path of the file or folder to upload. .PARAMETER Credential Credentials to access the destination SMB share path where file or folder should be uploaded. .PARAMETER certificateThumbprint Thumbprint of the certificate which should be used for encryption/decryption. .EXAMPLE $securePassword = ConvertTo-SecureString -String 'password' -AsPlainText -Force $credential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList 'domain\user', $securePassword xFileUpload ` -DestinationPath '\\machine\share\destinationfolder' ` -SourcePath 'C:\folder\file.txt' ` -Credential $credential #> configuration xFileUpload { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingConvertToSecureStringWithPlainText', '')] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('DscResource.AnalyzerRules\Measure-Keyword', '', Justification = 'Script resource name is seen as a keyword if this is not used.')] param ( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.String] $DestinationPath, [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.String] $SourcePath, [Parameter()] [System.Management.Automation.Credential()] [System.Management.Automation.PSCredential] $Credential, [Parameter()] [System.String] $CertificateThumbprint ) $cacheLocation = "$env:ProgramData\Microsoft\Windows\PowerShell\configuration\BuiltinProvCache\DSC_xFileUpload" if ($Credential) { $username = $Credential.UserName # Encrypt password $password = Invoke-Command ` -ScriptBlock $getEncryptedPassword ` -ArgumentList $Credential, $CertificateThumbprint } Script FileUpload { # Get script is not implemented cause reusing Script resource's schema does not make sense GetScript = { return @{} }; SetScript = { # Generating credential object if password and username are specified $Credential = $null if (($using:password) -and ($using:username)) { # Validate that certificate thumbprint is specified if (-not $using:CertificateThumbprint) { $errorMessage = 'Certificate thumbprint has to be specified if credentials are present.' Invoke-Command ` -ScriptBlock $using:throwTerminatingError ` -ArgumentList 'CertificateThumbprintIsRequired', $errorMessage, 'InvalidData' } Write-Debug -Message 'Username and password specified.' # Decrypt password $decryptedPassword = Invoke-Command ` -ScriptBlock $using:getDecryptedPassword ` -ArgumentList $using:password, $using:CertificateThumbprint # Generate credential $securePassword = ConvertTo-SecureString -String $decryptedPassword -AsPlainText -Force $Credential = New-Object ` -TypeName System.Management.Automation.PSCredential ` -ArgumentList ($using:username, $securePassword) } # Validate DestinationPath is UNC path if (-not ($using:DestinationPath -as [System.Uri]).isUnc) { $errorMessage = "Destination path $using:DestinationPath is not a valid UNC path." Invoke-Command ` -ScriptBlock $using:throwTerminatingError ` -ArgumentList 'DestinationPathIsNotUNCFailure', $errorMessage, 'InvalidData' } # Verify source is localpath if (-not (($using:SourcePath -as [System.Uri]).Scheme -match 'file')) { $errorMessage = "Source path $using:SourcePath has to be local path." Invoke-Command ` -ScriptBlock $using:throwTerminatingError ` -ArgumentList 'SourcePathIsNotLocalFailure', $errorMessage, 'InvalidData' } # Check whether source path is existing file or directory $sourcePathType = $null if (-not (Test-Path -Path $using:SourcePath)) { $errorMessage = "Source path $using:SourcePath does not exist." Invoke-Command ` -ScriptBlock $using:throwTerminatingError ` -ArgumentList 'SourcePathDoesNotExistFailure', $errorMessage, 'InvalidData' } else { $item = Get-Item -Path $using:SourcePath switch ($item.GetType().Name) { 'FileInfo' { $sourcePathType = 'File' } 'DirectoryInfo' { $sourcePathType = 'Directory' } } } Write-Debug -Message "SourcePath $using:SourcePath is of type: $sourcePathType" $psDrive = $null # Mount the drive only if Credentials are specified and it's currently not accessible if ($Credential) { if (Test-Path -Path $using:DestinationPath -ErrorAction Ignore) { Write-Debug -Message "Destination path $using:DestinationPath is already accessible. No mount needed." } else { $psDriveArgs = @{ Name = ([System.Guid]::NewGuid()) PSProvider = 'FileSystem' Root = $using:DestinationPath Scope = 'Private' Credential = $Credential } try { Write-Debug -Message "Create psdrive with destination path $using:DestinationPath..." $psDrive = New-PSDrive @psDriveArgs -ErrorAction Stop } catch { $errorMessage = "Cannot access destination path $using:DestinationPath with given Credential" Invoke-Command ` -ScriptBlock $using:throwTerminatingError ` -ArgumentList 'DestinationPathNotAccessibleFailure', $errorMessage, 'InvalidData' } } } try { # Get expected destination path $expectedDestinationPath = $null if (-not (Test-Path -Path $using:DestinationPath)) { # DestinationPath has to exist $errorMessage = 'Invalid parameter values: DestinationPath does not exist, but has to be existing directory.' Throw-TerminatingError -ErrorMessage $errorMessage -ErrorCategory 'InvalidData' -ErrorId 'DestinationPathDoesNotExistFailure' } else { $item = Get-Item -Path $using:DestinationPath switch ($item.GetType().Name) { 'FileInfo' { # DestinationPath cannot be file $errorMessage = 'Invalid parameter values: DestinationPath is file, but has to be existing directory.' Invoke-Command ` -ScriptBlock $using:throwTerminatingError ` -ArgumentList 'DestinationPathCannotBeFileFailure', $errorMessage, 'InvalidData' } 'DirectoryInfo' { $expectedDestinationPath = Join-Path ` -Path $using:DestinationPath ` -ChildPath (Split-Path -Path $using:SourcePath -Leaf) } } Write-Debug -Message "ExpectedDestinationPath is $expectedDestinationPath" } # Copy destination path try { Write-Debug -Message "Copying $using:SourcePath to $using:DestinationPath" Copy-Item -Path $using:SourcePath -Destination $using:DestinationPath -Recurse -Force -ErrorAction Stop } catch { $errorMessage = "Could not copy source path $using:SourcePath to $using:DestinationPath : $($_.Exception)" Invoke-Command ` -ScriptBlock $using:throwTerminatingError ` -ArgumentList 'CopyDirectoryOverFileFailure', $errorMessage, 'InvalidData' } # Verify whether expectedDestinationPath was created if (-not (Test-Path -Path $expectedDestinationPath)) { $errorMessage = "Destination path $using:DestinationPath could not be created" Invoke-Command ` -ScriptBlock $using:throwTerminatingError ` -ArgumentList 'DestinationPathNotCreatedFailure', $errorMessage, 'InvalidData' } # If expectedDestinationPath exists else { Write-Verbose -Message "$sourcePathType $expectedDestinationPath has been successfully created" # Update cache $uploadedItem = Get-Item -Path $expectedDestinationPath $lastWriteTime = $uploadedItem.LastWriteTimeUtc $inputObject = @{} $inputObject['LastWriteTimeUtc'] = $lastWriteTime $key = [System.String]::Join('', @($using:DestinationPath, $using:SourcePath, $expectedDestinationPath)).GetHashCode().ToString() $path = Join-Path $using:cacheLocation $key if (-not (Test-Path -Path $using:cacheLocation)) { New-Item -Path $using:cacheLocation -ItemType Directory | Out-Null } Write-Debug -Message "Updating cache for DestinationPath = $using:DestinationPath and SourcePath = $using:SourcePath. CacheKey = $key" Export-CliXml -Path $path -InputObject $inputObject -Force } } finally { # Remove PSDrive if ($psDrive) { Write-Debug -Message "Removing PSDrive on root $($psDrive.Root)" Remove-PSDrive -Name $psDrive -Force } } }; TestScript = { # Generating credential object if password and username are specified $Credential = $null if (($using:password) -and ($using:username)) { # Validate that certificate thumbprint is specified if (-not $using:CertificateThumbprint) { $errorMessage = 'Certificate thumbprint has to be specified if credentials are present.' Invoke-Command ` -ScriptBlock $using:throwTerminatingError ` -ArgumentList 'CertificateThumbprintIsRequired', $errorMessage, 'InvalidData' } Write-Debug -Message 'Username and password specified. Generating credential' # Decrypt password $decryptedPassword = Invoke-Command ` -ScriptBlock $using:getDecryptedPassword ` -ArgumentList $using:password, $using:CertificateThumbprint # Generate credential $securePassword = ConvertTo-SecureString -String $decryptedPassword -AsPlainText -Force $Credential = New-Object ` -TypeName System.Management.Automation.PSCredential ` -ArgumentList ($using:username, $securePassword) } else { Write-Debug -Message 'No credentials specified.' } # Validate DestinationPath is UNC path if (-not ($using:DestinationPath -as [System.Uri]).isUnc) { $errorMessage = "Destination path $using:DestinationPath is not a valid UNC path." Invoke-Command ` -ScriptBlock $using:throwTerminatingError ` -ArgumentList 'DestinationPathIsNotUNCFailure', $errorMessage, 'InvalidData' } # Check whether source path is existing file or directory (needed for expectedDestinationPath) $sourcePathType = $null if (-not (Test-Path -Path $using:SourcePath)) { $errorMessage = "Source path $using:SourcePath does not exist." Invoke-Command ` -ScriptBlock $using:throwTerminatingError ` -ArgumentList 'SourcePathDoesNotExistFailure', $errorMessage, 'InvalidData' } else { $item = Get-Item -Path $using:SourcePath switch ($item.GetType().Name) { 'FileInfo' { $sourcePathType = 'File' } 'DirectoryInfo' { $sourcePathType = 'Directory' } } } Write-Debug -Message "SourcePath $using:SourcePath is of type: $sourcePathType" $psDrive = $null # Mount the drive only if credentials are specified and it's currently not accessible if ($Credential) { if (Test-Path -Path $using:DestinationPath -ErrorAction Ignore) { Write-Debug -Message "Destination path $using:DestinationPath is already accessible. No mount needed." } else { $psDriveArgs = @{ Name = ([System.Guid]::NewGuid()) PSProvider = 'FileSystem' Root = $using:DestinationPath Scope = 'Private' Credential = $Credential } try { Write-Debug -Message "Create psdrive with destination path $using:DestinationPath..." $psDrive = New-PSDrive @psDriveArgs -ErrorAction Stop } catch { $errorMessage = "Cannot access destination path $using:DestinationPath with given Credential" Invoke-Command ` -ScriptBlock $using:throwTerminatingError ` -ArgumentList 'DestinationPathNotAccessibleFailure', $errorMessage, 'InvalidData' } } } try { # Get expected destination path $expectedDestinationPath = $null if (-not (Test-Path -Path $using:DestinationPath)) { # DestinationPath has to exist $errorMessage = 'Invalid parameter values: DestinationPath does not exist or is not accessible. DestinationPath has to be existing directory.' Invoke-Command ` -ScriptBlock $using:throwTerminatingError ` -ArgumentList 'DestinationPathDoesNotExistFailure', $errorMessage, 'InvalidData' } else { $item = Get-Item -Path $using:DestinationPath switch ($item.GetType().Name) { 'FileInfo' { # DestinationPath cannot be file $errorMessage = 'Invalid parameter values: DestinationPath is file, but has to be existing directory.' Invoke-Command ` -ScriptBlock $using:throwTerminatingError ` -ArgumentList 'DestinationPathCannotBeFileFailure', $errorMessage, 'InvalidData' } 'DirectoryInfo' { $expectedDestinationPath = Join-Path ` -Path $using:DestinationPath ` -ChildPath (Split-Path -Path $using:SourcePath -Leaf) } } Write-Debug -Message "ExpectedDestinationPath is $expectedDestinationPath" } # Check whether ExpectedDestinationPath exists and has expected type $itemExists = $false if (-not (Test-Path $expectedDestinationPath)) { Write-Debug -Message 'Expected destination path does not exist or is not accessible.' } # If expectedDestinationPath exists else { $expectedItem = Get-Item -Path $expectedDestinationPath $expectedItemType = $expectedItem.GetType().Name # If expectedDestinationPath has same type as sourcePathType, we need to verify cache to determine whether no upload is needed if ((($expectedItemType -eq 'FileInfo') -and ($sourcePathType -eq 'File')) -or ` (($expectedItemType -eq 'DirectoryInfo') -and ($sourcePathType -eq 'Directory'))) { # Get cache Write-Debug -Message "Getting cache for $expectedDestinationPath" $cacheContent = $null $key = [System.String]::Join('', @($using:DestinationPath, $using:SourcePath, $expectedDestinationPath)).GetHashCode().ToString() $path = Join-Path -Path $using:cacheLocation -ChildPath $key Write-Debug -Message "Looking for cache under $path" if (-not (Test-Path -Path $path)) { Write-Debug -Message "No cache found for DestinationPath = $using:DestinationPath and SourcePath = $using:SourcePath. CacheKey = $key" } else { $cacheContent = Import-CliXml -Path $path Write-Debug -Message "Found cache for DestinationPath = $using:DestinationPath and SourcePath = $using:SourcePath. CacheKey = $key" } # Verify whether cache reflects current state or upload is needed if ($cacheContent -ne $null -and ($cacheContent.LastWriteTimeUtc -eq $expectedItem.LastWriteTimeUtc)) { # No upload needed Write-Debug -Message 'Cache reflects current state. No need for upload.' $itemExists = $true } else { Write-Debug -Message 'Cache is empty or it does not reflect current state. Upload will be performed.' } } else { Write-Debug -Message "Expected destination path: $expectedDestinationPath is of type $expectedItemType, although source path is $sourcePathType" } } } finally { # Remove PSDrive if ($psDrive) { Write-Debug -Message "Removing PSDrive on root $($psDrive.Root)" Remove-PSDrive -Name $psDrive -Force } } return $itemExists }; } } # Encrypts password using the defined public key [System.Management.Automation.ScriptBlock] $getEncryptedPassword = { param ( [Parameter(Mandatory = $true)] [PSCredential] $Credential, [Parameter(Mandatory = $true)] [System.String] $CertificateThumbprint ) $value = $Credential.GetNetworkCredential().Password $cert = Invoke-Command ` -ScriptBlock $getCertificate ` -ArgumentList $CertificateThumbprint $encryptedPassword = $null if ($cert) { # Cast the public key correctly $rsaProvider = [System.Security.Cryptography.RSACryptoServiceProvider] $cert.PublicKey.Key if ($rsaProvider -eq $null) { $errorMessage = "Could not get public key from certificate with thumbprint: $CertificateThumbprint . Please verify certificate is valid for encryption." Invoke-Command ` -ScriptBlock $throwTerminatingError ` -ArgumentList "DecryptionCertificateNotFound", $errorMessage, "InvalidOperation" } # Convert to a byte array $keybytes = [System.Text.Encoding]::UNICODE.GetBytes($value) # Add a null terminator to the byte array $keybytes += 0 $keybytes += 0 # Encrypt using the public key $encbytes = $rsaProvider.Encrypt($keybytes, $false) # Return a string $encryptedPassword = [Convert]::ToBase64String($encbytes) } else { $errorMessage = "Could not find certificate which matches thumbprint: $CertificateThumbprint . Could not encrypt password" Invoke-Command ` -ScriptBlock $throwTerminatingError ` -ArgumentList "EncryptionCertificateNot", $errorMessage, "InvalidOperation" } return $encryptedPassword } # Retrieves certificate by thumbprint [System.Management.Automation.ScriptBlock] $getCertificate = { param ( [Parameter(Mandatory = $true)] [System.String] $CertificateThumbprint ) $cert = $null foreach ($certIndex in (Get-Childitem -Path Cert:\LocalMachine\My)) { if ($certIndex.Thumbprint -match $CertificateThumbprint) { $cert = $certIndex break } } if (-not $cert) { $errorMessage = "Error Reading certificate store for {0}. Please verify thumbprint is correct and certificate belongs to cert:\LocalMachine\My store." -f ${CertificateThumbprint}; Invoke-Command ` -ScriptBlock $throwTerminatingError ` -ArgumentList "InvalidPathSpecified", $errorMessage, "InvalidOperation" } else { $cert } } # Throws terminating error specified errorCategory, errorId and errorMessage [System.Management.Automation.ScriptBlock] $throwTerminatingError = { param ( [Parameter(Mandatory = $true)] [System.String] $ErrorId, [Parameter(Mandatory = $true)] [System.String] $ErrorMessage, [Parameter(Mandatory = $true)] $ErrorCategory ) $exception = New-Object -TypeName System.InvalidOperationException -ArgumentList $ErrorMessage $errorRecord = New-Object -TypeName System.Management.Automation.ErrorRecord -ArgumentList ($exception, $ErrorId, $ErrorCategory, $null) throw $errorRecord } # Decrypts password using the defined private key [System.Management.Automation.ScriptBlock] $getDecryptedPassword = { param ( [Parameter(Mandatory = $true)] [System.String] $Value, [Parameter(Mandatory = $true)] [System.String] $CertificateThumbprint ) $cert = $null foreach ($certIndex in (Get-Childitem -Path Cert:\LocalMachine\My)) { if ($certIndex.Thumbprint -match $CertificateThumbprint) { $cert = $certIndex break } } if (-not $cert) { $errorMessage = "Error Reading certificate store for {0}. Please verify thumbprint is correct and certificate belongs to cert:\LocalMachine\My store." -f ${CertificateThumbprint}; $exception = New-Object -TypeName System.InvalidOperationException -ArgumentList $errorMessage $errorRecord = New-Object -TypeName System.Management.Automation.ErrorRecord -ArgumentList ($exception, "InvalidPathSpecified", "InvalidOperation", $null) throw $errorRecord } $decryptedPassword = $null # Get RSA provider $rsaProvider = [System.Security.Cryptography.RSACryptoServiceProvider] $cert.PrivateKey if ($rsaProvider -eq $null) { $errorMessage = "Could not get private key from certificate with thumbprint: $CertificateThumbprint . Please verify certificate is valid for decryption." $exception = New-Object -TypeName System.InvalidOperationException -ArgumentList $errorMessage $errorRecord = New-Object -TypeName System.Management.Automation.ErrorRecord -ArgumentList ($exception, "DecryptionCertificateNotFound", "InvalidOperation", $null) throw $errorRecord } # Convert to bytes array $encBytes = [Convert]::FromBase64String($value) # Decrypt bytes $decryptedBytes = $rsaProvider.Decrypt($encBytes, $false) # Convert to string $decryptedPassword = [System.Text.Encoding]::Unicode.GetString($decryptedBytes) return $decryptedPassword } |