Creating a CyberArk Central Policy Manager plugin for an API using WSChains
In a previous article, we created a CyberArk Central Policy Manager plugin for the web application Shopizer using the Web Application CPM Plugin Framework and while it works, we had to employ an awkward workaround to get reconcile functioning. As Shopizer's web interfaces leverage its APIs, we should skip the middleman and have our CPM plugin interface those.
WSChains is an undocumented, unsupported, used-by-CyberArk-interally framework to build CPM plugins that interact with APIs. Using our existing knowledge of Shopizer, examples of WSChains-based plugins from the CyberArk Marketplace, we will build a WSChains-based CPM plugin to manage Shopizer users.
Developing the WSChains plugin
Armed with Shopizer REST API documentation and a bit of knowledge about WSChains, we begin. We will re-use the Shopizer administrator users we previously created.
Creating a new web application platform
We can clone the web application framework-based platform and make some changes:
Under Automatic Password Management -> CPM Plug-in, change
DLLName
toCyberark.Extensions.Plugin.WSChains.dll
.Under Automatic Password Management -> Generate Password, update
PasswordForbiddenChars
to include<>&\"'
, which are characters that need to be escaped inside of XML documents.Under Automatic Password Management -> Additional Policy Settings, set
Port
to8080
as this is the default port the Shopizer REST API listens on.Debug
can be set toYes
but a majority of testing will be done outside of CyberArk.Under Automatic Password Management -> Additional Policy Settings -> Parameters, we only need a single, additional parameter named
ChainsFilePath
with the value ofbin\ShopizerAdministratorChains.xml
-- the relative path to the XMLChains
file that holds the operations of the CPM plugin.
Cyberark.Extensions.Plugin.WSChains.dll
is not available as part of the CPM's base installation. You need to acquire the DLL as part of a platform package from the CyberArk Marketplace or another source.Preparing our parameters file for testing
We can test a WSChains plugin the same way we can test any other and we will do this to facilitate faster and easier development of our plugin. The Parameters file looks like the following:
[targetaccount]
username=shopizeradministrator@timschindler.dev
newpassword=Password2
password=Password2
safename=Safename
foldername=Foldername
objectname=Objectname
PolicyID=PolicyID
; The machine where the Shopizer instance is running.
Address=192.168.178.61
; The port the Shopizer REST API is running on.
Port=8080
[extrapass3]
username=reconcile@timschindler.dev
password=Password1
[extrainfo]
; Path to the chains file.
ChainsFilePath=bin\ShopizerAdministratorChains.xml
Debug=Yes
This is saved as an .ini
file in the root of the CPM folder.
Crafting ShopizerAdministratorChains.xml
Like a WebFormFieldsFile
, the Chains file will hold the details for all three operations of our CPM plugin. For each operation, we will need to define the necessary requests that will be used by the links in the chains that represent each CPM operation.
We start with the bare minimum and some Fails.
<WSChains name="ShopizerAdministrator">
<GlobalSettings>
</GlobalSettings>
<Requests>
</Requests>
<Fails>
<Fail name="FAILBadRequest" rc="8101" message="Status Code = 400. Bad Request" />
<Fail name="FAILUnauthorized" rc="8102" message="Status Code = 401. The request sent by the CPM could not be authenticated" />
<Fail name="FAILForbidden" rc="8103" message="Status Code = 403. Access to the requested (valid) URL by the client is forbidden" />
<Fail name="FAILNotFound" rc="8104" message="Status Code = 404. Resource not found." />
<Fail name="FAILMethodNotFound" rc="8105" message="Status Code = 405. Method not allowed." />
<Fail name="FAILRequestConflict" rc="8106" message="Status Code = 409. Request conflict with current state of the server" />
<Fail name="FAILInternalServerError" rc="8107" message="Status Code = 500. The server cannot process the request for an unknown reason" />
<Fail name="FAILWebServer" rc="8108" message="Status Code = 501. The server cannot process the request for an unknown reason" />
<Fail name="FAILGeneral" rc="8109" message="General Error. Check the logs for more information." />
<Fail name="FAILUnauthorizedCurrPass" rc="8110" message= "Failed to authenticate to the API using the target account username and current password. Status code 401." />
<Fail name="FAILUnauthorizedNewPass" rc="8111" message= "Failed to authenticate to the API using the target account username and new password. Status code 401." />
<Fail name="FAILToParseRecUsername" rc="8112" message= "Failed to parse the target account ID to perform a reconciliation" />
</Fails>
<Chains>
</Chains>
</WSChains>
Save the file as ShopizerAdministratorChains.xml
in the bin
folder of the CPM.
Verify
Like the web application framework plugin, we will simply authenticate to the Shopizer REST API. Authenticating with a correct username and password will result in an authentication token.
In the Requests
element, the following RequestWrapper
needs to be added based on the Shopizer REST API documentation:
<Requests>
<RequestWrapper>
<request name="GetToken">
<version>2</version>
<general>
<endpoint>/api/v1/private/login</endpoint>
<method>POST</method>
</general>
<header>
<Content-Type>application/json</Content-Type>
<Accept>application/json</Accept>
<User-Agent>Integration/1.0 (CyberArk, CPM, 1)</User-Agent>
</header>
<body>
<json>
{
"username":"{{username}}",
"password":"{{pmpass}}"
}
</json>
</body>
</request>
</RequestWrapper>
</Requests>
Inside the request
element, the name
attribute is used to refer to the Request in our Chain's links. {{username}}
and {{pmpass}}
will be replaced with the username and password of the account when the request is executed.
Under the Chains
element, we create a Chain
with the name of verifypass
. It contains only a single Link
:
<Chain name="verifypass">
<Link name="VerifyPasswordLink" request="GetToken">
<StatusCode value="200" next="END">
<Parse name="parsed/token" type="json" value="token" secure="true"/>
</StatusCode>
<StatusCode value="400" next="FAILBadRequest"/>
<StatusCode value="401" next="FAILUnauthorized"/>
<StatusCode value="403" next="FAILForbidden"/>
<StatusCode value="404" next="FAILNotFound"/>
<StatusCode value="405" next="FAILMethodNotFound"/>
<StatusCode value="409" next="FAILRequestConflict"/>
<StatusCode value="500" next="FAILInternalServerError"/>
<StatusCode value="501" next="FAILWebServer"/>
<StatusCode value="*" next="FAILGeneral"/>
</Link>
</Chain>
The Link
object is mostly self-explanatory: the request
attribute defines what Request
is invoked and the different StatusCode
elements determine how the response to the request is handled.
Parse
element under the StatusCode
that represents the end of the CPM operation however not parsing the returned authentication token will result in it being shown in plaintext in the debug logs!When testing, we quickly get positive feedback:
PS C:\Program Files (x86)\CyberArk\Password Manager> .\bin\CANetPluginInvoker.exe shopizerwschains.ini verifypass Cyberark.Extensions.Plugin.WSChains.dll True
The plugin ended successfully (RC = 0)
Looking in the debug logs, we get a nice picture of what the plugin is doing.
Change
Change is made simple for us as in the same response that provides the authentication token, it provides the id
of the user the token is for. This is important as we need to specify the id
of the user whose password we are changing as part of the request URL.
As part of the change operation, at the end, we are going to authenticate with the new password to be sure of the change so we need three requests
in total. We can re-use the GetToken
request but we need one represent the change operation itself as well as logging in with the new password:
<Requests>
<RequestWrapper>
<request name="ChangePassword">
<version>2</version>
<general>
<endpoint>/api/v1/private/user/{{parsed/id}}/password</endpoint>
<method>PATCH</method>
</general>
<header>
<Content-Type>application/json</Content-Type>
<Accept>application/json</Accept>
<User-Agent>Integration/1.0 (CyberArk, CPM, 1)</User-Agent>
<Authorization>Bearer {{parsed/token}}</Authorization>
</header>
<body>
<json>
{
"password": "{{pmpass}}",
"changePassword": "{{pmnewpass}}"
}
</json>
</body>
</request>
</RequestWrapper>
<RequestWrapper>
<request name="GetTokenWithNewPass">
<version>2</version>
<general>
<endpoint>/api/v1/private/login</endpoint>
<method>POST</method>
</general>
<header>
<Content-Type>application/json</Content-Type>
<Accept>application/json</Accept>
<User-Agent>Integration/1.0 (CyberArk, CPM, 1)</User-Agent>
</header>
<body>
<json>
{
"username":"{{username}}",
"password":"{{pmnewpass}}"
}
</json>
</body>
</request>
</RequestWrapper>
</Requests>
We add the Authorization
header with the Bearer token we received from the GetToken
request to the ChangePassword
request. GetTokenWithNewPass
looks exactly like GetToken
but we use {{pmnewpass}}
as the password.
Under the Chains
element, we create a Chain
with the name of changepass
with three Links
:
<Chains>
<Chain name="changepass">
<Link name="ChangePasswordGetTokenLink" request="GetToken">
<StatusCode value="200" next="ChangePasswordLink">
<Parse name="parsed/token" type="json" value="token" secure="true"/>
<Parse name="parsed/id" type="json" value="id" secure="false"/>
</StatusCode>
<StatusCode value="400" next="FAILBadRequest"/>
<StatusCode value="401" next="FAILUnauthorizedCurrPass"/>
<StatusCode value="403" next="FAILForbidden"/>
<StatusCode value="404" next="FAILNotFound"/>
<StatusCode value="405" next="FAILMethodNotFound"/>
<StatusCode value="409" next="FAILRequestConflict"/>
<StatusCode value="500" next="FAILInternalServerError"/>
<StatusCode value="501" next="FAILWebServer"/>
<StatusCode value="*" next="FAILGeneral"/>
</Link>
<Link name="ChangePasswordLink" request="ChangePassword">
<StatusCode value="200" next="VerifyNewPassLink"/>
<StatusCode value="400" next="FAILBadRequest"/>
<StatusCode value="401" next="FAILUnauthorized"/>
<StatusCode value="403" next="FAILForbidden"/>
<StatusCode value="404" next="FAILNotFound"/>
<StatusCode value="405" next="FAILMethodNotFound"/>
<StatusCode value="409" next="FAILRequestConflict"/>
<StatusCode value="500" next="FAILInternalServerError"/>
<StatusCode value="501" next="FAILWebServer"/>
<StatusCode value="*" next="FAILGeneral"/>
</Link>
<Link name="VerifyNewPassLink" request="GetTokenWithNewPass">
<StatusCode value="200" next="END">
<Parse name="parsed/tokennew" type="json" value="token" secure="true" />
</StatusCode>
<StatusCode value="400" next="FAILBadRequest"/>
<StatusCode value="401" next="FAILUnauthorizedNewPass"/>
<StatusCode value="403" next="FAILForbidden"/>
<StatusCode value="404" next="FAILNotFound"/>
<StatusCode value="405" next="FAILMethodNotFound"/>
<StatusCode value="409" next="FAILRequestConflict"/>
<StatusCode value="500" next="FAILInternalServerError"/>
<StatusCode value="501" next="FAILWebServer"/>
<StatusCode value="*" next="FAILGeneral"/>
</Link>
</Chain>
</Chains>
The first Link
is copied from our verify operation's Chain
. The only difference is the addition of the Parse
element that captures the id
of the account so we can use it in the next Link. The next
attribute of the Link tells the plugin the name of the Link in the Chain to execute.
ChangePasswordLink
invokes the ChangePassword
request and when executed successfully (the response's status code is 200
), VerifyNewPassLink
is called to login with the password just set. Again we parse the result so that we can mask the authentication token from the response in the logs.
Testing shows success:
PS C:\Program Files (x86)\CyberArk\Password Manager> .\bin\CANetPluginInvoker.exe shopizerwschains.ini changepass Cyberark.Extensions.Plugin.WSChains.dll True
The plugin ended successfully (RC = 0)
Reconcile
Reconcile is complex. We need to use a different endpoint when changing the password of another user. The request URL includes the id
of the user to be changed -- requiring us to either store it as an account property or look this up as part of the CPM operation -- and because it is a PUT operation, we need to provide all the existing details of the user along with the new password, otherwise they will be deleted from the user's profile.
There is an endpoint that will list all users and their id
so we will need to call this as a link in our reconcile operation's chain and parse the response. We will then retrieve the user's profile in order to get the information we need to pass along as part of the change password request.
We need the following, additional requests:
<Requests>
<RequestWrapper>
<request name="RecGetToken">
<version>2</version>
<general>
<endpoint>/api/v1/private/login</endpoint>
<method>POST</method>
</general>
<header>
<Content-Type>application/json</Content-Type>
<Accept>application/json</Accept>
<User-Agent>Integration/1.0 (CyberArk, CPM, 1)</User-Agent>
</header>
<body>
<json>
{
"username":"{{extrapass3\username}}",
"password":"{{pmextrapass3}}"
}
</json>
</body>
</request>
</RequestWrapper>
<RequestWrapper>
<request name="GetUserByID">
<version>2</version>
<general>
<endpoint>/api/v1/private/users</endpoint>
<method>GET</method>
</general>
<header>
<Content-Type>application/json</Content-Type>
<Accept>application/json</Accept>
<User-Agent>Integration/1.0 (CyberArk, CPM, 1)</User-Agent>
<Authorization>Bearer {{parsed/token}}</Authorization>
</header>
<body>
<json></json>
</body>
</request>
</RequestWrapper>
<RequestWrapper>
<request name="GetUserProfileByID">
<version>2</version>
<general>
<endpoint>/api/v1/private/users/{{parsed/id}}</endpoint>
<method>GET</method>
</general>
<header>
<Content-Type>application/json</Content-Type>
<Accept>application/json</Accept>
<User-Agent>Integration/1.0 (CyberArk, CPM, 1)</User-Agent>
<Authorization>Bearer {{parsed/token}}</Authorization>
</header>
<body>
<json></json>
</body>
</request>
</RequestWrapper>
<RequestWrapper>
<request name="RecChangePassword">
<version>2</version>
<general>
<endpoint>/api/v1/private/user/{{parsed/id}}</endpoint>
<method>PUT</method>
</general>
<header>
<Content-Type>application/json</Content-Type>
<Accept>application/json</Accept>
<User-Agent>Integration/1.0 (CyberArk, CPM, 1)</User-Agent>
<Authorization>Bearer {{parsed/token}}</Authorization>
</header>
<body>
<json>
{
<!-- No quotes because it needs to be treated as a bool -->
"active": {{parsed/active}},
"defaultLanguage": "{{parsed/defaultLanguage}}",
"firstName": "{{parsed/firstName}}",
<!-- The value for the groups key will be a string representation of the groups array. -->
"groups:": {{parsed/groups}},
"id": "{{parsed/id}}",
"lastName": "{{parsed/lastName}}",
"password": "{{pmnewpass}}",
"repeatPassword": "{{pmnewpass}}",
"store": "{{parsed/merchant}}",
"emailAddress": "{{parsed/emailAddress}}",
"userName": "{{parsed/userName}}"
}
</json>
</body>
</request>
</RequestWrapper>
</Requests>
The RecGetToken
request is like GetToken
but uses the linked reconcile account's username and password. The GetUserByID
and GetUserProfileByID
requests facilitate retrieving the needed details from the user's profile while RecChangePassword
is used to change the password, including the parsed details.
The reconcilepass
Chain will have five links:
<Chains>
<Chain name="reconcilepass">
<Link name="ReconcilePasswordGetTokenLink" request="RecGetToken">
<StatusCode value="200" next="GetUserByIDLink">
<Parse name="parsed/token" type="json" value="token" secure="true" />
</StatusCode>
<StatusCode value="400" next="FAILBadRequest"/>
<StatusCode value="401" next="FAILUnauthorizedCurrPass"/>
<StatusCode value="403" next="FAILForbidden"/>
<StatusCode value="404" next="FAILNotFound"/>
<StatusCode value="405" next="FAILMethodNotFound"/>
<StatusCode value="409" next="FAILRequestConflict"/>
<StatusCode value="500" next="FAILInternalServerError"/>
<StatusCode value="501" next="FAILWebServer"/>
<StatusCode value="*" next="FAILGeneral"/>
</Link>
<Link name="GetUserByIDLink" request="GetUserByID">
<StatusCode value="200" next="ByParse" condition="OR" nomatch="FAILToParseRecUsername">
<Parse name="parsed/id" type="text" value=""id":(\d+)(?=[^}]*"userName":"{{username}}")">
<Equals value="Success" next="END"/>
<Equals value="*" next="GetUserProfileByIDLink"/>
</Parse>
</StatusCode>
<StatusCode value="400" next="FAILBadRequest"/>
<StatusCode value="401" next="FAILUnauthorized"/>
<StatusCode value="403" next="FAILForbidden"/>
<StatusCode value="404" next="FAILNotFound"/>
<StatusCode value="405" next="FAILMethodNotFound"/>
<StatusCode value="409" next="FAILRequestConflict"/>
<StatusCode value="500" next="FAILInternalServerError"/>
<StatusCode value="501" next="FAILWebServer"/>
<StatusCode value="*" next="FAILGeneral"/>
</Link>
<Link name="GetUserProfileByIDLink" request="GetUserProfileByID">
<StatusCode value="200" next="ReconcilePasswordLink">
<!-- Uses method=lowercase as we later treat it as a bool. Works with at least v12.5.7.3 of WSChains.dll-->
<Parse name="parsed/active" type="json" value="active" method="lowercase"/>
<Parse name="parsed/defaultLanguage" type="json" value="defaultLanguage" />
<Parse name="parsed/emailAddress" type="json" value="emailAddress" />
<Parse name="parsed/firstName" type="json" value="firstName" />
<!-- Use regex to get the value of the groups key as when we parse it as type json, it cannot convert an Array to string. -->
<Parse name="parsed/groups" type="text" value=""groups":(\[[^\]]+\])" />
<Parse name="parsed/lastName" type="json" value="lastName" />
<Parse name="parsed/merchant" type="json" value="merchant" />
<Parse name="parsed/username" type="json" value="userName" />
</StatusCode>
<StatusCode value="400" next="FAILBadRequest"/>
<StatusCode value="401" next="FAILUnauthorized"/>
<StatusCode value="403" next="FAILForbidden"/>
<StatusCode value="404" next="FAILNotFound"/>
<StatusCode value="405" next="FAILMethodNotFound"/>
<StatusCode value="409" next="FAILRequestConflict"/>
<StatusCode value="500" next="FAILInternalServerError"/>
<StatusCode value="501" next="FAILWebServer"/>
<StatusCode value="*" next="FAILGeneral"/>
</Link>
<Link name="ReconcilePasswordLink" request="RecChangePassword">
<StatusCode value="200" next="RecVerifyNewPassLink"/>
<StatusCode value="400" next="FAILBadRequest"/>
<StatusCode value="401" next="FAILUnauthorized"/>
<StatusCode value="403" next="FAILForbidden"/>
<StatusCode value="404" next="FAILNotFound"/>
<StatusCode value="405" next="FAILMethodNotFound"/>
<StatusCode value="409" next="FAILRequestConflict"/>
<StatusCode value="500" next="FAILInternalServerError"/>
<StatusCode value="501" next="FAILWebServer"/>
<StatusCode value="*" next="FAILGeneral"/>
</Link>
<Link name="RecVerifyNewPassLink" request="GetTokenWithNewPass">
<StatusCode value="200" next="END">
<Parse name="parsed/tokennew" type="json" value="token" secure="true"/>
</StatusCode>
<StatusCode value="400" next="FAILBadRequest"/>
<StatusCode value="401" next="FAILUnauthorizedNewPass"/>
<StatusCode value="403" next="FAILForbidden"/>
<StatusCode value="404" next="FAILNotFound"/>
<StatusCode value="405" next="FAILMethodNotFound"/>
<StatusCode value="409" next="FAILRequestConflict"/>
<StatusCode value="500" next="FAILInternalServerError"/>
<StatusCode value="501" next="FAILWebServer"/>
<StatusCode value="*" next="FAILGeneral"/>
</Link>
</Chain>
</Chains>
The heavy lifting is done in GetUserByIDLink
and GetUserProfileByIDLink
.
Regular expression is used to get the id
of the user we are trying to reconcile in GetUserByIDLink
. In GetUserProfileByIDLink
, we can easily parse all the details except for the groups as this is a JSON array and again we use regular expression to grab the array as a string.
Again, our testing validates the chain.
PS C:\Program Files (x86)\CyberArk\Password Manager> .\bin\CANetPluginInvoker.exe shopizerwschains.ini reconcilepass Cyberark.Extensions.Plugin.WSChains.dll True
The plugin ended successfully (RC = 0)
Conclusion
WSChains is a great framework and our CPM plugin uses only a small amount of its features. I previously said it being unsupported by CyberArk makes it unsuitable for production use but I no longer agree with that statement. The maturity of WSChains and the functionality of it makes it a no-brainer for developing CPM plugins that interact with REST APIs.
The platform files, Chain file, and parameter file can be found in my GitHub repository. If you are interested in developing your own CPM plugin for Shopizer administrator users, you can use this docker-compose.ymlto quickly stand up a containerized Shopizer environment. Check out the README for more information. The same GitHub repository contains the platform files, Chain file, and a WSChains DLL.