Friday 9 August 2019

SAP HANA XSA Controller API Interaction

Since the introduction of SAP HANA extended application services, advanced model (XSA), SAP has supported many new approaches to accessing the platform APIs.  XSA is based upon Cloud Foundry which provides its core functionality via a command line client and a REST enabled web API.  XSA has this same functionality, but most of the focus on platform admin and interaction tends to focus on the CLI interface or the browser-based administrative UI which SAP deliveries. However behind that web UI are essentially calls to the controller APIs. These same APIs can be called from and used in your own applications.  In this blog I’d like to share some experiences trying to use these APIs.  There are some common challenges anyone using them will face and perhaps this blog can be helpful in that regard.

Documentation


One of the first questions about these APIs is probably going to be where to go to get documentation.  Your first inclination might be to go to the SAP HANA Platform documentation.

However you won’t find much in the way of details about the XSA controller REST APIs. This is where falling back to underlying Cloud Foundry concepts is necessary. The XSA controller APIs are essentially the same as their standard Cloud Foundry counterparts. Of course this is very helpful if writing an application that will also run on SAP Cloud Platform where standard Cloud Foundry is used as well.  Therefore much of the documentation you might want about these APIs can be found on the Cloud Foundry site:

http://apidocs.cloudfoundry.org/1.27.0/

Let’s take an example. You might want to list all the users in XSA. There is a Cloud Foundry API for this (/v2/users):

https://apidocs.cloudfoundry.org/1.27.0/users/list_all_users.html

If we come over to XSA and write a Node.js application to call this same API on the XSA Controller, we get the same kind of results:

let request = require("request");
var options = {
method: "GET",
json: true,
url: global.__controller + "/v2/users",
auth: {}
};

SAP HANA XSA, SAP HANA Tutorial and Materials, SAP HANA Certifications, SAP HANA Online Exam

Which incidentally would be the same kind of results you could get by calling the xs users or cf users CLI command.

Controller URL


If you notice in the above example, I’m building a URL that points to the controller API plus the API REST path. But the next challenge we face is; how to get this URL. Of course we could hard code the URL, but that’s not very elegant as we would have to change the code for each system we deploy to. We could setup a user provided service to configure the URL. This would be a good solution, but there is something easier.

The XS/CF deployer has certain built-in variables which are filled automatically. These can be accessed in the mta.yaml file using the ${<variable name>} approach. If you’ve done much XSA or CF based development, you’ve probably used the most common of these variables – ${default-url}.

This common example of default-url is how we can take the generated URL of one micro service and pass it dynamically to another.  Its key to how the application router/web module can wrap inner Node.js and Java services:

 - name: node
   type: nodejs
   path: node
   provides:
    - name: node_api
      properties:
         url: ${default-url}
   requires:
    - name: controller-api-ex-uaa
    - name: controller-config
      group: destinations
      properties:
        name: controller-config
        url: ~{url}
        forwardAuthToken: true

But default-url is hardly the only variable we have available. Now this is where things get interesting.  We have variables controller-url and authorization-url that can give us access to the controller and UAA APIs.  So you can setup a resource with this property getting filled via the controller-url variable:

resources:
 - name: controller-api-ex-uaa
   type: com.sap.xs.uaa
   parameters:
     config-path: ./xs-security.json   
 - name: controller-config
   properties:
     url: ${controller-url}

The only problem: this doesn’t work when running the application from the SAP Web IDE.  The run command from the Web IDE uses a special approach (in order to provide debugging and delta run support). However this means its doesn’t have all the functionality of the full deployer.  So this does mean that for debugging and development in the Web IDE, you need to temporarily hard-code the controller url and only use this controller-url variable for “real” deployments.

Now that we got the messiness out of the way, we can see that we’ve added the controller URL to a resource and we’ve bound that resource to a Node.js module. This can then be easily accessed from coding because bound resources are detailed in the environment of the bound application.  From Node.js the environment is read from process.env variable. So at the bootstrap of the Node.js application, I just tuck away this value in a global variable:

