@sap-ux/btp-utils
Version:
Library to simplify working with SAP BTP specific features especially in SAP Business Application
257 lines • 12.4 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.BAS_DEST_INSTANCE_CRED_HEADER = void 0;
exports.isAppStudio = isAppStudio;
exports.getAppStudioProxyURL = getAppStudioProxyURL;
exports.getAppStudioBaseURL = getAppStudioBaseURL;
exports.getCredentialsForDestinationService = getCredentialsForDestinationService;
exports.getDestinationUrlForAppStudio = getDestinationUrlForAppStudio;
exports.listDestinations = listDestinations;
exports.exposePort = exposePort;
exports.generateABAPCloudDestinationName = generateABAPCloudDestinationName;
exports.createOAuth2UserTokenExchangeDest = createOAuth2UserTokenExchangeDest;
const bas_sdk_1 = require("@sap/bas-sdk");
const cf_tools_1 = require("@sap/cf-tools");
const axios_1 = __importDefault(require("axios"));
const app_studio_env_1 = require("./app-studio.env");
const destination_1 = require("./destination");
/**
* ABAP Cloud destination instance name.
*/
const DESTINATION_INSTANCE_NAME = 'abap-cloud-destination-instance';
/**
* HTTP header that is to be used for encoded credentials when communicating with a destination service instance.
*/
exports.BAS_DEST_INSTANCE_CRED_HEADER = 'bas-destination-instance-cred';
/**
* Check if this is executed in SAP Business Application Studio.
*
* @returns true if yes
*/
function isAppStudio() {
return !!process.env[app_studio_env_1.ENV.H2O_URL];
}
/**
* Read and return the BAS proxy url.
*
* @returns the proxy url or undefined if called outside of BAS.
*/
function getAppStudioProxyURL() {
return process.env[app_studio_env_1.ENV.PROXY_URL];
}
/**
* Read and return the BAS base url.
*
* @returns the base url or undefined if called outside of BAS.
*/
function getAppStudioBaseURL() {
return process.env[app_studio_env_1.ENV.H2O_URL];
}
/**
* Asynchronously creates a base64 encoded credentials for the given destination service instance based on the client information fetched from BTP.
*
* @param instance name/id of the destination service instance
* @returns the base64 encoded user
*/
async function getCredentialsForDestinationService(instance) {
try {
const serviceInfo = await (0, cf_tools_1.cfGetInstanceKeyParameters)(instance);
if (!serviceInfo) {
throw new Error(`No destination instance ${instance} found`);
}
const serviceCredentials = serviceInfo.credentials;
if (!serviceCredentials) {
throw new Error(`No credentials for destination instance ${instance} found`);
}
const clientId = serviceCredentials.uaa?.clientid || serviceCredentials.clientid;
const clientSecret = serviceCredentials.uaa?.clientsecret || serviceCredentials.clientsecret;
return Buffer.from(`${encodeURIComponent(clientId)}:${encodeURIComponent(clientSecret)}`).toString('base64');
}
catch (error) {
throw new Error(`An error occurred while retrieving service key for the destination instance ${instance}: ${error}`);
}
}
/**
* Returns a url for AppStudio for the given url with the given destination.
*
* @param name name of the destination
* @param path optional path
* @returns destination url working in BAS
*/
function getDestinationUrlForAppStudio(name, path) {
const origin = `https://${name}.dest`;
return path && path.length > 1 ? new URL(path, origin).toString() : origin;
}
/**
* Helper function to strip `-api` from the host name.
*
* @param host -
* @returns an updated string value, with `-api` removed
*/
function stripS4HCApiHost(host) {
const [first, ...rest] = host.split('.');
return [first.replace(/-api$/, ''), ...rest].join('.');
}
/**
* Get a list of available destinations in SAP Business Application Studio.
*
* @param options - options for the destinations
* @returns the list of destinations
*/
async function listDestinations(options) {
const destinations = {};
await axios_1.default.get('/reload', { baseURL: process.env[app_studio_env_1.ENV.PROXY_URL] });
const response = await axios_1.default.get('/api/listDestinations', { baseURL: process.env[app_studio_env_1.ENV.H2O_URL] });
const list = Array.isArray(response.data) ? response.data : [];
list.forEach((destination) => {
if (options?.stripS4HCApiHosts && (0, destination_1.isS4HC)(destination)) {
destination.Host = stripS4HCApiHost(destination.Host);
}
if (destination.WebIDEEnabled) {
destinations[destination.Name] = destination;
}
});
return destinations;
}
/**
* Exposes port in SAP Business Application Studio.
*
* @param port Port that needs to be exposed
* @param logger Logger
* @returns url on which the port is exposed
*/
async function exposePort(port, logger) {
try {
const response = await axios_1.default.get(`http://localhost:3001/AppStudio/api/getHostByPort?port=${port}`);
return `${response.data.result}`;
}
catch (error) {
logger?.error(`Port ${port} was not exposed!`);
return '';
}
}
/**
* Transform a destination object into a TokenExchangeDestination destination, appended with UAA properties.
*
* @param destinationName name of the destination to be created
* @param destinationDescription description of the destination to be created
* @param credentials object representing the Client ID and Client Secret and token endpoint {@link ServiceInfo['uaa']}
* @param hostUrl The full resolved cloud host instance url e.g. https://123bd07a-eda5-50bc-b11a-b1a7b88b632b.abap.somewhereaws.hanavlab.ondemand.com
* @returns Populated OAuth destination
*/
function transformToBASSDKDestination(destinationName, destinationDescription, credentials, hostUrl) {
const BASProperties = {
usage: 'odata_abap,dev_abap,abap_cloud',
html5DynamicDestination: 'true',
html5Timeout: '60000'
};
const oauth2UserTokenExchange = {
clientId: credentials.clientid,
clientSecret: credentials.clientsecret,
tokenServiceURL: new URL('/oauth/token', credentials.url).toString(),
tokenServiceURLType: destination_1.OAuthUrlType.DEDICATED
};
return {
name: destinationName,
description: destinationDescription,
url: new URL(hostUrl),
type: destination_1.DestinationType.HTTP,
proxyType: destination_1.DestinationProxyType.INTERNET,
basProperties: BASProperties,
credentials: {
authentication: destination_1.Authentication.OAUTH2_USER_TOKEN_EXCHANGE,
oauth2UserTokenExchange
}
};
}
/**
* Generate a destination name representing the CF target the user is logged into i.e. abap-cloud-mydestination-myorg-mydevspace.
*
* @param name destination name
* @returns formatted destination name using target space and target organisation
*/
async function generateABAPCloudDestinationName(name) {
const target = await (0, cf_tools_1.cfGetTarget)(true);
if (!target.space) {
throw new Error(`No Dev Space has been created for the subaccount.`);
}
const formattedInstanceName = `${name}-${target.org}-${target.space}`.replace(/\W/gi, '-').toLowerCase();
return `abap-cloud-${formattedInstanceName}`.substring(0, 199);
}
/**
* Generate a new object representing an OAuth2 token exchange BTP destination.
*
* @param serviceInstanceName the service instance name on which the new destination will be based
* @param uaaCredentials the credentials associated to the specified service instance {@link ServiceInfo['uaa']}
* @param hostUrl the full resolved cloud host instance url e.g. https://123bd07a-eda5-50bc-b11a-b1a7b88b632b.abap.somewhereaws.hanavlab.ondemand.com
* @param logger Logger
* @returns Preconfigured OAuth destination
*/
async function generateOAuth2UserTokenExchangeDestination(serviceInstanceName, uaaCredentials, hostUrl, logger) {
const generatedDestinationName = await generateABAPCloudDestinationName(serviceInstanceName);
const instances = await (0, cf_tools_1.apiGetServicesInstancesFilteredByType)(['destination']);
const destinationInstance = instances.find((instance) => instance.label === DESTINATION_INSTANCE_NAME);
if (!destinationInstance) {
// Create a new abap-cloud destination instance on the target CF subaccount
await (0, cf_tools_1.apiCreateServiceInstance)('destination', 'lite', DESTINATION_INSTANCE_NAME, null);
logger?.info(`New ABAP destination instance ${DESTINATION_INSTANCE_NAME} created.`);
}
return transformToBASSDKDestination(generatedDestinationName, `Destination generated by App Studio for Cloud Foundry Abap service instance: '${serviceInstanceName}', Do not remove.`, uaaCredentials, hostUrl);
}
/**
* Creates a new SAP BTP subaccount destination of type 'OAuth2UserTokenExchange' using the specified `serviceInstanceName`
* param to generate a new destination name with the format: `abap-cloud-<serviceInstanceName>-<cfOrg>-<cfDevSpace>`.
* If the destination already exists, missing properties will be appended, existing fields are not updated with newer values.
* For example: If an existing SAP BTP destination already contains `WebIDEEnabled` and the value is set as `false`,
* the value will remain `false` even after the update. The specified `serviceInstanceInfo` parameter will be used as part of the destination name.
* If the param `uaaCredentials` is not provided, the function will attempt to retrieve the UAA properties from the specified service instance.
*
* Exceptions: an exception will be thrown if the user is not logged into Cloud Foundry,
* ensure you are logged in using: `cf login -a https://<cf-api-endpoint-path> -o <cfOrg> -s <cfSpace>`
*
* @param serviceInstanceName The name of the service instance (returned in {@link ServiceInstanceInfo} as `label`, not the `serviceName` (technical name)),
* for example, as returned by the CF tools API: apiGetServicesInstancesFilteredByType {@link ServiceInstanceInfo}.
* @param serviceCredentials
* @param serviceCredentials.uaaCredentials object representing the Client ID and Client Secret and token endpoint {@link ServiceInfo['uaa']}
* @param serviceCredentials.hostUrl The full resolved cloud host instance url e.g. https://123bd07a-eda5-50bc-b11a-b1a7b88b632b.abap.somewhereaws.hanavlab.ondemand.com
* Note: This is not necessarily the same as the credentials uaa url, which is used as the host part of the tokenServiceURL.
* @param logger Logger
* @returns the newly generated SAP BTP destination
* @throws an error if the destination could not be created
*/
async function createOAuth2UserTokenExchangeDest(serviceInstanceName, serviceCredentials, logger) {
if (!isAppStudio()) {
throw new Error(`Creating a SAP BTP destinations is only supported on SAP Business Application Studio.`);
}
let serviceCredInfo = serviceCredentials;
try {
if (!serviceCredInfo) {
const instanceDetails = await (0, cf_tools_1.apiGetInstanceCredentials)(serviceInstanceName);
if (!instanceDetails?.credentials) {
throw new Error(`Could not retrieve SAP BTP credentials for ${serviceInstanceName}.`);
}
serviceCredInfo = {
uaaCredentials: instanceDetails.credentials.uaa,
hostUrl: instanceDetails.credentials.url
};
}
const basSDKDestination = await generateOAuth2UserTokenExchangeDestination(serviceInstanceName, serviceCredInfo.uaaCredentials, serviceCredInfo.hostUrl, logger);
// Destination is created on SAP BTP but nothing is returned to validate this!
await bas_sdk_1.destinations.createDestination(basSDKDestination);
logger?.debug(`SAP BTP destination ${JSON.stringify(basSDKDestination, null, 2)} created.`);
// Return updated destination from SAP BTP
const destinations = await listDestinations();
const newDestination = destinations?.[basSDKDestination.name];
if (!newDestination) {
throw new Error('Destination not found on SAP BTP.');
}
return newDestination;
}
catch (error) {
throw new Error(`An error occurred while generating destination ${serviceInstanceName}: ${error}`);
}
}
//# sourceMappingURL=app-studio.js.map