Technology Blogs by Members
Explore a vibrant mix of technical expertise, industry insights, and tech buzz in member blogs covering SAP products, technology, and events. Get in the mix!
cancel
Showing results for 
Search instead for 
Did you mean: 
pfefferf
Active Contributor
Recently I searched for an option to deploy UI5 applications to an ABAP On-Premise system from a Bitbucket Pipline running in Cloud. As (at time of writing this) Bitbucket does not provide self-hosted agents which can be excuted in the corporate network, able to connect to the Bitbucket Cloud infrastrcuture and to the internal ABAP system, I had to search for another way to reach my goal for the moment. In the future Bitbucket will provide the option for self-hosted Pipeline Runners which will make the following described approach obsolete (for further information please have a look to Bitbucket Pipelines Runners Early Access Program EAP (atlassian.com)). And if someone says now, hey you can expose your ABAP On-Premise system to the external network and do an IP filtering on the IPs of the Bitbucket Pipeline Cloud infrastructure (there is a list of specific IPs), I say: No, that is from a security point of view a no-go for productive scenarios as everyone which is able to use that shared Bitbucket Pipeline Runner infrastructure in the Cloud could potentially reach the exposed ABAP On-Premise system.

So, what is my approach to reach my goal? I thought I have a secure connection of my ABAP On-Premise system to SAP Business Technology Platform using SAP Cloud Connector already. Let's use this. But how can I make use of that connection from my Bitbucket environment? For that I need to expose an OAuth secured API which internally proxies my UI5 application deployment requests to the ABAP On-Premise system.

Overview


Following you can see a small overview of my approach and the included components and services:

  • In the Bitbucket Cloud Infrastructure I have the UI5 application repository. This repository uses the UI5 Tooling for building the application sources and the ui5-nwabap-deployer-cli tooling for doing the deployment stuff.

  • On SAP BTP a Cloud Foundry Node.js service is running which provides a /deploy endpoint. The implementation of the /deploy endpoint proxies the requests to the ABAP On-Premise system with the help of the SAP BTP Connectivity and the SAP BTP Destination Service. In my approach the credentials for the ABAP On-Premise system are stored in the destination pointing to that system. The consumer of the /deploy endpoint has to determine upfront an access token from the bound XSUAA service. The token is determined using client credentials provided by a service key created on the XSUAA service.



 

Proxy application running on SAP BTP Cloud Foundry


The proxy application is realized as an MTA application having one Node.js module and service definitions for the XSUAA, Connectivity and Destination service. All sources can be seen here: pfefferf / fpf-cf-sap-onpremise-proxy / packages / proxy — Bitbucket, therefore I will just describe the most interesting coding.

The MTA Deployment Descriptor:
_schema-version: '3.1'
ID: fpf-cf-sap-onpremise-proxy
description: On-Premise Proxy
version: 1.0.0
modules:
- name: fpf-cf-sap-onpremise-proxy-api
type: nodejs
path: proxy-api
parameters:
memory: 128M
disk-quota: 256M
requires:
- name: fpf-cf-sap-onpremise-proxy-uaa
- name: fpf-cf-sap-onpremise-proxy-conn
- name: fpf-cf-sap-onpremise-proxy-dest

resources:
- name: fpf-cf-sap-onpremise-proxy-uaa
type: org.cloudfoundry.managed-service
parameters:
service: xsuaa
service-plan: application
path: ./xs-security.json
- name: fpf-cf-sap-onpremise-proxy-conn
type: org.cloudfoundry.managed-service
parameters:
service: connectivity
service-plan: lite
- name: fpf-cf-sap-onpremise-proxy-dest
type: org.cloudfoundry.managed-service
parameters:
service: destination
service-plan: lite

The descriptor shows the Node.js module and the description of the used services bound to the application.

 

The server.js file:
const express = require("express");
const passport = require("passport");
const xssec = require("@sap/xssec");
const xsenv = require("@sap/xsenv");
const cfenv = require("cfenv");
const axios = require("axios");

const fetchToken = require("./lib/fetchToken");
const readDestination = require("./lib/readDestination");

const app = express();
const port = process.env.PORT || 3001;

passport.use("JWT", new xssec.JWTStrategy(xsenv.getServices({
uaa: {
tag: "xsuaa"
}
}).uaa));

