Unable to create a FileZilla/SFTP entry with Devolutions.PowerShell for DVLS
Hello,
I am currently migrating automation scripts from Remote Desktop Manager PowerShell to Devolutions.PowerShell for DVLS.
I managed to create SSH and Web entries using New-DSEntryBase. Now, I am trying to automatically create FileZilla SFTP entries using New-DSEntryBase.
I successfully create the entry in DVLS, but when opening it from the Web UI or from RDM app, the entry is incomplete. There is no host, no username, no password...
What I already tested:
However, exported FileZilla entries seem incomplete:
I also tried using New-DSEntryBase -FromRDMConnection and integrate the input configuration that is used in my functional RDM PowerShell script but it fails because the cmdlet expects:
Devolutions.RemoteDesktopManager.Business.Entities.ConnectionInfoEntity
while New-RDMSession returns:
RemoteDesktopManager.PowerShellModule.PSOutputObject.PSConnection
My goal is to fully automate the creation of FileZilla SFTP entries in DVLS from PowerShell.
Could you please provide:
Thank you.
Here is my non-functional JSON configuration :
$body = @{
name = "SFTP - Test 01"
group = "FOLDER01"
vaultID = "00000000-0000-0000-0000-000000000000"
connectionType = "9"
data = @{
host = "10.1.1.1"
username = "sftpuser"
protocol = 1
passwordItem = @{
sensitiveData = "MyPassword123!"
}
}
}Hello dsi1,
Thank you for contacting the Devolutions support team.
We do not recommend creating FileZilla/SFTP entries by manually inventing a New-DSEntryBase -JsonBody schema.
FileZilla is an add-on/provider-style entry, and the provider-specific data is not reliably represented by simple fields such as data.host or data.username.
Since you want to use DS cmdlets only, the supported workaround is to create one valid FileZilla entry as a template, retrieve it with Get-DSEntry -AsRDMConnection, modify the returned ConnectionInfoEntity, and then create the new entry with New-DSEntryBase -FromRDMConnection.
This avoids the New-RDMSession / PSConnection mismatch and preserves the FileZilla-specific internal fields.
Here is a script template to use.
Note that I didn't test this script, and I suggest trying it in a test vault or a test environment before executing it for the production database.
#requires -Modules Devolutions.PowerShell
function ConvertTo-PlainText {
param(
[Parameter(Mandatory)]
[System.Security.SecureString] $SecureString
)
$bstr = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($SecureString)
try {
[Runtime.InteropServices.Marshal]::PtrToStringBSTR($bstr)
}
finally {
if ($bstr -ne [IntPtr]::Zero) {
[Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr)
}
}
}
function Set-NotePropertyValue {
param(
[Parameter(Mandatory)]
[object] $InputObject,
[Parameter(Mandatory)]
[string] $Name,
[Parameter(Mandatory)]
[AllowNull()]
[object] $Value
)
if ($InputObject.PSObject.Properties[$Name]) {
$InputObject.$Name = $Value
}
else {
$InputObject | Add-Member -MemberType NoteProperty -Name $Name -Value $Value -Force
}
}
function New-DSFileZillaSftpEntry {
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[guid] $TemplateEntryId,
[Parameter(Mandatory)]
[guid] $VaultId,
[Parameter(Mandatory)]
[string] $Name,
[Parameter(Mandatory)]
[string] $Group,
[Parameter(Mandatory)]
[string] $HostName,
[Parameter(Mandatory)]
[string] $Username,
[Parameter(Mandatory)]
[System.Security.SecureString] $Password
)
# 1. Fetch an existing working FileZilla entry as a full RDM connection entity.
$connection = Get-DSEntry `
-EntryId $TemplateEntryId `
-VaultID $VaultId `
-AsRDMConnection
if (-not $connection) {
throw "Template entry '$TemplateEntryId' was not found."
}
if (-not $connection.PSObject.Properties['AddOn'] -or -not $connection.AddOn) {
throw "The template entry does not expose AddOn settings. Use a working FileZilla AddOn entry as the template."
}
# 2. Modify the full connection object.
$connection.Name = $Name
$connection.Group = $Group
# Keep the template's ConnectionType and ConnectionSubType.
# For FileZilla AddOn entries, ConnectionSubType must remain the FileZilla add-on identifier.
$connection.AddOn.Host = $HostName
$connection.AddOn.Username = $Username
# If the object exposes a repository/vault property, keep it aligned.
foreach ($propertyName in @('RepositoryID', 'RepositoryId', 'VaultID', 'VaultId')) {
if ($connection.PSObject.Properties[$propertyName]) {
$connection.$propertyName = $VaultId
break
}
}
# 3. Create the new entry using DS cmdlet only.
$createdConnection = New-DSEntryBase -FromRDMConnection $connection
$createdEntryId = $createdConnection.ID
if (-not $createdEntryId) {
$createdEntryId = $createdConnection.Id
}
if (-not $createdEntryId) {
throw "The entry was created, but the new entry ID could not be resolved from the returned object."
}
# 4. Update the password using DS JSON update pattern.
# Update-DSEntryBase documentation shows password updates through data.passwordItem.SensitiveData.
$plainPassword = ConvertTo-PlainText -SecureString $Password
try {
$createdEntry = Get-DSEntry -EntryId $createdEntryId -VaultID $VaultId
if (-not $createdEntry.PSObject.Properties['data'] -or -not $createdEntry.data) {
throw "The created entry does not expose a data object through Get-DSEntry."
}
if (-not $createdEntry.data.PSObject.Properties['passwordItem'] -or -not $createdEntry.data.passwordItem) {
Set-NotePropertyValue -InputObject $createdEntry.data -Name 'passwordItem' -Value ([pscustomobject]@{})
}
Set-NotePropertyValue `
-InputObject $createdEntry.data.passwordItem `
-Name 'SensitiveData' `
-Value $plainPassword
$jsonBody = $createdEntry | ConvertTo-Json -Depth 25
Update-DSEntryBase -JsonBody $jsonBody | Out-Null
}
finally {
$plainPassword = $null
}
# 5. Return the created entry.
Get-DSEntry -EntryId $createdEntryId -VaultID $VaultId
}
# Example usage
$password = Read-Host "SFTP password" -AsSecureString
New-DSFileZillaSftpEntry `
-TemplateEntryId '11111111-1111-1111-1111-111111111111' `
-VaultId '00000000-0000-0000-0000-000000000000' `
-Name 'SFTP - Test 01' `
-Group 'FOLDER01' `
-HostName '10.1.1.1' `
-Username 'sftpuser' `
-Password $password
Best regards,
Patrick Ouimet
Sorry, but it's not working. I'm getting almost no value from the template entry. In particular, there's no "AddOn" property. I only get :
Just to reiterate, I can create the entry, but it doesn't contain some of the data I need:
Hello,
Thank you for your feedback.
Have you tried creating the entry in the DVLS web UI and fetching it from PowerShell as a reverse-engineering method to understand the content of this object, as my colleague Patrick mentioned? The supported workaround is to create one valid FileZilla entry as a template, retrieve it with Get-DSEntry -AsRDMConnection, modify the returned ConnectionInfoEntity, and then create the new entry with New-DSEntryBase -FromRDMConnection.
Properties are maybe unavailable because the FileZilla entry type is an add-on/provider-style entry, and the provider-specific data is not reliably represented by simple fields such as data.host or data.username.
Let us know if that helps.
Best regards,
Érica Poirier
Hello,
I just want to share the work I've been doing on the script since my last response.
Configuring an entry that is not supported by the web UI is quite complex.
However, I was able to set the SFTP protocol and the host in the entry by reverse engineering the process.
Note that the password is still missing in the entry.
I can provide a way to change the credential connection type to Inherited or Linked to the vault.
Also, to ensure that it could be easier for the furtur to set this entry with DS cmdlet, i suggest to open a feature request thread to support this entry type by the web UI.
I will provide the new script next Tuesday after a full test in my environment.
Best regards,
Patrick Ouimet
Hello dsi1,
Here is the script:
#requires -Modules Devolutions.PowerShell
<#
.SYNOPSIS
Clone a FileZilla/WinSCP SFTP template entry in Devolutions Server and link it
to an existing credential entry by CredentialConnectionID.
.DESCRIPTION
This script:
- Authenticates to Devolutions Server with an application identity.
- Fetches a source FTP/SFTP template as an RDM ConnectionInfoEntity.
- Clones the entry into the target vault/folder.
- Updates the connection XML with:
<CredentialConnectionID>1310CF82-6FAB-4B7A-9EEA-3E2E451CA2CF</CredentialConnectionID>
- Removes embedded username/password/domain XML fields from the cloned entry.
.NOTES
If the linked credential is in another vault, pass -CredentialRepositoryId.
If it is in the same vault, do not pass -CredentialRepositoryId.
IMPORTANT:
Do not hardcode application keys in production scripts.
#>
# ============================================================================
# 0. Devolutions Server session - Application identity
# ============================================================================
#requires -Modules Devolutions.PowerShell
# ============================================================================
# 0. Devolutions Server session (Application identity)
# ============================================================================
$env:DS_URL = "<DVLS URL>"
$env:DS_USER = "<App Key>"
$env:DS_PASSWORD = '<App Secret>'
$DSApplicationId = $env:DS_USER
$DSApplicationKey = $env:DS_PASSWORD
$DSBaseUri = $env:DS_URL
$DSSecureKey = ConvertTo-SecureString -String $DSApplicationKey -AsPlainText -Force
$DSCreds = [pscredential]::new($DSApplicationId, $DSSecureKey)
$null = New-DSSession -Credential $DSCreds -BaseURI $DSBaseUri -AsApplication
# ============================================================================
# 1. Helpers
# ============================================================================
function Set-XmlElementValue {
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[System.Xml.XmlElement] $Parent,
[Parameter(Mandatory)]
[string] $TagName,
[Parameter(Mandatory)]
[AllowEmptyString()]
[string] $Value
)
$node = $Parent.SelectSingleNode($TagName)
if (-not $node) {
$node = $Parent.OwnerDocument.CreateElement($TagName)
$Parent.AppendChild($node) | Out-Null
}
$node.InnerText = $Value
}
function Remove-XmlElementIfPresent {
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[System.Xml.XmlElement] $Parent,
[Parameter(Mandatory)]
[string] $TagName
)
$node = $Parent.SelectSingleNode($TagName)
if ($node) {
$Parent.RemoveChild($node) | Out-Null
}
}
function Clear-EmbeddedCredentialXml {
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[System.Xml.XmlElement] $Parent
)
foreach ($tagName in @(
'UserName',
'Username',
'Domain',
'Password',
'SafePassword',
'MnemonicPassword',
'PrivateKeyPassphrase',
'SafePrivateKeyPassphrase'
)) {
Remove-XmlElementIfPresent -Parent $Parent -TagName $tagName
}
}
# ============================================================================
# 2. Clone function - linked credential only
# ============================================================================
function New-DSFileZillaSftpEntry {
[CmdletBinding(SupportsShouldProcess)]
param(
[Parameter(Mandatory)]
[guid] $TemplateEntryId,
[Parameter(Mandatory)]
[guid] $VaultId,
[Parameter(Mandatory)]
[string] $Name,
[Parameter(Mandatory)]
[string] $Group,
[Parameter(Mandatory)]
[string] $HostName,
[ValidateSet('SFtp', 'Ftp', 'Ftps', 'FtpEs')]
[string] $Protocol = 'SFtp',
[ValidateSet('Filezilla', 'WinSCP', 'Native')]
[string] $Application = 'Filezilla',
[Parameter(Mandatory)]
[guid] $CredentialConnectionId,
# Optional. Use only if the credential entry is stored in another vault.
[guid] $CredentialRepositoryId
)
# Fetch the template as a full RDM connection object.
$wrapper = Get-DSEntry -EntryId $TemplateEntryId -AsRDMConnection -ErrorAction Stop
if (-not $wrapper -or -not $wrapper.ConnectionInfo) {
throw "Source entry '$TemplateEntryId' was not found, or did not return a ConnectionInfo object."
}
$conn = $wrapper.ConnectionInfo
if ([string]$conn.ConnectionType -ne 'Ftp') {
throw "Source ConnectionType is '$($conn.ConnectionType)', expected 'Ftp'."
}
if (-not $conn.Data) {
throw "Source entry has no Data XML payload."
}
$newId = [guid]::NewGuid()
# Mirror identity fields on the wrapper.
$conn.Name = $Name
$conn.Group = $Group
$conn.ID = $newId.ToString()
$conn.RepositoryID = [string]$VaultId
if ($conn.PSObject.Properties['GroupMain']) {
$conn.GroupMain = $Group
}
# Edit the connection XML.
[xml] $xml = $conn.Data
$root = $xml.Connection
if (-not $root) {
throw "Source Data XML is missing a <Connection> root element."
}
Set-XmlElementValue -Parent $root -TagName 'ID' -Value $newId.ToString()
Set-XmlElementValue -Parent $root -TagName 'Name' -Value $Name
Set-XmlElementValue -Parent $root -TagName 'Group' -Value $Group
$ftp = $root.SelectSingleNode('Ftp')
if (-not $ftp) {
$ftp = $xml.CreateElement('Ftp')
$root.AppendChild($ftp) | Out-Null
}
Set-XmlElementValue -Parent $ftp -TagName 'Host' -Value $HostName
Set-XmlElementValue -Parent $ftp -TagName 'Protocol' -Value $Protocol
Set-XmlElementValue -Parent $ftp -TagName 'Application' -Value $Application
# Linked credential configuration.
Set-XmlElementValue -Parent $root -TagName 'CredentialConnectionID' `
-Value ($CredentialConnectionId.ToString().ToUpperInvariant())
if ($PSBoundParameters.ContainsKey('CredentialRepositoryId') -and $CredentialRepositoryId -ne [guid]::Empty) {
Set-XmlElementValue -Parent $root -TagName 'CredentialRepositoryID' `
-Value ($CredentialRepositoryId.ToString().ToUpperInvariant())
}
else {
Remove-XmlElementIfPresent -Parent $root -TagName 'CredentialRepositoryID'
}
# Remove inline credential fields from the cloned entry.
Clear-EmbeddedCredentialXml -Parent $root
Clear-EmbeddedCredentialXml -Parent $ftp
$conn.Data = $xml.OuterXml
if ($VerbosePreference -ne 'SilentlyContinue') {
$dumpPath = Join-Path $env:TEMP "ds-clone-$($newId).xml"
$xml.Save($dumpPath)
Write-Verbose "Outgoing XML written to: $dumpPath"
}
if (-not $PSCmdlet.ShouldProcess(
"'$Name' in vault $VaultId",
"Create new FileZilla/WinSCP $Protocol entry from template $TemplateEntryId using linked credential $CredentialConnectionId"
)) {
return
}
# Create the new entry from the modified ConnectionInfoEntity.
$createdResp = New-DSEntryBase -FromRDMConnection $conn -ErrorAction Stop
# Resolve the created entry ID. Response shape can vary by version.
$createdEntryId = $null
if ($createdResp.PSObject.Properties['ConnectionInfo'] -and $createdResp.ConnectionInfo.ID) {
$createdEntryId = $createdResp.ConnectionInfo.ID
}
if (-not $createdEntryId) {
foreach ($propertyName in 'ID', 'Id', 'id') {
if ($createdResp.PSObject.Properties[$propertyName] -and $createdResp.$propertyName) {
$createdEntryId = $createdResp.$propertyName
break
}
}
}
if (-not $createdEntryId -and $createdResp.PSObject.Properties['data'] -and $createdResp.data) {
foreach ($propertyName in 'ID', 'Id', 'id') {
if ($createdResp.data.PSObject.Properties[$propertyName] -and $createdResp.data.$propertyName) {
$createdEntryId = $createdResp.data.$propertyName
break
}
}
}
if (-not $createdEntryId) {
$createdEntryId = $newId
}
# Return a clean result object.
[pscustomobject]@{
EntryId = [guid]$createdEntryId
Name = $Name
Group = $Group
Host = $HostName
Protocol = $Protocol
Application = $Application
CredentialMode = 'Linked'
CredentialConnectionId = [guid]$CredentialConnectionId
CredentialRepositoryId = if ($PSBoundParameters.ContainsKey('CredentialRepositoryId') -and $CredentialRepositoryId -ne [guid]::Empty) {
[guid]$CredentialRepositoryId
}
else {
$null
}
VaultId = [guid]$VaultId
SourceEntryId = [guid]$TemplateEntryId
}
}
# ============================================================================
# 3. Example usage - linked credential only
# ============================================================================
New-DSFileZillaSftpEntry `
-TemplateEntryId '<EntryID>' `
-VaultId 'VaultID' `
-Name '<New Entry Name>' `
-Group 'Folder Name' `
-HostName '<Host Name or IP>' `
-CredentialConnectionId '1310CF82-6FAB-4B7A-9EEA-3E2E451CA2CF' `
-Protocol 'SFtp' `
-Application 'Filezilla' `
-Verbose
With this script, you should be able to adapt it to your environment and add the necessary properties.
Best regards,
Patrick Ouimet
Hello. I can confirm that the script works. I'm not sure how I'll be able to integrate it, but I should be able to manage.
Thank you very much for your help.