@cap-js-community/sap-afc-sdk
Version:
SAP Advanced Financial Closing SDK for CDS
734 lines (702 loc) • 22.9 kB
JavaScript
/* eslint-disable no-console */
;
const fs = require("fs");
const path = require("path");
const shelljs = require("shelljs");
const fetch = require("node-fetch");
const https = require("https");
const YAML = require("yaml");
const commander = require("commander");
const { open } = require("openurl");
const prompt = require("prompt-sync")();
const { adjustLines, generateHashBrokerPassword } = require("../common/util");
const PLAN_NAME = "standard";
const INTERNAL_SUFFIX = "internal";
const SERVICE_SUFFIX = "api";
const SERVICE_KEY_SUFFIX = "key";
const DEFAULT_VALIDITY = 365; // days
const PLACEHOLDERS = {
host: {
default: "<server host from cloud deployment>",
url: true,
},
api: {
default: "<api endpoint from cloud deployment>",
url: true,
},
auth: {
default: "<auth endpoint from cloud deployment>",
url: true,
},
clientId: {
default: "<xsuaa client id>",
variable: "cfServiceKeyClientId",
},
clientSecret: {
default: "<xsuaa client secret>",
variable: "cfServiceKeyClientSecret",
},
token: {
default: "<xsuaa token>",
variable: "oauthToken",
},
internalClientId: {
default: "<xsuaa internal client id>",
variable: "internalClientId",
},
internalClientSecret: {
default: "<xsuaa internal client secret>",
variable: "internalClientSecret",
},
internalToken: {
default: "<xsuaa intenral token>",
variable: "oauthToken",
},
};
module.exports = {
register: function (program) {
return program
.command("api")
.description("Manage API")
.addArgument(new commander.Argument("<action>", "Manage API keys").choices(["key"]))
.option("-n, --new [name]", "Create new API key with optional name")
.option("-l, --label <label>", "Label for instances")
.option("-s, --server <server-url>", "Server URL")
.option("-p, --password <password>", "Broker password")
.option("-a, --passcode", "Request temporary authentication code")
.option("-i, --internal", "Fetch internal credentials")
.option("-t, --token", "Fetch oauth token")
.option("-b, --bearer", "Generate authorization bearer header")
.option("-d, --destination", "Generate destination import file (json)")
.option("-o, --properties", "Generate destination import file (txt)")
.option("-h, --http", "Fill .http files")
.option("-e, --endpoint <endpoint>", "API endpoint path")
.option("-j, --jobscheduling", "Job Scheduling Provider API endpoint path")
.option("-c, --clear", "Clear .http files placeholders")
.option("-z, --certificate [days]", "x509 certificate (2048 bytes) days valid (default: 365)")
.option("-r, --reset", "Reset API management")
.option("-x, --xreset", "Reset API management")
.option("-g, --generate", "Generate broker password and hash")
.option("-v, --verbose", "Verbose log output")
.addHelpText(
"afterAll",
`
Actions:
key \t\t - manage service keys
Examples:
afc api key
afc api key --new
afc api key -n test
afc api key --server <server-url>
afc api key -s <server-url>
afc api key -h
afc api key -h -i
afc api key -h -c
afc api key -d -e <endpoint>
afc api key -d -j
`,
);
},
handle: async function (action) {
const options = this.opts();
if (options.properties) {
options.destination = true;
}
const success = await module.exports.process(action, options);
if (success) {
if (options.xreset || options.reset) {
console.log("Successfully reset API management.");
}
} else {
// eslint-disable-next-line n/no-process-exit
process.exit(-1);
}
},
process: async function (action, options) {
let success = true;
switch (action) {
case "key":
if (options.generate) {
success = manageBroker(options);
} else if (options.passcode) {
success = managePasscode(options);
} else if (options.reset || options.xreset) {
success = manageClear(options) && manageReset(options);
} else if (options.clear) {
success = manageClear(options);
} else if (options.internal) {
success = await manageInternal(options);
} else {
success = await manageKey(options);
}
break;
}
return success;
},
};
function manageBroker() {
const brokerPassword = generateHashBrokerPassword();
console.log(
`Keep it safe to create broker and to fetch key after deployment: afc api key -p '${brokerPassword.clear}'`,
);
return true;
}
function managePasscode(options) {
if (!cfLogin(options)) {
return false;
}
const config = fetchAppInfo(options);
if (!config.cf) {
console.log("Command only supported for CF apps");
return false;
}
if (!config.app) {
config.app = prompt("App name: ");
config.url ??= fetchServerUrl(config.app, options) || options.server;
}
const result = cfExec(`cf env ${config.app}`, options);
if (result === false) {
return false;
}
config.authUrl = /"xsuaa": \[.*"credentials": \{.*"url": "(.*?)"/s.exec(result)?.[1];
open(`${config.authUrl}/passcode`);
return true;
}
async function manageInternal(options) {
if (!cfLogin(options)) {
return false;
}
const config = fetchAppInfo(options);
if (!config.cf) {
console.log("Command only supported for CF apps");
return false;
}
if (!config.app) {
config.app = prompt("App name: ");
config.url ??= fetchServerUrl(config.app, options) || options.server;
}
const result = cfExec(`cf env ${config.app}`, options);
if (result === false) {
return false;
}
config.certUrl = /"xsuaa": \[.*"credentials": \{.*"certurl": "(.*?)"/s.exec(result)?.[1];
config.authUrl = config.certUrl || /"xsuaa": \[.*"credentials": \{.*"url": "(.*?)"/s.exec(result)?.[1];
config.auth = stripHTTPS(config.authUrl);
config.tokenUrl = `${config.authUrl}/oauth/token`;
config.internalClientId = /"xsuaa": \[.*"credentials": \{.*"clientid": "(.*?)"/s.exec(result)?.[1];
config.internalClientSecret = /"xsuaa": \[.*"credentials": \{.*"clientsecret": "(.*?)"/s.exec(result)?.[1];
config.internalCertificate = /"xsuaa": \[.*"credentials": \{.*"certificate": "(.*?)"/s.exec(result)?.[1];
config.internalKey = /"xsuaa": \[.*"credentials": \{.*"key": "(.*?)"/s.exec(result)?.[1];
if (!(config.auth && config.internalClientId)) {
console.log(`Failed to retrieve internal credentials`, { app: config.app });
return false;
}
if (!options.token && !options.bearer && !options.http && !options.destination) {
console.log(`url: ${config.tokenUrl}`);
console.log(`clientId (internal): ${config.internalClientId}`);
if (config.internalClientSecret) {
console.log(`clientSecret (internal): ${config.internalClientSecret}`);
}
if (config.internalCertificate) {
console.log(`certificate (internal): ${config.internalCertificate}`);
}
if (config.internalKey) {
console.log(`key (internal): ${config.internalKey}`);
}
}
if (options.destination) {
serverUrl(config);
const destinationName = `${config.service || config.app}${options.label ? `-${options.label}` : ""}-${INTERNAL_SUFFIX}-${SERVICE_SUFFIX}`;
createDestination(
destinationName,
config.url,
config.tokenUrl,
config.internalClientId,
config.internalClientSecret,
options,
);
}
if (options.token || options.bearer || options.http) {
config.internalToken = await fetchOAuthToken(
config.authUrl,
config.internalClientId,
config.internalClientSecret,
config.internalCertificate,
config.internalKey,
);
if (!config.internalToken) {
return false;
}
if (options.token) {
console.log(config.internalToken);
}
if (options.bearer) {
console.log(`Authorization: Bearer ${config.internalToken}`);
}
if (options.http) {
fillHTTPFiles(options, config);
}
}
return true;
}
async function manageKey(options) {
if (!cfLogin(options)) {
return false;
}
const service = cfService();
if (!service) {
return false;
}
const config = fetchAppInfo(options, service);
const broker = cfBroker(options, config);
if (!broker) {
return false;
}
config.broker = broker;
const serviceInstance = cfServiceInstance(options, config);
if (!serviceInstance) {
return false;
}
config.serviceInstance = serviceInstance;
const serviceKey = cfServiceKey(options, config);
if (!serviceKey) {
return false;
}
config.serviceKey = serviceKey;
if (!cfServiceCredentials(options, config)) {
return false;
}
if (!options.token && !options.bearer && !options.http && !options.destination) {
console.log(`name: ${config.serviceKey}`);
console.log(`api: ${config.api}`);
console.log(`auth: ${config.tokenUrl}`);
console.log(`clientId: ${config.clientId}`);
if (config.clientSecret) {
console.log(`clientSecret: ${config.clientSecret}`);
}
if (config.certificate) {
console.log(`certificate: ${config.certificate}`);
}
if (config.key) {
console.log(`key: ${config.key}`);
}
}
if (options.destination) {
const destinationName = `${config.service}${options.label ? `-${options.label}` : ""}-${SERVICE_SUFFIX}`;
createDestination(destinationName, config.api, config.tokenUrl, config.clientId, config.clientSecret, options);
}
if (options.token || options.bearer || options.http) {
config.token = await fetchOAuthToken(
config.authUrl,
config.clientId,
config.clientSecret,
config.certificate,
config.key,
);
if (!config.token) {
return false;
}
if (options.token) {
console.log(config.token);
}
if (options.bearer) {
console.log(config.api);
console.log(`Authorization: Bearer ${config.token}`);
}
if (options.http) {
fillHTTPFiles(options, config);
}
}
return true;
}
function manageClear(options) {
fillHTTPFiles(options, {}, true);
return true;
}
function manageReset(options) {
if (!cfLogin(options)) {
return false;
}
const service = cfService();
if (!service) {
return false;
}
const config = fetchAppInfo(options, service);
const broker = cfBroker(options, config, true);
if (!broker) {
return true;
}
let success = true;
config.broker = broker;
const serviceInstance = cfServiceInstance(options, config, true);
if (serviceInstance) {
config.serviceInstance = serviceInstance;
if (!cfDeleteServiceKeys(options, config)) {
success = false;
}
if (!cfDeleteService(options, config)) {
success = false;
}
}
if (!cfDeleteBroker(options, config)) {
success = false;
}
return success;
}
function fetchAppInfo(options, service) {
let serverUrl = options.server;
let app = service;
let cf = false;
const mtaPath = path.join(process.cwd(), "mta.yaml");
if (fs.existsSync(mtaPath)) {
cf = true;
const mta = fs.readFileSync(mtaPath, "utf8");
app = /- name: (.*)\n\s*type: nodejs\n\s*path: gen\/srv/s.exec(mta)?.[1];
if (!app) {
app = /- name: (.*)\n\s*type: java\n\s*path: srv/s.exec(mta)?.[1];
}
if (app) {
const appServerUrl = fetchServerUrl(app, options);
if (appServerUrl) {
serverUrl = appServerUrl;
}
}
}
serverUrl = ensureHttps(serverUrl);
return {
cf,
app,
service,
url: serverUrl,
};
}
function fetchServerUrl(app, options) {
const result = cfExec(`cf app ${app}`, options);
if (result === false) {
return false;
}
return /routes:\s*(.*)/.exec(result)?.[1];
}
function cfLogin(options) {
const result = cfExec(`cf target`, options);
if (result !== false && !result.trim().endsWith("FAILED")) {
return true;
}
console.log("Not logged in to Cloud Foundry. Call 'cf login' and try again.");
return false;
}
function cfService() {
const brokerPath = path.join(process.cwd(), "srv/broker.json");
if (fs.existsSync(brokerPath)) {
const broker = require(brokerPath);
const service = Object.keys(broker.SBF_SERVICE_CONFIG)[0];
if (service) {
return service;
}
}
const applicationPath = path.join(process.cwd(), "srv/src/main/resources/application.yaml");
if (fs.existsSync(applicationPath)) {
const content = fs.readFileSync(applicationPath, "utf8");
for (const yaml of YAML.parseAllDocuments(content)) {
const service = yaml?.getIn(["sap-afc-sdk", "broker", "name"]);
if (service) {
return service;
}
}
}
console.log(`No service found in broker configuration. Call 'afc add broker'`);
return "";
}
function cfBroker(options, config, optional) {
const brokerName = `${config.service}${options.label ? `-${options.label}` : ""}-broker`;
const regexBroker = new RegExp(`(${brokerName})\\s+https://`);
const cfBrokersCommand = `cf service-brokers`;
const cfCreateBrokerCommandLog = `cf create-service-broker ${brokerName} broker-user '***' ${config.url}/broker --space-scoped`;
let result = cfExec(cfBrokersCommand, options);
if (result === false) {
return false;
}
let cfBroker = regexBroker.exec(result)?.[1];
if (!cfBroker && !optional) {
serverUrl(config);
if (!options.password) {
options.password = prompt.hide("Broker password: ");
}
const cfCreateBrokerCommand = `cf create-service-broker ${brokerName} broker-user '${options.password}' ${config.url}/broker --space-scoped`;
result = cfExec(cfCreateBrokerCommand, options, cfCreateBrokerCommandLog);
if (result === false) {
return false;
}
result = cfExec(cfBrokersCommand, options);
if (result === false) {
return false;
}
cfBroker = regexBroker.exec(result)?.[1];
}
if (cfBroker) {
return brokerName;
}
if (!optional) {
console.log(`Failed to create service broker via command: '${cfCreateBrokerCommandLog}'`);
return false;
}
return "";
}
function cfServiceInstance(options, config, optional) {
const serviceInstanceName = `${config.service}${options.label ? `-${options.label}` : ""}-${SERVICE_SUFFIX}`;
const regexService = new RegExp(`${serviceInstanceName}.*${config.service}.*${PLAN_NAME}.*(${config.broker})`);
const cfServicesCommand = `cf services`;
const cfCreateServiceCommand = `cf create-service -b ${config.broker} ${config.service} ${PLAN_NAME} ${serviceInstanceName}`;
let result = cfExec(cfServicesCommand, options);
if (result === false) {
return false;
}
let cfService = regexService.exec(result)?.[1];
if (!cfService && !optional) {
result = cfExec(cfCreateServiceCommand, options);
if (result === false) {
return false;
}
result = cfExec(cfServicesCommand, options);
if (result === false) {
return false;
}
cfService = regexService.exec(result)?.[1];
}
if (cfService) {
return serviceInstanceName;
}
if (!optional) {
console.log(`Failed to create service via command: '${cfCreateServiceCommand}'`);
return false;
}
return "";
}
function cfServiceKey(options, config, optional) {
const serviceKeyName =
`${config.serviceInstance}-${SERVICE_KEY_SUFFIX}` +
(options.new ? `-${options.new === true ? Date.now() : options.new}` : "");
const regexServiceKey = new RegExp(`(${serviceKeyName})`);
const cfServiceKeysCommand = `cf service-keys ${config.serviceInstance}`;
let cfCreateServiceKeyCommand = `cf create-service-key ${config.serviceInstance} ${serviceKeyName}`;
if (options.certificate) {
const validity = options.certificate === true ? DEFAULT_VALIDITY : options.certificate;
const config = {
xsuaa: {
"credential-type": "x509",
x509: { "key-length": 2048, validity, "validity-type": "DAYS" },
},
};
cfCreateServiceKeyCommand += ` -c '${JSON.stringify(config)}'`;
}
let result = cfExec(cfServiceKeysCommand, options);
if (result === false) {
return false;
}
let cfServiceKey = regexServiceKey.exec(result)?.[1];
if (!cfServiceKey && !optional) {
result = cfExec(cfCreateServiceKeyCommand, options);
if (result === false) {
return false;
}
result = cfExec(cfServiceKeysCommand, options);
if (result === false) {
return false;
}
cfServiceKey = regexServiceKey.exec(result)?.[1];
}
if (cfServiceKey) {
return serviceKeyName;
}
if (!optional) {
console.log(`Failed to create service key via command: '${cfCreateServiceKeyCommand}'`);
return false;
}
}
function cfServiceCredentials(options, config) {
const cfServiceKeyCommand = `cf service-key ${config.serviceInstance} ${config.serviceKey}`;
const result = cfExec(cfServiceKeyCommand, options);
if (result === false) {
return false;
}
config.certUrl = /"certurl": "(.*?)"/.exec(result)?.[1];
config.authUrl = config.certUrl || /"url": "(.*?)"/.exec(result)?.[1];
config.auth = stripHTTPS(config.authUrl);
config.tokenUrl = `${config.authUrl}/oauth/token`;
config.clientId = /"clientid": "(.*?)"/.exec(result)?.[1];
config.clientSecret = /"clientsecret": "(.*?)"/.exec(result)?.[1];
config.certificate = /"certificate": "(.*?)"/.exec(result)?.[1];
config.key = /"key": "(.*?)"/.exec(result)?.[1];
config.api = /"api": "(.*?)"/.exec(result)?.[1];
if (options.endpoint) {
config.api = `${config.api}/${options.endpoint}}`;
} else if (options.jobscheduling) {
config.api = `${config.api}/job-scheduling/v1`;
}
if (config.auth && config.clientId) {
return true;
}
console.log(`Failed to retrieve service key via command: '${cfServiceKeyCommand}'`);
return false;
}
function cfDeleteServiceKeys(options, config) {
const serviceKeyPrefix = `${config.serviceInstance}-${SERVICE_KEY_SUFFIX}`;
const cfServiceKeysCommand = `cf service-keys ${config.serviceInstance}`;
let result = cfExec(cfServiceKeysCommand, options);
if (result === false) {
return false;
}
const lines = result.split("\n");
for (const line of lines) {
if (line.trim().startsWith(serviceKeyPrefix)) {
const serviceKey = line.trim().split(" ")[0];
const cfDeleteServiceKeyCommand = `cf delete-service-key -f ${config.serviceInstance} ${serviceKey}`;
result = cfExec(cfDeleteServiceKeyCommand, options);
if (result === false) {
return false;
}
}
}
return true;
}
function cfDeleteService(options, config) {
const cfDeleteServiceCommand = `cf delete-service -f ${config.serviceInstance}`;
const result = cfExec(cfDeleteServiceCommand, options);
return result !== false;
}
function cfDeleteBroker(options, config) {
const cfDeleteServiceCommand = `cf delete-service-broker -f ${config.broker}`;
const result = cfExec(cfDeleteServiceCommand, options);
return result !== false;
}
function fillHTTPFiles(options, config, clear) {
if (!clear) {
serverUrl(config);
config.host ??= stripHTTPS(config.url);
}
const httpPath = path.join(process.cwd(), "http");
const files = fs.readdirSync(httpPath, { recursive: true });
for (let file of files) {
if (file.endsWith(".cloud.http")) {
file = path.join("http", file);
if (
adjustLines(file, (line) => {
for (const name in PLACEHOLDERS) {
const placeholder = PLACEHOLDERS[name];
if (line.startsWith("#")) {
if (config[name] !== undefined) {
line = line.replace(`{{${name}}}`, config[name]);
}
}
if (line.startsWith(`@${name}=`)) {
if (options.clear) {
return `@${name}=${placeholder.default}`;
} else if (config[name] !== undefined) {
return `@${name}=${config[name]}`;
}
}
}
return line;
})
) {
console.log(`File '${file}' written.`);
}
}
}
}
function serverUrl(config) {
if (!config.url) {
config.url = prompt("Server URL: ");
config.url = ensureHttps(config.url);
}
}
function createDestination(name, serverUrl, authUrl, clientId, clientSecret, options) {
if (!clientSecret) {
console.log(`Destinations only supported for client secrets`);
}
const destination = {
destination: {
Name: name,
tokenServiceURLType: "Dedicated",
Type: "HTTP",
clientId: clientId,
Authentication: "OAuth2ClientCredentials",
clientSecret: clientSecret,
tokenServiceURL: authUrl,
ProxyType: "Internet",
URL: serverUrl,
},
};
if (options.properties) {
const destinationProperties = Object.keys(destination.destination).reduce((result, key) => {
return `${result}${key}=${destination.destination[key]}\n`;
}, "");
const destinationPath = path.join(process.cwd(), `default-${name}.properties`);
fs.writeFileSync(destinationPath, destinationProperties);
console.log(`File '${destinationPath}' written.`);
} else {
const destinationPath = path.join(process.cwd(), `default-${name}.json`);
fs.writeFileSync(destinationPath, JSON.stringify(destination, null, 2));
console.log(`File '${destinationPath}' written.`);
}
}
async function fetchOAuthToken(url, clientId, clientSecret, certificate, key) {
let result;
if (clientSecret) {
result = await fetch(`${url}/oauth/token?grant_type=client_credentials`, {
method: "POST",
headers: {
Accept: "application/json",
Authorization: `Basic ${btoa(clientId + ":" + clientSecret)}`,
},
});
} else if (certificate && key) {
certificate = certificate.split("\\n").join("\n");
key = key.split("\\n").join("\n");
const form = new URLSearchParams({
grant_type: "client_credentials",
client_id: clientId,
});
result = await fetch(`${url}/oauth/token`, {
method: "POST",
agent: new https.Agent({ key, cert: certificate }),
headers: {
"Content-type": "application/x-www-form-urlencoded",
},
body: form,
});
}
if (result?.ok) {
const data = await result.json();
if (data.access_token) {
return data.access_token;
}
} else {
console.log(await result.text());
}
console.log("Failed to fetch OAuth token");
}
function ensureHttps(url) {
if (url) {
return url.startsWith("https://") ? url : `https://${url}`;
}
return url;
}
function stripHTTPS(url) {
if (url?.startsWith("https://")) {
return url.substring(8);
}
return url;
}
function cfExec(command, options, log) {
const result = shelljs.exec(command, { silent: !options.verbose });
if (result.code !== 0) {
if (options.verbose) {
console.log(`"${log || command}", code ${result.code}: ${result.stderr}`);
} else {
console.log(result.stderr);
}
return false;
}
return result.stdout;
}