From 2dfa686bf5a1ad8d08c9316fb3f4d8ec76d612c8 Mon Sep 17 00:00:00 2001 From: "Steven R. Loomis" Date: Tue, 16 Apr 2019 14:33:30 -0700 Subject: [PATCH] iam: wip use API-KEY for interim iam support work in progress Fixes: https://github.com/IBM-Cloud/gp-js-client/issues/147 --- lib/client.js | 46 ++++++++++++----- lib/consts.js | 18 +++++++ lib/gp-iam.js | 123 ++++++++++++++++++++++++++++++++++++++++++++ lib/gpcli.js | 8 ++- lib/main.js | 7 ++- test/cli-test.js | 52 ++++++++++++++++--- test/lib/gp-test.js | 4 +- 7 files changed, 234 insertions(+), 24 deletions(-) create mode 100644 lib/gp-iam.js diff --git a/lib/client.js b/lib/client.js index 98ab2c9..fcc553b 100644 --- a/lib/client.js +++ b/lib/client.js @@ -27,6 +27,7 @@ var utils = require('./utils.js'); var SwaggerClient = require('swagger-client'); var GpHmac = require('./gp-hmac'); +var GpIAM = require('./gp-iam'); var cfEnvUtil = require('./cfenv-credsbylabel'); const TranslationRequest = require('./tr.js'); const DocumentTranslationRequest = require('./doctr.js'); @@ -54,11 +55,22 @@ class Client { } this._options = options; if (!this._options.credentials) { - throw new Error("g11n-pipeline: missing 'credentials' " + Object.keys(Consts.exampleCredentials)); + throw new Error("g11n-pipeline: missing 'credentials'"); // don't give specific contents + // could be CF or IAM. } - var missingField = utils.isMissingField(this._options.credentials, Object.keys(Consts.exampleCredentials)); - if (missingField.length !== 0) { - throw new Error("g11n-pipeline: missing credentials fields: \"" + missingField.join(' ') + "\" - expected: " + Consts.exampleCredentialsString); + if(this._options.credentials.apikey) { + const missingField = utils.isMissingField(this._options.credentials, Object.keys(Consts.exampleIamCredentials)); + if (missingField.length !== 0) { + throw new Error("g11n-pipeline: missing IAM credentials fields: \"" + missingField.join(' ') + + "\" - expected: " + Consts.exampleIamCredentialsString); + } + } else { + // expect 'GP auth' fields + const missingField = utils.isMissingField(this._options.credentials, Object.keys(Consts.exampleCredentials)); + if (missingField.length !== 0) { + throw new Error("g11n-pipeline: missing GP credentials fields: \"" + + missingField.join(' ') + "\" - expected: " + Consts.exampleCredentialsString + ' but got ' + JSON.stringify(this._options.credentials)); + } } // instanceId optional @@ -106,16 +118,24 @@ class Client { const schemaUrl = this._schemaUrl = this._options.credentials.url + '/swagger.json'; // if (debugREST) /*istanbul ignore next*/ console.log('.. fetching ' + schemaUrl); - const gphmac = new GpHmac("gp-hmac", this._options.credentials.userId, this._options.credentials.password); - if(this._options.basicAuth) { - // ignore basicAuth. - // throw Error('basicAuth is not supported'); // TODO: support this- maybe? + if (this._options.credentials.apikey) { + // IAM + const gpiam = new GpIAM(this._options.credentials); + const clientPromise = new SwaggerClient({ + url: schemaUrl, + requestInterceptor: (req) => gpiam.apply(req) + }); + return clientPromise; + } else { + // assume GP credentials + const gphmac = new GpHmac("gp-hmac", this._options.credentials.userId, this._options.credentials.password); + const clientPromise = new SwaggerClient({ + url: schemaUrl, + requestInterceptor: (req) => gphmac.apply(req) // TODO: change if we are using Basic + }); + return clientPromise; } - const clientPromise = new SwaggerClient({ - url: schemaUrl, - requestInterceptor: (req) => gphmac.apply(req) // TODO: change if we are using Basic - }); - return clientPromise; + // Not supported: this._options.basicAuth } diff --git a/lib/consts.js b/lib/consts.js index d6977c4..a23c548 100644 --- a/lib/consts.js +++ b/lib/consts.js @@ -35,12 +35,30 @@ exports.exampleCredentials = { password: "secretpassword", instanceId: "your Instance ID" }; + +/** + * Example IAM credentials such as for documentation. + * @property exampleUamCredentials + */ +exports.exampleIamCredentials = { + url: "Globalization Pipeline URL", + apikey: "your IAM apikey", // if we see apikey, we assume this is IAM + iam_endpoint: "your IAM endpoint URL", + instanceId: "your Instance ID" +}; + /** * Example credentials string * @property exampleCredentialsString */ exports.exampleCredentialsString = "credentials: " + JSON.stringify(exports.exampleCredentials); +/** + * Example IAM credentials string + * @property exampleIamCredentialsString + */ +exports.exampleIamCredentialsString = "credentials: " + JSON.stringify(exports.exampleIamCredentials); + /** * Current version */ diff --git a/lib/gp-iam.js b/lib/gp-iam.js new file mode 100644 index 0000000..fa327d5 --- /dev/null +++ b/lib/gp-iam.js @@ -0,0 +1,123 @@ +/* + * Copyright IBM Corp. 2015 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* eslint no-console: "off" */ + +// const crypto = require('crypto'); +// const url = require('url'); +/** + * @author Steven R. Loomis + * @ignore + */ + +const GpIAM = function GpIAM(credentials) { + this.credentials = credentials; + if(!this.credentials || !this.credentials.apikey || !this.credentials.iam_endpoint) { + throw new Error('GpIAM: params need to be "apikey, iam_endpoint"'); + } +}; + +GpIAM.prototype.name = null; +GpIAM.prototype.user = undefined; +GpIAM.prototype.secret = undefined; + +GpIAM.prototype.API_KEY = "API-KEY"; +// GpIAM.prototype.AUTH_SCHEME = "GP-HMAC"; +GpIAM.prototype.SEP = "\n"; +GpIAM.prototype.ENC = "ascii"; // ISO-8859-1 not supported! +GpIAM.prototype.HMAC_SHA1_ALGORITHM = 'sha1'; // "HmacSHA1"; +GpIAM.prototype.forceDate = null; +GpIAM.prototype.forceDateString = null; + +GpIAM.prototype.VERBOSE = process.env.GP_VERBOSE || false; + +// function rfc1123date(d) { +// return d.toUTCString(); +// } + +/** + * ( from GpIAM.java ) + * + * Generate GaaS HMAC credentials used for HTTP Authorization header. + * Gaas HMAC uses HMAC SHA1 algorithm signing a message composed by: + * + * (HTTP method): (in UPPERCASE) + * (Target URL): + * (RFC1123 date): + * (Request Body) + * + * If the request body is empty, it is simply omitted, + * the 'message' then ends with the separator ":" + * + * The format for HTTP Authorization header is: + * + * "Authorization: GaaS-HMAC (user ID):(HMAC above)" + * + * For example, with user "MyUser" and secret "MySecret", + * the URL "http://example.com/gaas", + * the method "https", + * the date "Mon, 30 Jun 2014 00:00:00 -0000", + * the body "param=value", + * the following text to be signed will be generated: + * + * GET:http://example.com/gaas:Mon, 30 Jun 2014 00:00:00 -0000:param=value + * + * And the resulting headers are: + * Authorization: GaaS-HMAC MyUser:Y4qPpmKpyYhdAKA7p3U/y4nNDvY= + * Date: Mon, 30 Jun 2014 00:00:00 -0000 + * + * The Date: header is required for HMAC. + */ +GpIAM.prototype.apply = function(obj) { + if(this.VERBOSE) console.dir(obj, {color: true, depth: null}); + if(obj.url.indexOf("/swagger.json") !== -1) return obj; // skip for swagger.json + + // const href = url.parse(obj.url).href; + // if(href !== obj.url) { + // if(this.VERBOSE) console.log('hmac: Warn: normalized', obj.url, '->', href); + // // For example, if there is a quote (') in the URL, it will turn into %27 + // // AFTER this interceptor is called (by Fetch). + // obj.url = href; + // } + + // var dateString = (this.forceDateString || + // rfc1123date(this.forceDate || new Date())); + // var bodyString = ""; + // if(obj.body) { + // if((typeof obj.body) === "string") { + // bodyString = obj.body; + // } else { + // bodyString = JSON.stringify(obj.body); // === what swagger does + // } + // } + // var hmacText = obj.method.toUpperCase() + this.SEP + + // obj.url + this.SEP + + // dateString + this.SEP + + // bodyString; + // if(this.VERBOSE) console.log('hmacText = <<' + hmacText + '>>'); + + // var hmacHash = crypto.createHmac(this.HMAC_SHA1_ALGORITHM, this.secretBuffer ) + // .update(hmacText, 'utf8') + // .digest('base64'); + // if(this.VERBOSE) console.log('hmacHash = ' + hmacHash ); + var hmacHeader = this.API_KEY + ' ' + this.credentials.apikey; + if(this.VERBOSE) console.log('hmacHeader = ' + hmacHeader); + obj.headers.Authorization = hmacHeader; + // obj.headers["GP-Date"] = dateString; + return obj; +}; + +module.exports = GpIAM; diff --git a/lib/gpcli.js b/lib/gpcli.js index cac2f8f..9e3f2bc 100644 --- a/lib/gpcli.js +++ b/lib/gpcli.js @@ -44,6 +44,8 @@ class Cli { instanceId: 'i', user: 'u', password: 'p', + apikey: 'a', + iam_endpoint: 'A', jsonCreds: 'j', bundle: 'b', outputFormat: 'F', @@ -138,12 +140,14 @@ class Cli { if(credentials.credentials) return credentials.credentials; return credentials; } else { - const {GP_URL, GP_INSTANCE_ID, GP_USER_ID, GP_PASSWORD} = process.env; + const {GP_URL, GP_INSTANCE_ID, GP_USER_ID, GP_PASSWORD, GP_IAM_API_KEY, GP_IAM_ENDPOINT} = process.env; const credentials = { url: this.argv.serviceUrl || GP_URL, userId: this.argv.user || GP_USER_ID, password: this.argv.password || GP_PASSWORD, - instanceId: this.argv.instanceId || GP_INSTANCE_ID + instanceId: this.argv.instanceId || GP_INSTANCE_ID, + apikey: this.argv.apikey || GP_IAM_API_KEY, + iam_endpoint: this.argv.iam_endpoint || GP_IAM_ENDPOINT }; // TODO: validate return credentials; diff --git a/lib/main.js b/lib/main.js index 2ad330f..47f2c7d 100644 --- a/lib/main.js +++ b/lib/main.js @@ -25,12 +25,15 @@ const utils = require('./utils.js'); /** * Construct a g11n-pipeline client. * params.credentials is required unless params.appEnv is supplied. + * Required either: (userId & password) or (apikey & iam_endpoint) * @param {Object} params - configuration params * @param {Object} params.appEnv - pass the result of cfEnv.getAppEnv(). Ignored if params.credentials is supplied. * @param {Object.} params.credentials - Bound credentials as from the CF service broker (overrides appEnv) * @param {string} params.credentials.url - service URL. (should end in '/translate') - * @param {string} params.credentials.userId - service API key. - * @param {string} params.credentials.password - service API key. + * @param {string} params.credentials.userId - GP auth userid. + * @param {string} params.credentials.password - GP auth password. + * @param {string} params.credentials.apikey - IAM apikey + * @param {string} params.credentials.iam_endpoint - IAM endpoint * @param {string} params.credentials.instanceId - instance ID * @returns {Client} * @function getClient diff --git a/test/cli-test.js b/test/cli-test.js index 8b1c762..7f1bcc6 100644 --- a/test/cli-test.js +++ b/test/cli-test.js @@ -246,7 +246,9 @@ describe('cli test', () => { serviceUrl: opts.credentials.url, instanceId: opts.credentials.instanceId, user: opts.credentials.userId, - password: opts.credentials.password + password: opts.credentials.password, + apikey: opts.credentials.apikey, + iam_endpoint: opts.credentials.iam_endpoint }).run(); expect(output).to.equal(true); @@ -257,7 +259,9 @@ describe('cli test', () => { serviceUrl: opts.credentials.url, instanceId: opts.credentials.instanceId, user: opts.credentials.userId, - password: opts.credentials.password + password: opts.credentials.password, + apikey: opts.credentials.apikey, + iam_endpoint: opts.credentials.iam_endpoint }).run(); expect(output).to.be.ok; @@ -268,7 +272,9 @@ describe('cli test', () => { serviceUrl: opts.credentials.url, instanceId: opts.credentials.instanceId, user: opts.credentials.userId, - password: opts.credentials.password + password: opts.credentials.password, + apikey: opts.credentials.apikey, + iam_endpoint: opts.credentials.iam_endpoint }).run(); expect(output).to.be.ok; @@ -279,7 +285,9 @@ describe('cli test', () => { serviceUrl: opts.credentials.url, instanceId: opts.credentials.instanceId, user: opts.credentials.userId, - password: opts.credentials.password + password: opts.credentials.password, + apikey: opts.credentials.apikey, + iam_endpoint: opts.credentials.iam_endpoint }).run(); expect(output).to.be.ok; @@ -291,6 +299,8 @@ describe('cli test', () => { instanceId: opts.credentials.instanceId, user: opts.credentials.userId, password: opts.credentials.password, + apikey: opts.credentials.apikey, + iam_endpoint: opts.credentials.iam_endpoint, bundle: 'mybundle', languages: 'en,mt,fr' }).run(); @@ -304,6 +314,8 @@ describe('cli test', () => { instanceId: opts.credentials.instanceId, user: opts.credentials.userId, password: opts.credentials.password, + apikey: opts.credentials.apikey, + iam_endpoint: opts.credentials.iam_endpoint, bundle: 'mybundle' }).run(); @@ -319,6 +331,8 @@ describe('cli test', () => { instanceId: opts.credentials.instanceId, user: opts.credentials.userId, password: opts.credentials.password, + apikey: opts.credentials.apikey, + iam_endpoint: opts.credentials.iam_endpoint, bundle: 'mybundle', languages: 'es,fr,mt' // add some target languages }).run(); @@ -332,6 +346,8 @@ describe('cli test', () => { instanceId: opts.credentials.instanceId, user: opts.credentials.userId, password: opts.credentials.password, + apikey: opts.credentials.apikey, + iam_endpoint: opts.credentials.iam_endpoint, bundle: 'mybundle', languages: 'en', file: 'test/data/t1_0_en.json' @@ -345,7 +361,9 @@ describe('cli test', () => { serviceUrl: opts.credentials.url, instanceId: opts.credentials.instanceId, user: opts.credentials.userId, - password: opts.credentials.password + password: opts.credentials.password, + apikey: opts.credentials.apikey, + iam_endpoint: opts.credentials.iam_endpoint }).run(); expect(output).to.be.ok; @@ -356,7 +374,9 @@ describe('cli test', () => { serviceUrl: opts.credentials.url, instanceId: opts.credentials.instanceId, user: opts.credentials.userId, - password: opts.credentials.password + password: opts.credentials.password, + apikey: opts.credentials.apikey, + iam_endpoint: opts.credentials.iam_endpoint }).run(); expect(output).to.be.ok; @@ -368,6 +388,8 @@ describe('cli test', () => { instanceId: opts.credentials.instanceId, user: opts.credentials.userId, password: opts.credentials.password, + apikey: opts.credentials.apikey, + iam_endpoint: opts.credentials.iam_endpoint, bundle: 'mybundle', languages: 'en' }).run(); @@ -380,6 +402,8 @@ describe('cli test', () => { instanceId: opts.credentials.instanceId, user: opts.credentials.userId, password: opts.credentials.password, + apikey: opts.credentials.apikey, + iam_endpoint: opts.credentials.iam_endpoint, bundle: 'mybundle', languages: 'en', flatten: true @@ -393,6 +417,8 @@ describe('cli test', () => { instanceId: opts.credentials.instanceId, user: opts.credentials.userId, password: opts.credentials.password, + apikey: opts.credentials.apikey, + iam_endpoint: opts.credentials.iam_endpoint, bundle: 'mybundle', languages: 'en', file: 'test/data/t1_0_en.json', @@ -408,6 +434,8 @@ describe('cli test', () => { instanceId: opts.credentials.instanceId, user: opts.credentials.userId, password: opts.credentials.password, + apikey: opts.credentials.apikey, + iam_endpoint: opts.credentials.iam_endpoint, bundle: 'mybundle', languages: 'en', flatten: true @@ -421,6 +449,8 @@ describe('cli test', () => { instanceId: opts.credentials.instanceId, user: opts.credentials.userId, password: opts.credentials.password, + apikey: opts.credentials.apikey, + iam_endpoint: opts.credentials.iam_endpoint, bundle: 'mybundle' }).run(); @@ -436,6 +466,8 @@ describe('cli flatten/unflatten test', () => { instanceId: opts.credentials.instanceId, user: opts.credentials.userId, password: opts.credentials.password, + apikey: opts.credentials.apikey, + iam_endpoint: opts.credentials.iam_endpoint, bundle: 'flattest', languages: 'en' }).run(); @@ -449,6 +481,8 @@ describe('cli flatten/unflatten test', () => { instanceId: opts.credentials.instanceId, user: opts.credentials.userId, password: opts.credentials.password, + apikey: opts.credentials.apikey, + iam_endpoint: opts.credentials.iam_endpoint, bundle: 'flattest', languages: 'en', file: 'test/data/flattest.json', @@ -464,6 +498,8 @@ describe('cli flatten/unflatten test', () => { instanceId: opts.credentials.instanceId, user: opts.credentials.userId, password: opts.credentials.password, + apikey: opts.credentials.apikey, + iam_endpoint: opts.credentials.iam_endpoint, bundle: 'flattest', languages: 'en' }).run(); @@ -476,6 +512,8 @@ describe('cli flatten/unflatten test', () => { instanceId: opts.credentials.instanceId, user: opts.credentials.userId, password: opts.credentials.password, + apikey: opts.credentials.apikey, + iam_endpoint: opts.credentials.iam_endpoint, bundle: 'flattest', languages: 'en', flatten: true @@ -489,6 +527,8 @@ describe('cli flatten/unflatten test', () => { instanceId: opts.credentials.instanceId, user: opts.credentials.userId, password: opts.credentials.password, + apikey: opts.credentials.apikey, + iam_endpoint: opts.credentials.iam_endpoint, bundle: 'flattest' }).run(); diff --git a/test/lib/gp-test.js b/test/lib/gp-test.js index 4c39992..98f522d 100644 --- a/test/lib/gp-test.js +++ b/test/lib/gp-test.js @@ -75,7 +75,9 @@ module.exports.getCredentials = function getCredentials() { instanceId: process.env.GP_INSTANCE_ID || process.env.GAAS_INSTANCE_ID || null /*admin*/, userId: process.env.GP_ADMIN_ID || process.env.GAAS_ADMIN_ID || process.env.GAAS_USER_ID || null, password: process.env.GP_ADMIN_PASSWORD || process.env.GAAS_ADMIN_PASSWORD || process.env.GAAS_PASSWORD || null, - isAdmin: ((process.env.GP_ADMIN_ID || process.env.GAAS_ADMIN_ID) !== null) + isAdmin: ((process.env.GP_ADMIN_ID || process.env.GAAS_ADMIN_ID) !== null), + apikey: process.env.GP_IAM_API_KEY, + iam_endpoint: process.env.GP_IAM_ENDPOINT }; } if(VERBOSE) console.dir(creds);