Creating a CyberArk Central Policy Manager plugin for an API using WSChains

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.

Interested in seeing WSChains becoming a documented and supported option? Vote on this enhancement request!

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 to Cyberark.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 to 8080 as this is the default port the Shopizer REST API listens on. Debug can be set to Yes 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 of bin\ShopizerAdministratorChains.xml -- the relative path to the XML Chains file that holds the operations of the CPM plugin.

The WSChains DLL 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.

There is not a hard requirement to having a child 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="&quot;id&quot;:(\d+)(?=[^}]*&quot;userName&quot;:&quot;{{username}}&quot;)">
                    <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="&quot;groups&quot;:(\[[^\]]+\])" />
                <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.