Large organizations hugely rely on systems managed via SSH interface. MidPoint only provides a lightweight built-in SSH connector with limited capabilities. So, we decided to move it to the next level by developing an adaptive midpoint SSH connector to enable greater control over SSH-managed systems.
Why We Developed the Adaptive midPoint SSH Connector
The original SSH Connector was designed as an additional connector, meaning it could only be used before or after the main identity operation. For example, it could add a user to a specific group only after an Active Directory account was created or create a mailbox only after the initial provisioning step.
While this worked for simple use cases, it became a significant limitation for complex identity management scenarios (for example, role-based management). The lack of projection capabilities meant that organizations couldn’t fully integrate SSH-based systems into their midPoint provisioning workflows. By them, we mean the lack of automatical data provisioning, meaning if provisioned data was changed, IDM is not able to correct them since it has no track of the current state (we are not saying that there are no ways around this, but they are not ideal).
This is where the Adaptive midPoint SSH Connector was born. We needed a scalable and adaptable solution capable of handling a wide variety of SSH-enabled systems. That’s why we introduced dynamic schema support—to bridge the massive gap between different SSH-based environments like Microsoft Exchange and OpenBSD. Now, with the Adaptive midPoint SSH Connector, provisioning across diverse systems is not just possible— it’s seamless.
Why Choose the Adaptive midPoint SSH Connector?
If you’ve used the original SSH Connector from Evolveum, you’ll love the improvements and added functionality of the Adaptive SSH Connector. While the previous version primarily enabled script execution, the Adaptive midPoint SSH Connector takes it to the next level with provisioning, password management, and dynamic schema support.
Key Upgrades Over the SSH Connector:
Feature | SSH Connector | Adaptive SSH Connector |
Provisioning | ❌ Not Supported | ✅ Supported |
Schema | ❌ Not Supported | ✅ Supported |
Password Management | ❌ Not Supported | ✅ Supported |
Live Synchronization | ❌ Not Supported | ❌ Not Supported |
Script Execution | ✅ Supported | ✅ Supported |
This may sound great on paper, but let’s examine a simple example together to fully understand what we have achieved.
MS Exchange Integration
Let’s say that you have an organization that wants you to fully provision their mailboxes on on-prem MS Exchange and manage them by assigning a special role called “BUS:User Mailbox.” So, let’s begin. The first step is to get your SSH access to the server where Exchange is running. Once you connect, you have a first really important step ahead of you.
Set up PowerShell as the main shell
Clever reader who read the README on our GitHub already knows that. We can achieve this by executing the following command
New-ItemProperty -Path "HKLM:\SOFTWARE\OpenSSH" -Name DefaultShell -Value "C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe" -PropertyType String -Force
Now that we have this out of the way (since you need to do this only once), we can continue.
Take a look at the Mailbox data
For us to create a dynamic schema, we need to know what data we have available. So, for that purpose, let’s call:
# these steps are mandatory for use to have session to Exchange$Session = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri https://ExchangeServerAddr -Authentication KerberosImport-PSSession $Session -CommandName $commandsToImport -AllowClobber> $null
# get one mailbox based on the GUID
Get-Mailbox -Identity $ExchangeGuid
take a look at the data and choose what you need for your integration. The real basic ones are:
- GUID – we will use it as ICFS:UID
- UserPrincipalName or UPN – we will use it as ICFS:NAME
- PrimarySmtpAddress – this one we will map as ri:email
- EmailAddresses – this will be represented as ri:emailAddresses
- Password – this one you will not see in return data, but you will need it for mailbox to be fully functional
Now that we have the data we want to provision, we can create our dynamic schema.
Dynamic Schema
From our selected data we will create schemaConfig.json as follow:
{
"objects":[
{
"icfsName": "UserPrincipalName",
"icfsUid": "ExchangeGuid",
"objectClass": "user",
"createScript": "C:\\Users\\inaadm\\ex\\usr\\createScript.ps1",
"updateScript": "C:\\Users\\inaadm\\ex\\usr\\updateScript.ps1",
"deleteScript": "C:\\Users\\inaadm\\ex\\usr\\deleteScript.ps1",
"searchScript": "C:\\Users\\inaadm\\ex\\usr\\searchScript.ps1",
"attributes": [
{
"email": {
"required": false,
"creatable": true,
"updateable": true,
"dataType": "String",
"multivalued": false,
"returnedByDefault": true,
"readable": true
},
"emailAddresses": {
"required": false,
"creatable": false,
"updateable": true,
"dataType": "String",
"multivalued": true,
"returnedByDefault": true,
"readable": true
},
"password": {
"required": true,
"creatable": true,
"updateable": false,
"dataType": "String",
"multivalued": false,
"returnedByDefault": false,
"readable": false
}
}
]
}
]
}
As you can see, it is simple. You simply write the names you choose, data type, multiplicity, and operations you require for each specific attribute. A clever reader noticed that there are also some “scripts” defined. Yes, those are your main data management tools, but more about them later in this blog.
Connector Config
Before diving into the scripts themselves, we need to tell the connector some other important information. For example:
- how you will be representing empty values
- what will be the password attribute (native or custom)
- what to do with white spaces
- what will be your attribute separator
- what will be your multivalue separator
- your new line character
- and prefixes for ADD and REMOVE for multivalue attributes
You see, these SSH-like systems can be very different from one to another. Even on Exchange, two different versions could require a different approach. For that reason, you’ll need to set these things up. We only ensure the connector will react and adjust for such situations. When you finish, you should end up with something like this (for Exchange, you should be good to go with this):
{
"configName": "Configuration for Microsoft Exchange v1.1.0",
"settings": {
"scriptResponseSettings": {
"scriptEmptyAttribute": "null",
"multiValuedAttributeSeparator": "~",
"responseNewLineSeparator": "\n",
"responseColumnSeparator": "||"
},
"connectorSettings": {
"replaceWhiteSpaceCharacterInAttributeValues": {
"enabled": false,
"value": "---"
},
"addSudoExecution": {
"enabled": false,
"value": "sudo"
},
"icfsPasswordFlagEquivalent": {
"enabled": false,
"value": "password"
},
"icfsUidFlagEquivalent": {
"enabled": false,
"value": ""
},
"icfsNameFlagEquivalent": {
"enabled": true,
"value": "name"
}
},
"createOperationSettings": {
"alreadyExistsErrorParameter": "ObjectAlreadyExists",
"successStatusMessage": ""
},
"updateOperationSettings": {
"unknownUidException": "UnknownUid",
"updateDeltaAddParameter": "ADD:",
"updateDeltaRemoveParameter": "REMOVE:",
"updateSuccessResponse": ""
},
"deleteOperationSettings": {
"deleteSuccessResponse": ""
},
"searchOperationSettings": {
"noResultSuccessMessage": ""
}
}
}
Scripts
Now we have our configurations out of the way, we can move forward with our scripts. We recommend writing the scripts separately and testing them directly before uploading them to IDM for usage. This way, you will be able to identify possible issues better and avoid problems with scripts. Depending on your needs, you need to create scripts. In our case, we will be creating the complete set:
– SEARCH
– CREATE
– UPDATE
– DELETE
This way, you will have a full picture of what is happening. So, let’s start with the most basic one you will always need – SEARCH.
SEARCH
As we all know, a search in IdentityConnectorFramework needs to have at least get and getAll. Taking that into account, we can end up with something like this:
param(
[string]$ExchangeGuid
)
# searchScript.ps1 must always return all attributes defined in schema for particular objectClass
$columnsHeaderDefinition = "ExchangeGuid||UserPrincipalName||Email||EmailAddresses"
$commandsToImport = "Get-Mailbox"
$Session = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri https://ExchangeServerAddr -Authentication Kerberos
Import-PSSession $Session -CommandName $commandsToImport -AllowClobber > $null
# search for 1 mailbox based on Guid
if ($ExchangeGuid){
$mailbox = Get-Mailbox -Identity $ExchangeGuid -ErrorAction SilentlyContinue
if ($mailbox) {
$email = $mailbox.PrimarySmtpAddress
$mailboxUpn = $mailbox.UserPrincipalName
$EmailAddresses = $mailbox.EmailAddresses
Write-Host $columnsHeaderDefinition
Write-Host "$ExchangeGuid||$mailboxUpn||$email||$EmailAddresses"
}
else {
Write-Host "Error mailBox notfound"
}
}
else {
# Get all mailboxes and output their exchangeGuid and all attributes defined in schema
$mailboxes = Get-Mailbox -ResultSize Unlimited
Write-Host $columnsHeaderDefinition
foreach ($mailbox in $mailboxes)
{
$guid = $mailbox.ExchangeGuid
$email = $mailbox.PrimarySmtpAddress
$EmailAddresses = $mailbox.EmailAddresses
$mailboxUpn = $mailbox.UserPrincipalName
Write-Host "$guid||$mailboxUpn||$email||$EmailAddresses"
}
}
Remove-PSSession $Session > $null
Lets take a look on some important notes here. First notice, that we’re creating header for our response and commands we want to import:
$columnsHeaderDefinition = "ExchangeGuid||UserPrincipalName||Email||EmailAddresses"
$commandsToImport = "Get-Mailbox"
Powershell scripts for microsoft exchange use weird UI element when importing remote session in terminal, sshj which is responsible for executing/reading output crash since by default sshj create connection with -T flag, so it needs to be bypassed. To bypass this every command should be imported separately! To test this simply connect to your testing server with ssh -T name@host and execute test script.
Then we will create our session to Exchange:
$Session = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri https://ExchangeServerAddr -Authentication Kerberos
Import-PSSession $Session -CommandName $commandsToImport -AllowClobber > $null
That is needed since the connector is basically reading output from scripts. You need to define this header so it knows which column it should expect values for attributes in.
Then, we execute our get/getAll command and parse the output. Then, we create our response back to the connector:
Write-Host $columnsHeaderDefinition
Write-Host "$ExchangeGuid||$mailboxUpn||$email||$EmailAddresses"
We print the header, and then we iterate over mailboxes to return the attributes with our delimiter. And then, in the last step, we remove our session, please don’t forget to do this, or you can run out of sessions:
Remove-PSSession $Session > $null
And that’s our SEARCH script.
CREATE
The create script is pretty straightforward. Take input params, put them in command, and execute it. That said, here is the final result:
param(
[string]$name,
[string]$email,
[string]$password
)
# example of powershell script for creating mailbox
$columnsHeaderDefinition = "ExchangeGuid||UserPrincipalName"
$commandsToImport = "New-Mailbox"
$Session = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri https://ExchangeServerAddr -Authentication Kerberos
Import-PSSession $Session -CommandName $commandsToImport -AllowClobber > $null
$securePassword = ConvertTo-SecureString $password -AsPlainText -Force
# Create mailbox
$mailbox = New-Mailbox -Name $name -UserPrincipalName "$name@example.net" -PrimarySmtpAddress $email -Password $securePassword
$mailboxName = $mailbox.UserPrincipalName
$guid = $mailbox.ExchangeGuid
Write-Host $columnsHeaderDefinition
Write-Host "$guid||$mailboxName"
Remove-PSSession $Session > $null
Clever reader noticed that other than that, we again define headers. That is for IdentityConnectorFramework because, as we all know, it expects us to return ICFS:UID back after a successful creation operation. And that is what we are doing:
Write-Host $columnsHeaderDefinition
Write-Host "$guid||$mailboxName"
Let’s move on to a more interesting UPDATE operation.
UPDATE
The update script is a bit more tricky, but only if you have multi-value attributes. If not, it is easy. But we want to show you a more complex example, so we will include the EmailAddresses attribute. So let’s take a look:
param(
[string]$ExchangeGuid,
[string]$Email,
[string[]]$EmailAddresses
)
$commandsToImport = "Set-Mailbox", "Get-Mailbox"
$Session = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri http://PowershellServerAddr -Authentication Kerberos
Import-PSSession $Session -CommandName $commandsToImport -AllowClobber > $null
# example of updateScript
# .\updateScript.ps1 -ExchangeGuid "a97f7a95-299d-46d0-8687-787fa9298c2d" -email "jon.stark@protonmail.com" -EmailAddresses "ADD:smtp:jon.targaryen@protonmail.com ","REMOVE:smtp:jon.snow@protonmail.com"
# handle multivalued operation (singlevalue replace too if present) if no error occurs this script should return ""
if($EmailAddresses){
# Split EmailAddresses into an array of operations
$operations = $EmailAddresses -split ' '
# Loop through the operations
foreach($operation in $operations){
$mailbox = Get-Mailbox -Identity $ExchangeGuid
# Split each operation into action (ADD or REMOVE) and emailaddress
$action, $emailaddress = $operation -split ':', 2
# Check if email is present
$isPresent = $mailbox.EmailAddresses -contains $emailaddress
switch($action){
'ADD' {
if(!$isPresent){
$mailbox.EmailAddresses += $emailaddress
Set-Mailbox $mailbox.Identity -EmailAddresses $mailbox.EmailAddresses
}
}
'REMOVE' {
if($isPresent){
$mailbox.EmailAddresses = $mailbox.EmailAddresses -ne $emailaddress
Set-Mailbox $mailbox.Identity -EmailAddresses $mailbox.EmailAddresses
}
}
default {
#Write-Host "ERROR"
}
}
}
if ($email){
# handle replace
# Update the PrimarySmtpAddress
Set-Mailbox -Identity $ExchangeGuid -PrimarySmtpAddress $email
}
}
# handle replace single value if $emailAddresses is not passed as argument
else {
#check which attribute to update
if ($email){
# Update the mailbox name
Set-Mailbox -Identity $ExchangeGuid -PrimarySmtpAddress $email
}
}
Remove-PSSession $Session > $null
All other commands are the same. Now, take a look at the logic in the script. We start by dividing the update operation into two separate actions:
1. update – single value attributes only
2. update & add/remove values – multi-value attribute is present in data from IDM
This way, if you only update single-value attributes, the script will avoid any specific multi-value logic, and you save up some performance. But when the multi-value attribute is present (EmailAddress in our case), then we need to check if we want to “ADD” or “REMOVE” values from it. Based on that, the appropriate commands are called on Exchange. Then, we also check if we have a single value attribute present, and we will update them as well. This script and the logic around it can be written based on your preferences. Just keep in mind that you should divide your actions appropriately based on inputs from IDM. And that’s our update script.
DELETE
This is the most simplest of all. Just take input and call delete on Exchange, like so:
param(
[string]$ExchangeGuid
)
# example of delete script
# .\deleteScript.ps1 -ExchangeGuid "a97f7a95-299d-46d0-8687-787fa9298c2d"
$commandsToImport = "Remove-Mailbox"
$Session = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri https://ExchangeServerAddr -Authentication Kerberos
Import-PSSession $Session -CommandName $commandsToImport -AllowClobber > $null
# Remove mailbox imidiatelly from DB
Remove-Mailbox -Identity $ExchangeGuid -Permanent $true -Confirm:$false
Remove-PSSession $Session > $null
Simple right? We can finally move to IDM and set up our resource configuration.
Resource Configuration
Resource configuration is the same as for any other connector. That said, here is a resource for our example:
<resource xmlns="http://midpoint.evolveum.com/xml/ns/public/common/common-3"
xmlns:c="http://midpoint.evolveum.com/xml/ns/public/common/common-3"
xmlns:icfs="http://midpoint.evolveum.com/xml/ns/public/connector/icf-1/resource-schema-3"
xmlns:org="http://midpoint.evolveum.com/xml/ns/public/common/org-3"
xmlns:q="http://prism.evolveum.com/xml/ns/public/query-3"
xmlns:ri="http://midpoint.evolveum.com/xml/ns/public/resource/instance-3"
xmlns:t="http://prism.evolveum.com/xml/ns/public/types-3"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
oid="c7d7ad50-ef91-4174-87c3-a3d84d1c317c">
<name>SshMicrosoftExchange</name>
<connectorRef relation="org:default" type="c:ConnectorType">
<filter>
<q:equal>
<q:path>c:connectorType</q:path>
<q:value>com.inalogy.midpoint.connectors.ssh.AdaptiveSshConnector</q:value>
</q:equal>
</filter>
</connectorRef>
<connectorConfiguration xmlns:icfc="http://midpoint.evolveum.com/xml/ns/public/connector/icf-1/connector-schema-3">
<icfc:configurationProperties xmlns:gen629="http://midpoint.evolveum.com/xml/ns/public/connector/icf-1/bundle/com.inalogy.midpoint.connectors.ssh/com.inalogy.midpoint.connectors.ssh.AdaptiveSshConnector">
<gen629:host>ipAddr</gen629:host>
<gen629:schemaFilePath>/opt/midpoint/var/ssh-schema/schemaConfig.json</gen629:schemaFilePath>
<gen629:dynamicConfigurationFilePath>/opt/midpoint/var/ssh-schema/connectorConfig.json</gen629:dynamicConfigurationFilePath>
<gen629:shellType>powershell</gen629:shellType>
<gen629:port>22</gen629:port>
<gen629:username>svc-midpoint</gen629:username>
<gen629:password>
<t:clearValue></t:clearValue>
</gen629:password>
<gen629:authenticationScheme>password</gen629:authenticationScheme>
</icfc:configurationProperties>
</connectorConfiguration>
<schemaHandling>
<objectType>
<kind>account</kind>
<intent>user</intent>
<displayName>User</displayName>
<default>true</default>
<delineation>
<objectClass>ri:user</objectClass>
</delineation>
<focus>
<type>UserType</type>
</focus>
<attribute>
<ref>icfs:name</ref>
<displayName>Name</displayName>
<outbound>
<strength>strong</strength>
<source>
<path>name</path>
</source>
</outbound>
</attribute>
<attribute>
<ref>ri:email</ref>
<displayName>Email Address</displayName>
<outbound>
<strength>strong</strength>
<source>
<path>emailAddress</path>
</source>
</outbound>
</attribute>
<credentials>
<password>
<outbound>
<strength>normal</strength>
<!-- asIs -->
</outbound>
</password>
</credentials>
<correlation>
<correlators>
<filter>
<ownerFilter>
<q:equal>
<q:path>emailAddress</q:path>
<expression>
<path>$shadow/attributes/icfs:name</path>
</expression>
</q:equal>
</ownerFilter>
</filter>
</correlators>
</correlation>
<synchronization>
<reaction>
<name>linked -> synchronized</name>
<situation>linked</situation>
<actions>
<synchronize/>
</actions>
</reaction>
<reaction>
<name>unlinked -> link</name>
<situation>unlinked</situation>
<actions>
<link/>
</actions>
</reaction>
<reaction>
<name>deleted -> unlink</name>
<situation>deleted</situation>
<actions>
<unlink/>
</actions>
</reaction>
<reaction>
<name>unmatched -> disabled</name>
<situation>unmatched</situation>
<condition>
<value>false</value>
</condition>
</reaction>
</synchronization>
</objectType>
</schemaHandling>
</resource>
As you can see, it is the same resource configuration as for any other connector. There is nothing special here. And that’s it—you have your Exchange ready.
Conclusion
Adaptive midPoint SSH Connector is a great tool that can help you integrate any SSH-like platform with midPoint. But, with power comes a great responsibility, which in this case is that you need to know the system to be able to define dynamic schema, connector configuration, and scripts. However, you are not required to make any modifications directly in Java. We’ve demonstrated you basic integration with MS Exchange, which (if you know what you are doing) can be done within one day. This blog will help you better understand our connector and how to work with it.
If you have any questions or feedback, we will be more than happy to hear from you!
Author: František Mikuš
Want to read more ?