global.__controller = JSON.parse(process.env.destinations)[0].url;

If you were ever curious about this environment information, it contains all kinds of good stuff. For example it can be used to retrieve the XSA/CF Organization at runtime

let VCAP = JSON.parse(process.env.VCAP_APPLICATION);
res.type("application/json").status(200).send(JSON.stringify(VCAP.organization_name));

Or the same for the XSA/CF Space:

let VCAP = JSON.parse(process.env.VCAP_APPLICATION);
res.type("application/json").status(200).send(JSON.stringify(VCAP.space_name));

This VCAP_APPLICATION and VCAP_SERVICES sections are particularly chalk full of good information.

/v2/info


Before we go further, let’s talk about a special controller api: /v2/info

This is the only one of the Controller APIs which doesn’t require authentication. Therefore once you have access to the base controller URL, this is often the first API you want to call. It returns lots of runtime information about the system including URLs to other important APIs and applications. This is a great entry point into the rest of the system.

app.get("/info", (req, res) => {
let request = require("request");
let options = {
url: global.__controller + "/v2/info"
};
request.get(
options,
function(error, response, body) {
if (error) {
console.log(error.toString());
res.type("text/html").status(200).send(error.toString());
return;
}
res.type("application/json").status(200).send(body);
}
);
});

SAP HANA XSA, SAP HANA Tutorial and Materials, SAP HANA Certifications, SAP HANA Online Exam

Authentication


Now that we have access to the controller API URL and we’ve called the un-authenticated /v2/info API, the final challenge we face is how to make authenticated requests to the remainder of the APIs. In all likelihood we probably want to use principal propagation – meaning whatever user is already authentication to our application services is the same one that we want to forward onto the controller APIs. This means the authenticated user for our application probably needs controller admin or developer rights in order to call most of these APIs.

This means that our application which wants to call the controller APIs needs to have a typical setup for UAA and Application Router.  There is nothing especially specific about this setup for the controller APIs, so I won’t repeat all that setup here. Its been described in other tutorials and documents already. However at the end you should have your Node.js and Web/App Router module and the redirect with xsuaa authentication between them:

SAP HANA XSA, SAP HANA Tutorial and Materials, SAP HANA Certifications, SAP HANA Online Exam

Where this gets interesting is in the Node.js module and how we re-use this authentication token for the outbound requests to the controller APIs.  We start by wiring the UAA service into Express via passport.  This will allow Express via middelware to do all the heavy lifting of the processing the authentication token:

//Initialize Express App for XS UAA and HDBEXT Middleware
var app = express();

passport.use("JWT", new xssec.JWTStrategy(xsenv.getServices({
uaa: {
tag: "xsuaa"
}
}).uaa));
app.use(logging.expressMiddleware(appContext));
app.use(passport.initialize());
app.use(
passport.authenticate("JWT", {
session: false
})
);

The beauty of this approach is that Express does the processing and then places the authorization information into the req object which it passes into all route handlers.  For the controller APIs we need to extract the Bearer authorization details out of this information:

module.exports = {
getAccessToken: function(req) {
var accessToken = null;
if (req.headers.authorization && req.headers.authorization.split(" ")[0] === "Bearer") {
   accessToken =  req.headers.authorization.split(" ")[1];
}
return accessToken;
}
};

Now whenever we want to call any of the controller APIs with authentication, extract this bearer information from the incoming request object and then insert it into the outbound one as shown below:

app.get("/getOrgs", (req, res) => {
let request = require("request");
var options = {
method: "GET",
json: true,
url: global.__controller + "/v2/organizations",
auth: {}
};
options.auth.bearer = require(global.__base + "utils/auth").getAccessToken(req);
request.get(
options,
function(error, response, body) {
if (error) {
console.log(error.toString());
res.type("text/html").status(200).send(error.toString());
return;
}
res.type("application/json").status(200).send(body);
}
);
});

No comments:

Post a Comment