app.use(passport.initialize());
app.use(passport.authenticate("JWT", { session: false }));

app.use(express.raw({ limit: "20MB", type: "*/*" }));

app.use("/info", (req, res) => {
res.json({
"description": "fpf-cf-sap-onpremise-proxy",
"version": "1.0.0"
});
});

app.use("/deploy/*", async (req, res) => {
try{
const urlParts = req.originalUrl.split("/");
urlParts.shift(); // remove empty element
urlParts.shift(); // remove deploy
const destinationName = urlParts.shift();
const targetUrl = "/" + urlParts.join("/");

const destCredentials = cfenv.getAppEnv().getService("fpf-cf-sap-onpremise-proxy-dest").credentials;
const connCredentials = cfenv.getAppEnv().getService("fpf-cf-sap-onpremise-proxy-conn").credentials;
const destToken = await fetchToken(destCredentials.url, destCredentials.clientid, destCredentials.clientsecret);
const destination = await readDestination(destinationName, destCredentials.uri, destToken);
const connToken = await fetchToken(connCredentials.url, connCredentials.clientid, connCredentials.clientsecret);

const reqOptions = {
method: req.method,
url: destination.destinationConfiguration.URL + targetUrl,
headers: {
"Proxy-Authorization": "Bearer " + connToken
},
proxy: {
host: connCredentials.onpremise_proxy_host,
port: connCredentials.onpremise_proxy_http_port
},
maxBodyLength: Infinity,
maxContentLength: Infinity
};

reqOptions.headers["authorization"] = `${destination.authTokens[0].type} ${destination.authTokens[0].value}`; // pre-condition: destination with credentials

["accept", "x-csrf-token", "content-type", "cookie", "accept-language", "if-match"].forEach((headerIdentifier) => {
if(req.headers[headerIdentifier]) {
reqOptions.headers[headerIdentifier] = req.headers[headerIdentifier];
}
});

if(req.method !== "GET" && req.method !== "DELETE" && req.body) {
reqOptions.data = Buffer.from(req.body).toString("utf-8");
}

reqOptions.validateStatus = (status) => {
return status < 999;
};

const resOnPrem = await axios(reqOptions);
res.status(resOnPrem.status).set(resOnPrem.headers).send(resOnPrem.data);
} catch (error) {
console.log(error);
res.status(500).send(error);
}
});

app.listen(port, () => {
console.log(`Proxy listening on port ${port}`);
});

The server.js file contains a simple implementation of an express server which uses passport middleware for token validation. The express server provides an /info endpoint which can be used to get some metadata of the running application.

But the main part is the /deploy endpoint which forwards the requests to the ABAP On-Premise system. Which ABAP On-Premise system should be connected, is part of the URL in form of the destinaton name. In case the deployment should be done to a system called A4H which is represnted by destination deva4h, the URL to be called by the deployer is /deploy/deva4h.

Internally the helper functions fetchToken and readDestination (see following code snippets) are used to fetch tokens to access the Connectivity and Destination service and to read information for a destination. From the Destination service the information is required which URL needs to be called to reach the destination endpoint and which credentials are necessary. From the Connectivity service the proxy URL, port and credentials are required which have to be used to reach the URL defined by the destination. Endpoints and further relevant information (like credentials) are taken from the VCAP_SERVICES environment variable.

Function to fetch token from an OAuth Endpoint (+ a simple token cache to avoid getting new tokens everytime the /deploy endpoint is called):
const axios = require("axios");

const tokenCache = [];

module.exports = async (oauthUrl, oauthClient, oauthSecret) => {
const tokenIdx = tokenCache.findIndex((cacheEntry) => {
return cacheEntry.oauthUrl === oauthUrl && cacheEntry.oauthClient === oauthClient;
})

if(tokenIdx !== -1) {
const cachedToken = tokenCache[tokenIdx];
if(Math.abs((new Date().getTime() - cachedToken.created.getTime()) / 1000) > 180) {
tokenCache.splice(tokenIdx, 1);
} else {
return cachedToken.token;
}
}

const tokenUrl = oauthUrl + "/oauth/token?grant_type=client_credentials&response_type=token";
const config = {
headers: {
Authorization: "Basic " + Buffer.from(oauthClient + ':' + oauthSecret).toString("base64")
}
};
const res = await axios.get(tokenUrl, config);
const access_token = res.data.access_token;

tokenCache.push({
token: access_token,
oauthUrl: oauthUrl,
oauthClient: oauthClient,
created: new Date()
});

return access_token;
}

