From 6d357d3f4d2bb93c4865806e66c37d3ec5164349 Mon Sep 17 00:00:00 2001 From: "Steven R. Loomis" Date: Tue, 16 Apr 2019 14:33:30 -0700 Subject: [PATCH] iam: Phase I: use API-KEY for interim iam support - iam_endpoint and apikey required - new CLI options and variables - test updates - docs Fixes: https://github.com/IBM-Cloud/gp-js-client/issues/147 --- API.md | 40 +++++++++++++++++++++++++++++++--- CLI.md | 39 +++++++++++++++++++++++++++++----- lib/client.js | 46 +++++++++++++++++++++++++++------------ lib/consts.js | 18 ++++++++++++++++ lib/gp-iam.js | 52 +++++++++++++++++++++++++++++++++++++++++++++ lib/gpcli.js | 8 +++++-- lib/main.js | 7 ++++-- test/cli-test.js | 52 +++++++++++++++++++++++++++++++++++++++------ test/lib/gp-test.js | 4 +++- 9 files changed, 234 insertions(+), 32 deletions(-) create mode 100644 lib/gp-iam.js diff --git a/API.md b/API.md index c3bafa4..ee9da14 100644 --- a/API.md +++ b/API.md @@ -99,9 +99,15 @@ Usage: var credentials = require('cfEnv')
exampleCredentials

Example credentials such as for documentation.

+
exampleIamCredentials
+

Example IAM credentials such as for documentation.

+
exampleCredentialsString

Example credentials string

+
exampleIamCredentialsString
+

Example IAM credentials string

+
version

Current version

@@ -112,7 +118,8 @@ Usage: var credentials = require('cfEnv')
getClient(params)Client

Construct a g11n-pipeline client. -params.credentials is required unless params.appEnv is supplied.

+params.credentials is required unless params.appEnv is supplied. +Required either: (userId & password) or (apikey & iam_endpoint)

readJson(filename)Promise.<Object>

Read a file, return promise to parsed obj

@@ -1352,6 +1359,18 @@ Example credentials such as for documentation. | --- | | exampleCredentials | + + +## exampleIamCredentials +Example IAM credentials such as for documentation. + +**Kind**: global variable +**Properties** + +| Name | +| --- | +| exampleUamCredentials | + ## exampleCredentialsString @@ -1364,6 +1383,18 @@ Example credentials string | --- | | exampleCredentialsString | + + +## exampleIamCredentialsString +Example IAM credentials string + +**Kind**: global variable +**Properties** + +| Name | +| --- | +| exampleIamCredentialsString | + ## version @@ -1417,6 +1448,7 @@ Possible translation domains. These provide hints as to the type of translation ## getClient(params) ⇒ [Client](#Client) Construct a g11n-pipeline client. params.credentials is required unless params.appEnv is supplied. +Required either: (userId & password) or (apikey & iam_endpoint) **Kind**: global function @@ -1426,8 +1458,10 @@ params.credentials is required unless params.appEnv is supplied. | params.appEnv | Object | pass the result of cfEnv.getAppEnv(). Ignored if params.credentials is supplied. | | params.credentials | Object.<string, string> | Bound credentials as from the CF service broker (overrides appEnv) | | params.credentials.url | string | service URL. (should end in '/translate') | -| params.credentials.userId | string | service API key. | -| params.credentials.password | string | service API key. | +| params.credentials.userId | string | GP auth userid. | +| params.credentials.password | string | GP auth password. | +| params.credentials.apikey | string | IAM apikey | +| params.credentials.iam_endpoint | string | IAM endpoint | | params.credentials.instanceId | string | instance ID | diff --git a/CLI.md b/CLI.md index 8277dc8..749f685 100644 --- a/CLI.md +++ b/CLI.md @@ -35,17 +35,26 @@ Options have a short or a long form. Therefore, the following are all equivalent Credentials -- +See also the [`getClient()` API docs](./API.md#getClient) Credentials may be passed in one of the following ways: 1. Via the `-j/--jsonCreds` option, which takes a path to a JSON file with credentials -2. Via the four individual options `--serviceUrl`, `--instanceId`, `--user`, and `--password` +2a. For GP Auth: the four individual options `--serviceUrl`, `--instanceId`, `--user`, and `--password` +2b. for IAM auth via `--iam_endpoint` and `--apikey` 3. By the following environment variables: * __GP_URL__: Service URL (e.g. https://gp-rest.ng.bluemix.net/translate/rest) * __GP_INSTANCE_ID__: Service instance ID (e.g. d3f537cd617f34c86ac6b270f3065e73) - * __GP_USER_ID__: User ID (e.g. e92a1282a0e4f97bec93aa9f56fdb838) - * __GP_PASSWORD__: User password (e.g. zg5SlD+ftXYRIZDblLgEA/ILkkCNqE1y) + - _if using GP Authentication_: + + * __GP_USER_ID__: User ID (e.g. e92a1282a0e4f97bec93aa9f56fdb838) + * __GP_PASSWORD__: User password (e.g. zg5SlD+ftXYRIZDblLgEA/ILkkCNqE1y) + + - _if using IAM Authentication_: + + * __GP_IAM_API_KEY__: IAM API Key + * __GP_IAM_ENDPOINT__: IAM endpoint (e.g. https://iam.cloud.ibm.com) Common Options -- @@ -54,7 +63,7 @@ Common Options See [Credentials](#Credentials), above. - This option specifies a credentials file containing the [GP credentials](https://github.com/IBM-Cloud/gp-common/blob/master/README.md#4-credentials). This is a JSON file with either of the following formats: + This option specifies a credentials file containing the [GP credentials](https://github.com/IBM-Cloud/gp-common/blob/master/README.md#4-credentials). This is a JSON file with any of the following formats: ```json {"url":"≈", @@ -70,11 +79,31 @@ Common Options "userId":"≈", "password":"≈"}} ``` + + ```json + {"url":"≈", + "instanceId":"≈", + "iam_endpoint":"≈", + "apikey":"≈"} + ``` + + ```json + {"credentials": + {"url":"≈", + "instanceId":"≈", + "iam_endpoint":"≈", + "apikey":"≈"}} + ``` - `--serviceUrl`, `--instanceId`, `--user`, `--password` See [Credentials](#Credentials), above. - These options specify the four credential parameters individually. + These options specify the four credential parameters individually for GP Auth. + +- `--iam_endpoint`, `--apikey` + + See [Credentials](#Credentials), above. + These options specify the two credential parameters individually for IAM Auth. - `-F json` | `--outputFormat=json` 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..02a8bd9 --- /dev/null +++ b/lib/gp-iam.js @@ -0,0 +1,52 @@ +/* + * Copyright IBM Corp. 2015-2019 + * + * 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" */ + +/** + * Manage use of IAM API Keys and Tokens + * Docs: https://cloud.ibm.com/docs/iam?topic=iam-iamtoken_from_apikey&locale=en#iamtoken_from_apikey + * + * @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.API_KEY = "API-KEY"; // GP SPECIFIC header + +GpIAM.prototype.VERBOSE = process.env.GP_VERBOSE || false; +GpIAM.prototype.GP_USE_APIKEY = process.env.GP_USE_APIKEY || true; // if false: use token manager + +/** + * Generate HTTP Authorization header. + */ +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 authHeader = this.API_KEY + ' ' + this.credentials.apikey; + if(this.VERBOSE) console.log('hmacHeader = ' + authHeader); + obj.headers.Authorization = authHeader; + 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);