Test Security Material in CPI Groovy Scripts

/

,

/

Views: 250

Introduction

As a follow up from my previous post on using mocks to test value mapping in CPI, this time round I will be looking into how to test security material in CPI Groovy scripts.

Around 9 years ago, when I first dabbled in the world of cloud integration, I wrote about developing custom OAuth 2.0 authentication. In the last 6 months alone, I still find myself having to deal with many flavours of authentication when accessing APIs by various providers. The reality is that there is no one-size-fits-all approach to authentication.

In order to tackle such requirements, inevitably some Groovy scripting will be involved to access the security material(s) that securely stores the credentials. With a mock implementation of com.sap.it.api.securestore.SecureStoreService, we can confidently develop and test our Groovy script logic locally before moving into the tenant.


Setting the Stage

For our (simple) example, we will use a service the requires the client ID and secret to be sent in a JSON payload (shown below). This differs from the approach of OAuth 2.0 which uses application/x-www-form-urlencoded for the transfer.

{
  "clientId": "sampleId",
  "clientSecret": "sampleSecret"
}

For the design, we will create a Security Material of type User Credentials to store the ID and secret securely.

In the integration flow, an externalised parameter will hold the name of the security material.

The following source code will be the logic for Groovy script, which does the following:

  • Gets the name of the security material from the property
  • Uses SecureStoreService to access the credential
  • Generates a JSON payload which contains the client ID and secret
import com.sap.gateway.ip.core.customdev.util.Message
import com.sap.it.api.ITApiFactory
import com.sap.it.api.securestore.SecureStoreService
import com.sap.it.api.securestore.UserCredential
import groovy.json.JsonOutput

Message processData(Message message) {
    def clientCredentialsSecureMaterialName = message.getProperty('ClientCredentialsSecureMaterialName')
    def secureStorageService = ITApiFactory.getService(SecureStoreService, null)
    UserCredential userCredential = secureStorageService.getUserCredential(clientCredentialsSecureMaterialName)

    def clientId = userCredential.getUsername()
    def clientSecret = userCredential.getPassword().toString()

    Map output = ['clientId': clientId, 'clientSecret': clientSecret]

    message.setBody(JsonOutput.toJson(output))
    message.setHeader('Content-Type', 'application/json')
    return message
}

Go Ahead, Mock Me!

Similar to the previous post, we need to be able to write and read simulated security material entries. Mock implementations allow us to achieve this in a local development environment. Following are the mock implementations of two key classes that are required.

com.sap.it.api.securestore.SecureStoreService

This class provides access to the user credential that is named in the alias. The key methods of the class are:

  • addCredential – adds a user credential entry which is stored in the class’s map
  • getUserCredential – retrieves a user credential entry from the class’s map
package com.sap.it.api.securestore

import com.sap.it.api.securestore.exception.SecureStoreException

class SecureStoreService {

    private static SecureStoreService secureStoreService
    private Map<String, UserCredential> userCredentials = [:]

    static SecureStoreService getInstance() {
        if (!secureStoreService) {
            secureStoreService = new SecureStoreService()
        }
        return secureStoreService
    }

    void addCredential(String alias, UserCredential userCredential) {
        userCredentials.put(alias, userCredential)
    }

    UserCredential getUserCredential(String alias) throws SecureStoreException {
        if (!userCredentials.containsKey(alias)) {
            throw new SecureStoreException("Could not fetch the credential for alias ${alias}")
        }
        return userCredentials.get(alias)
    }
}

com.sap.it.api.securestore.UserCredential

This class provides the actual user name and password that is stored in the credential. Its key methods are described below but not explicitly written in the code because Groovy’s compiler automatically provides default getters and setters.

  • getUsername – returns the user name stored in the credential
  • getPassword – returns the password stored in the credential
package com.sap.it.api.securestore

class UserCredential {

    String username
    char[] password

    UserCredential(String username, String password) {
        this.username = username
        this.password = password.toCharArray()
    }
}

Available Maven library

Similar to the mock implementation for value mapping, these mocks are also bundled in the same Maven library, and can be easily included by adding the following sections in the POM file.

i) Add the following dependency in the dependencies section of the POM.

<dependency>
    <groupId>com.equaliseit</groupId>
    <artifactId>sap-cpi-mocks</artifactId>
    <version>1.0.2</version>
</dependency>

ii) Add the following GitLab repository URL in the repositories section of the POM.

<repositories>
  <repository>
      <id>gitlab-maven</id>
      <url>https://gitlab.com/api/v4/groups/12926885/-/packages/maven</url>
  </repository>
</repositories>

Let’s Test This Out!

Now that all the key pieces are in place, we can test out the logic in a local development environment with some unit tests.

As always, my preferred approach is using a Spock specification. The following source code contains units tests to test the following scenarios:

  • Scenario 1 – Credential exists
    • Entry for Sample-Client-Credentials is stored using the mock
    • Parse JSON output to check that it contains the values that were passed into the mock entry
  • Scenario 2 – Credential missing
    • No credential was stored in the mock
    • Verify that an exception was thrown to indicate the credential was not found
import com.sap.gateway.ip.core.customdev.util.Message
import com.sap.it.api.securestore.SecureStoreService
import com.sap.it.api.securestore.UserCredential
import com.sap.it.api.securestore.exception.SecureStoreException
import groovy.json.JsonSlurper
import spock.lang.Shared
import spock.lang.Specification

class SetAuthorizationPayloadSpec extends Specification {
    @Shared
    Script script

    Message msg

    def setupSpec() {
        GroovyShell shell = new GroovyShell()
        script = shell.parse(this.getClass().getResource('/script/SetAuthorizationPayload.groovy').toURI())
    }

    def setup() {
        msg = new Message()
    }

    def 'Scenario 1 - Credential exists'() {
        given:
        msg.setProperty('ClientCredentialsSecureMaterialName', 'Sample-Client-Credentials')
        SecureStoreService secureStoreService = SecureStoreService.getInstance()
        secureStoreService.addCredential('Sample-Client-Credentials', new UserCredential('dummyId', 'dummySecret'))

        when:
        'Script is executed'
        script.processData(msg)

        then:
        def root = new JsonSlurper().parse(msg.getBody(Reader))
        verifyAll {
            root.clientId == 'dummyId'
            root.clientSecret == 'dummySecret'
        }
    }

    def 'Scenario 2 - Credential missing'() {
        given:
        msg.setProperty('ClientCredentialsSecureMaterialName', 'Missing-Client-Credentials')

        when:
        'Script is executed'
        script.processData(msg)

        then:
        def err = thrown(SecureStoreException)
        err.message == 'Could not fetch the credential for alias Missing-Client-Credentials'
    }
}

Once we execute the Spock specification (in IntelliJ IDEA in my case), we can confirm that the logic works as required.

Subsequently, the test can then be included in a CI/CD pipeline, for example using FlashPipe with GitHub Actions. The following screenshot provides an example of successful execution of the unit test as prerequisite before updating and deployment on a tenant.

Conclusion

As demonstrated previously, mocks provide a powerful yet convenient way of testing logic in Groovy scripts in a local development environment. The mock classes covered in this post extends this capability to cover access to security material in Groovy scripts.

From my experience, such requirements are not uncommon given the surge of solutions providing API access but with differing approaches to authentication.

While this post uses a simple example for the sake of illustration, the technique can be extended to handle a wide range of authentication requirements are not natively supported in SAP Cloud Integration.

References

The following links provide the full repository for the source code used in the post.


Comments

Feel free to provide your comment, feedback and/or opinion here.

About the Author