Function to read destination information:
const axios = require("axios");

module.exports = async (destinationName, destUri, token) => {
const destSrvUrl =
destUri + "/destination-configuration/v1/destinations/" + destinationName;
const config = {
headers: {
Authorization: "Bearer " + token,
},
};
const res = await axios.get(destSrvUrl, config);
return res.data;
};

 

The application can be built using the Cloud MTA Build Tool (sap.github.io) and deployed using the Cloud Foundry tooling. Relevant scripts are included in the package.json on the root of the MTA application (check the repository above).

UI5 Deployment Configuration


In the UI5 application respository a deployment configuration file (e.g. .ui5deployrc), required for the ui5-nwabap-deployer-cli tooling, is created like following. The most important part is the setting of the server information. It needs to be set to the URL of the proxy application running on SAP BTP Cloud Foundry + /deploy/<name of destination>.
{
"cwd": "./dist",
"files": "**/*.*",
"server": "https://e58fa57dtrial-dev-fpf-cf-sap-onpremise-proxy-api.cfapps.eu10.hana.ondemand.com/deploy/deva4h",
"client": "001",
"language": "EN",
"package": "ZZ_UI5_REPOSITORY",
"bspContainer": "ZZ_UI5_TEST_BB",
"bspContainerText": "Test UI5 Deployment from Bitbucket",
"createTransport": true,
"transportText": "Test Transport Deployment from Bitbucket",
"transportUseLocked": true,
"calculateApplicationIndex": true
}

In the package.json of the UI5 application repository the ui5-nwabap-deployer-cli is defined as development dependency + the following NPM script is defined; the script is only synthetic sugar to avoid writing the command in the pipeline itself:
"scripts": {
"deploy": "ui5-deployer deploy --config .ui5deployrc"
}

Bitbucket Pipeline


The final piece is the Bitbucket Pipeline itself. The following example is restriced to show only the build and deployment step.
image: node:14

pipelines:
branches:
master:
# deployments steps
- step:
name: 'Deployment to ABAP development system'
deployment: abap_dev
script:
- apt-get update
- apt-get install -y jq
- cd packages/ui5-test-app
- npm ci
- cf_token=$(curl -X POST https://e58fa57dtrial.authentication.eu10.hana.ondemand.com/oauth/token -H 'application/x-www-form-urlencoded' -d "client_id=$cf_clientid" -d "client_secret=$cf_clientsecret" -d 'grant_type=client_credentials' | jq -r '.access_token')
- npm run build
- npm run deploy -- --bearerToken $cf_token

The pipeline uses as base a Node 14 image. For more efficiency a self-built image could be uses which comes out of the box with relevant command line toolings like e.g. jq which is installed manually by the pipeline each time it is executed.

What steps the pipeline do:

  • It installs the additional required tooling jq, to be able to easily extract the access token from the XSUAA OAuth token endpoint call (apt-get install -y jq).

  • It installs all npm dependencies (npm ci).

  • It determines the an access token from the XSUAA OAuth token endpoint using a CURL command. The access token is stored in a variable cf_token. The client id and client secret to access the OAuth endpoint are determined from a service key created for the XSUAA service bound the proxy application. The client id and secret are stored as secured variables in the Bitbucket pipeline.

  • The UI5 sources are build using the "npm run build" command which uses the UI5 Tooling.

  • The built UI5 sources are deployed using the command "npm run deploy". The NPM script gets the access token determined before to be able to call the proxy application endpoint.


In that demo use case the pipeline is triggered everything something is commited to the master branch.

If that happens the pipeline starts and if everything works fine the UI5 application is deployed from the Bitbucket Cloud Pipeline infrastructure via SCP BTP CF to the via SAP Cloud Connector connected ABAP On-Premise system.


 

Hope this was interesting for you. And keep in mind that this approach is obsolete as soon as the Bitbucket Pipeline Runners are ready for execution in the corporate network.

The demo application can be installed and used using SAP BTP Trial + Bitbucket free infrastructure.

 
5 Comments
Labels in this area