From 030f00bfcc2572410176af681fe51fe602977ac5 Mon Sep 17 00:00:00 2001 From: Felix Meschberger Date: Tue, 6 Jul 2021 15:31:48 +0200 Subject: [PATCH 1/8] Re-add support for token login plugins * For now only in the CLI context not in the StateAction context * Fully lazily load plugins on use * Additional plugins are "labeled" after their location from where they are required as plugins are configured as an array rather than a map --- src/context.js | 8 +++-- src/ctx/ConfigCliContext.js | 34 +++++++++++++++++++++ src/ctx/Context.js | 45 +++++++++++++++++++++++++++ src/token-helper.js | 61 ++++++++++++++++++++++++++++--------- 4 files changed, 131 insertions(+), 17 deletions(-) diff --git a/src/context.js b/src/context.js index 3e061bd..d9f0754 100644 --- a/src/context.js +++ b/src/context.js @@ -35,6 +35,9 @@ const CLI = 'cli' /** Property holding the current context name */ const CURRENT = 'current' +/** Property holding the list of additional plugins */ +const PLUGINS = 'plugins' + /** @private */ function guessContextType () { if (process.env.__OW_ACTION_NAME) { @@ -52,7 +55,7 @@ function getContext () { if (guessContextType() === TYPE_ACTION) { context = new ActionContext({ IMS, CONTEXTS, CONFIG, CURRENT }) } else { - context = new CliContext({ IMS, CONTEXTS, CONFIG, CURRENT, CLI }) + context = new CliContext({ IMS, CONTEXTS, CONFIG, CURRENT, CLI, PLUGINS }) } } return context @@ -72,5 +75,6 @@ module.exports = { CURRENT, CLI, CONTEXTS, - CONFIG + CONFIG, + PLUGINS } diff --git a/src/ctx/ConfigCliContext.js b/src/ctx/ConfigCliContext.js index 2610ec6..ad0cc54 100644 --- a/src/ctx/ConfigCliContext.js +++ b/src/ctx/ConfigCliContext.js @@ -56,6 +56,31 @@ class ConfigCliContext extends Context { this.setContextValue(`${this.keyNames.CLI}`, { ...existingData, ...contextData }, local) } + /** + * Override super class implementation to prevent return the + * plugins if configured at all + * + * @override + */ + async getPlugins () { + aioLogger.debug('getPlugins()') + return this.getConfigValue(this.keyNames.PLUGINS) || [] + } + + /** + * Override super class implementation to prevent return the + * plugins if configured at all + * + * @override + */ + async setPlugins (plugins, local = false) { + aioLogger.debug('setPlugins(%o, %s)', plugins, !!local) + + if (plugins instanceof Array || plugins === null) { + this.setConfigValue(this.keyNames.PLUGINS, plugins, !!local) + } + } + /** * @protected * @override @@ -106,6 +131,15 @@ class ConfigCliContext extends Context { return Object.keys(this.aioConfig.get(`${this.keyNames.IMS}.${this.keyNames.CONTEXTS}`) || {}) } + /** + * @protected + * @override + * @ignore + */ + async plugins () { + return this.getConfigValue(this.keyNames.PLUGINS) + } + /** @private */ getContextValueFromOptionalSource (key, source) { const fullKey = `${this.keyNames.IMS}.${this.keyNames.CONTEXTS}.${key}` diff --git a/src/ctx/Context.js b/src/ctx/Context.js index 69d3c9b..64b7e48 100644 --- a/src/ctx/Context.js +++ b/src/ctx/Context.js @@ -113,6 +113,42 @@ class Context { return this.contextKeys() } + /** + * Gets the list of configured token creation plugins. This base + * implementation returns an empty array. Extensions supporting + * token creation plugins must override to return the list of + * plugins which can be require-d. + * + * @returns {Promise} the token creation plugins + */ + async getPlugins () { + aioLogger.debug('getPlugins() (none)') + return [] + } + + /** + * Sets the list of configured token creation plugins. This base + * implementation throws an Error as plugins are not support. + * Extensions supporting token creation plugins must override to + * return check and persist the list of plugins which can be + * require-d. + * + * If the plugins parameter is an empty array or null, the current + * plugins configuration is removed. Otherwise the current + * configuration is replaced by the new list of plugins. + * + * Note, that implementations are only required to validate that + * the plugins parameter is a possibly empty array of strings or + * null. Actually provided string values need not be validated. + * + * @param {string[]} plugins An array of plugins to configure + * @param {boolean} [local=false] set to true to save to local config, false for global config + */ + async setPlugins (plugins, local = false) { + aioLogger.debug('setPlugins(%o, %b)', plugins, !!local) + throwNotImplemented() + } + /* To be implemented */ /** @@ -166,6 +202,15 @@ class Context { async contextKeys () { throwNotImplemented() } + + /** + * @ignore + * @protected + * @returns {Promise} return plugins, empty by default + */ + async plugins () { + return [] + } } /** @private */ diff --git a/src/token-helper.js b/src/token-helper.js index d636246..9f23094 100644 --- a/src/token-helper.js +++ b/src/token-helper.js @@ -13,7 +13,7 @@ governing permissions and limitations under the License. const { Ims, ACCESS_TOKEN, REFRESH_TOKEN } = require('./ims') const aioLogger = require('@adobe/aio-lib-core-logging')('@adobe/aio-lib-ims:token-helper', { provider: 'debug' }) const { getContext } = require('./context') -const imsJwtPlugin = require('@adobe/aio-lib-ims-jwt') +const imsJwtPlugin = '@adobe/aio-lib-ims-jwt' /** * This is the default list of NPM packages used as plugins to create tokens @@ -31,8 +31,8 @@ by aio-lib-runtime */ const ACTION_BUILD = (typeof WEBPACK_ACTION_BUILD === 'undefined') ? false : WEBPACK_ACTION_BUILD if (!ACTION_BUILD) { // use OAuth and CLI imports only when WEBPACK_ACTION_BUILD global is not set - const imsCliPlugin = require('@adobe/aio-lib-ims-oauth/src/ims-cli') - const imsOAuthPlugin = require('@adobe/aio-lib-ims-oauth') + const imsCliPlugin = '@adobe/aio-lib-ims-oauth/src/ims-cli' + const imsOAuthPlugin = '@adobe/aio-lib-ims-oauth' DEFAULT_CREATE_TOKEN_PLUGINS = { cli: imsCliPlugin, @@ -41,9 +41,39 @@ if (!ACTION_BUILD) { } } -const IMS_TOKEN_MANAGER = { +async function getMergedPlugins (context) { + aioLogger.debug("getMergedPlugins(%o)", context); + + return context.getPlugins() + .then((plugins) => { + if (plugins instanceof Array && plugins.length > 0) { + aioLogger.debug(" > adding configured plugins: %o", plugins) + const configPluginMap = Object.fromEntries(plugins.map(element => [element, element])) + return Object.assign(configPluginMap, DEFAULT_CREATE_TOKEN_PLUGINS) + } + + return DEFAULT_CREATE_TOKEN_PLUGINS + } + ) +} - async getToken (contextName) { +function loadPlugin (name, location) { + aioLogger.debug("loadPlugin(%s, %s)", name, location) + + try { + return require(location) + } catch (error) { + aioLogger.debug("Ignoring plugin %s due to load failure from %s", name, location) + aioLogger.debug("Error: %o", error) + return { + supports: () => false, + canSupport: async () => Promise.reject(new Error(`Plugin not loaded: ${JSON.stringify(error)}`)) + } + } +} + +const IMS_TOKEN_MANAGER = { + async getToken(contextName) { aioLogger.debug('getToken(%s, %s)', contextName) return this._resolveContext(contextName) @@ -51,7 +81,7 @@ const IMS_TOKEN_MANAGER = { .then(result => this._persistTokens(result.name, result.data, result.result)) }, - async invalidateToken (contextName, force) { + async invalidateToken(contextName, force) { aioLogger.debug('invalidateToken(%s, %s)', contextName, force) const tokenLabel = force ? REFRESH_TOKEN : ACCESS_TOKEN @@ -75,11 +105,11 @@ const IMS_TOKEN_MANAGER = { }) }, - get _context () { + get _context() { return getContext() }, - async _resolveContext (contextName) { + async _resolveContext(contextName) { const context = await this._context.get(contextName) aioLogger.debug('LoginCommand:contextData - %O', context) @@ -90,7 +120,7 @@ const IMS_TOKEN_MANAGER = { } }, - async _getOrCreateToken (config) { + async _getOrCreateToken(config) { aioLogger.debug('_getOrCreateToken(config=%o)', config) const ims = new Ims(config.env) return this.getTokenIfValid(config.access_token) @@ -98,22 +128,23 @@ const IMS_TOKEN_MANAGER = { .catch(reason => this._generateToken(ims, config, reason)) }, - async _fromRefreshToken (ims, token, config) { + async _fromRefreshToken(ims, token, config) { aioLogger.debug('_fromRefreshToken(token=%s, config=%o)', token, config) return this.getTokenIfValid(token) .then(refreshToken => ims.getAccessToken(refreshToken, config.client_id, config.client_secret, config.scope)) }, - async _generateToken (ims, config, reason) { + async _generateToken(ims, config, reason) { aioLogger.debug('_generateToken(reason=%s)', reason) - const imsLoginPlugins = DEFAULT_CREATE_TOKEN_PLUGINS + const imsLoginPlugins = await getMergedPlugins(this._context); + aioLogger.debug(" > Got imsLoginPlugins: %o", imsLoginPlugins); let pluginErrors = ['Cannot generate token because no plugin supports configuration:'] // eslint-disable-line prefer-const for (const name of Object.keys(imsLoginPlugins)) { aioLogger.debug(' > Trying: %s', name) try { - const { canSupport, supports, imsLogin } = imsLoginPlugins[name] + const { canSupport, supports, imsLogin } = await loadPlugin(name, imsLoginPlugins[name]); aioLogger.debug(' > supports(%o): %s', config, supports(config)) if (typeof supports === 'function' && supports(config) && typeof imsLogin === 'function') { const result = imsLogin(ims, config) @@ -148,7 +179,7 @@ const IMS_TOKEN_MANAGER = { * @param {Promise} resultPromise the promise that contains the results (access token, or access token and refresh token) * @returns {Promise} resolves to the access token */ - async _persistTokens (context, contextData, resultPromise) { + async _persistTokens(context, contextData, resultPromise) { aioLogger.debug('persistTokens(%s, %o, %o)', context, contextData, resultPromise) const result = await resultPromise @@ -180,7 +211,7 @@ const IMS_TOKEN_MANAGER = { * * @returns {Promise} the token if existing and not expired, else a rejected Promise */ - async getTokenIfValid (token) { + async getTokenIfValid(token) { aioLogger.debug('getTokenIfValid(token=%o)', token) const minExpiry = Date.now() + 10 * 60 * 1000 // 10 minutes from now if (token && typeof (token.expiry) === 'number' && token.expiry > minExpiry && typeof (token.token) === 'string') { From 969c7795146b7b576c06e0489a8c238989c54def Mon Sep 17 00:00:00 2001 From: Felix Meschberger Date: Tue, 6 Jul 2021 18:01:22 +0200 Subject: [PATCH 2/8] Remove superfluous function plugins() --- src/ctx/ConfigCliContext.js | 9 --------- src/ctx/Context.js | 9 --------- 2 files changed, 18 deletions(-) diff --git a/src/ctx/ConfigCliContext.js b/src/ctx/ConfigCliContext.js index ad0cc54..f5d9e01 100644 --- a/src/ctx/ConfigCliContext.js +++ b/src/ctx/ConfigCliContext.js @@ -131,15 +131,6 @@ class ConfigCliContext extends Context { return Object.keys(this.aioConfig.get(`${this.keyNames.IMS}.${this.keyNames.CONTEXTS}`) || {}) } - /** - * @protected - * @override - * @ignore - */ - async plugins () { - return this.getConfigValue(this.keyNames.PLUGINS) - } - /** @private */ getContextValueFromOptionalSource (key, source) { const fullKey = `${this.keyNames.IMS}.${this.keyNames.CONTEXTS}.${key}` diff --git a/src/ctx/Context.js b/src/ctx/Context.js index 64b7e48..5933946 100644 --- a/src/ctx/Context.js +++ b/src/ctx/Context.js @@ -202,15 +202,6 @@ class Context { async contextKeys () { throwNotImplemented() } - - /** - * @ignore - * @protected - * @returns {Promise} return plugins, empty by default - */ - async plugins () { - return [] - } } /** @private */ From 951d6449e47bed5ad4394a71173d3edbd5464379 Mon Sep 17 00:00:00 2001 From: Felix Meschberger Date: Tue, 6 Jul 2021 18:04:12 +0200 Subject: [PATCH 3/8] Remove " || []" as this never triggers: the return value is always as promise ... --- src/ctx/ConfigCliContext.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ctx/ConfigCliContext.js b/src/ctx/ConfigCliContext.js index f5d9e01..d799e00 100644 --- a/src/ctx/ConfigCliContext.js +++ b/src/ctx/ConfigCliContext.js @@ -64,7 +64,7 @@ class ConfigCliContext extends Context { */ async getPlugins () { aioLogger.debug('getPlugins()') - return this.getConfigValue(this.keyNames.PLUGINS) || [] + return this.getConfigValue(this.keyNames.PLUGINS) } /** From 833f98dc1613c97adc214615d32ab22bb1e478c3 Mon Sep 17 00:00:00 2001 From: Felix Meschberger Date: Tue, 6 Jul 2021 18:05:14 +0200 Subject: [PATCH 4/8] Some more logging to retrieving plugins --- src/token-helper.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/token-helper.js b/src/token-helper.js index 9f23094..6b54e09 100644 --- a/src/token-helper.js +++ b/src/token-helper.js @@ -50,8 +50,11 @@ async function getMergedPlugins (context) { aioLogger.debug(" > adding configured plugins: %o", plugins) const configPluginMap = Object.fromEntries(plugins.map(element => [element, element])) return Object.assign(configPluginMap, DEFAULT_CREATE_TOKEN_PLUGINS) + } else if (plugins !== undefined) { + aioLogger.debug('Ignored configured plugins: Expected string[], got: \'%o\'', plugins) } + aioLogger.debug(' > using default plugins only') return DEFAULT_CREATE_TOKEN_PLUGINS } ) From cae23c47d6be4d97747cae884bb5ccce65086c0a Mon Sep 17 00:00:00 2001 From: Felix Meschberger Date: Tue, 6 Jul 2021 18:05:27 +0200 Subject: [PATCH 5/8] eslint fixes ... --- src/token-helper.js | 52 +++++++++++++++++++++++++++++---------------- 1 file changed, 34 insertions(+), 18 deletions(-) diff --git a/src/token-helper.js b/src/token-helper.js index 6b54e09..82f40ee 100644 --- a/src/token-helper.js +++ b/src/token-helper.js @@ -41,13 +41,19 @@ if (!ACTION_BUILD) { } } +/** + * Returns a consolidated list of login plugins to try for acquiring the token. + * + * @param {object} context The configuration context providing additional plugins + * @returns {Promise} The list of login plugins to try + */ async function getMergedPlugins (context) { - aioLogger.debug("getMergedPlugins(%o)", context); + aioLogger.debug('getMergedPlugins(%o)', context) return context.getPlugins() .then((plugins) => { if (plugins instanceof Array && plugins.length > 0) { - aioLogger.debug(" > adding configured plugins: %o", plugins) + aioLogger.debug(' > adding configured plugins: %o', plugins) const configPluginMap = Object.fromEntries(plugins.map(element => [element, element])) return Object.assign(configPluginMap, DEFAULT_CREATE_TOKEN_PLUGINS) } else if (plugins !== undefined) { @@ -57,17 +63,27 @@ async function getMergedPlugins (context) { aioLogger.debug(' > using default plugins only') return DEFAULT_CREATE_TOKEN_PLUGINS } - ) + ) } +/** + * Loads the requested plugin and returns it or a dummy pluggin in case + * of a load failure. The dummy plugin returns "false" for the supports() + * function and will reject the canSupport() function regardless of supplied + * parameters. + * + * @param {string} name The name of the plugin to try to load + * @param {string} location The location from where to load the plugin + * @returns {object} The loaded plugin or a dummy in case of failure to load + */ function loadPlugin (name, location) { - aioLogger.debug("loadPlugin(%s, %s)", name, location) + aioLogger.debug('loadPlugin(%s, %s)', name, location) try { return require(location) } catch (error) { - aioLogger.debug("Ignoring plugin %s due to load failure from %s", name, location) - aioLogger.debug("Error: %o", error) + aioLogger.debug('Ignoring plugin %s due to load failure from %s', name, location) + aioLogger.debug('Error: %o', error) return { supports: () => false, canSupport: async () => Promise.reject(new Error(`Plugin not loaded: ${JSON.stringify(error)}`)) @@ -76,7 +92,7 @@ function loadPlugin (name, location) { } const IMS_TOKEN_MANAGER = { - async getToken(contextName) { + async getToken (contextName) { aioLogger.debug('getToken(%s, %s)', contextName) return this._resolveContext(contextName) @@ -84,7 +100,7 @@ const IMS_TOKEN_MANAGER = { .then(result => this._persistTokens(result.name, result.data, result.result)) }, - async invalidateToken(contextName, force) { + async invalidateToken (contextName, force) { aioLogger.debug('invalidateToken(%s, %s)', contextName, force) const tokenLabel = force ? REFRESH_TOKEN : ACCESS_TOKEN @@ -108,11 +124,11 @@ const IMS_TOKEN_MANAGER = { }) }, - get _context() { + get _context () { return getContext() }, - async _resolveContext(contextName) { + async _resolveContext (contextName) { const context = await this._context.get(contextName) aioLogger.debug('LoginCommand:contextData - %O', context) @@ -123,7 +139,7 @@ const IMS_TOKEN_MANAGER = { } }, - async _getOrCreateToken(config) { + async _getOrCreateToken (config) { aioLogger.debug('_getOrCreateToken(config=%o)', config) const ims = new Ims(config.env) return this.getTokenIfValid(config.access_token) @@ -131,23 +147,23 @@ const IMS_TOKEN_MANAGER = { .catch(reason => this._generateToken(ims, config, reason)) }, - async _fromRefreshToken(ims, token, config) { + async _fromRefreshToken (ims, token, config) { aioLogger.debug('_fromRefreshToken(token=%s, config=%o)', token, config) return this.getTokenIfValid(token) .then(refreshToken => ims.getAccessToken(refreshToken, config.client_id, config.client_secret, config.scope)) }, - async _generateToken(ims, config, reason) { + async _generateToken (ims, config, reason) { aioLogger.debug('_generateToken(reason=%s)', reason) - const imsLoginPlugins = await getMergedPlugins(this._context); - aioLogger.debug(" > Got imsLoginPlugins: %o", imsLoginPlugins); + const imsLoginPlugins = await getMergedPlugins(this._context) + aioLogger.debug(' > Got imsLoginPlugins: %o', imsLoginPlugins) let pluginErrors = ['Cannot generate token because no plugin supports configuration:'] // eslint-disable-line prefer-const for (const name of Object.keys(imsLoginPlugins)) { aioLogger.debug(' > Trying: %s', name) try { - const { canSupport, supports, imsLogin } = await loadPlugin(name, imsLoginPlugins[name]); + const { canSupport, supports, imsLogin } = await loadPlugin(name, imsLoginPlugins[name]) aioLogger.debug(' > supports(%o): %s', config, supports(config)) if (typeof supports === 'function' && supports(config) && typeof imsLogin === 'function') { const result = imsLogin(ims, config) @@ -182,7 +198,7 @@ const IMS_TOKEN_MANAGER = { * @param {Promise} resultPromise the promise that contains the results (access token, or access token and refresh token) * @returns {Promise} resolves to the access token */ - async _persistTokens(context, contextData, resultPromise) { + async _persistTokens (context, contextData, resultPromise) { aioLogger.debug('persistTokens(%s, %o, %o)', context, contextData, resultPromise) const result = await resultPromise @@ -214,7 +230,7 @@ const IMS_TOKEN_MANAGER = { * * @returns {Promise} the token if existing and not expired, else a rejected Promise */ - async getTokenIfValid(token) { + async getTokenIfValid (token) { aioLogger.debug('getTokenIfValid(token=%o)', token) const minExpiry = Date.now() + 10 * 60 * 1000 // 10 minutes from now if (token && typeof (token.expiry) === 'number' && token.expiry > minExpiry && typeof (token.token) === 'string') { From aed8cf13e0e86a01035fce73457f60a5ff33d497 Mon Sep 17 00:00:00 2001 From: Felix Meschberger Date: Tue, 6 Jul 2021 18:06:46 +0200 Subject: [PATCH 6/8] token-helper/getMergedPlugins uses Object.fromEntries which was only added in NodeJS 12 .. requiring NodeJS 12 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 908f551..cd16b77 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "stdout-stderr": "^0.1.9" }, "engines": { - "node": ">=10.0.0" + "node": ">=12.0.0" }, "files": [ "/src" From d747c1897ff56a798784c0228b3432e19e531c7d Mon Sep 17 00:00:00 2001 From: Felix Meschberger Date: Tue, 6 Jul 2021 18:06:58 +0200 Subject: [PATCH 7/8] Testing configurable plugins --- test/ctx/ConfigCliContext.test.js | 57 ++++++++++++++++++++++++++++++- test/ctx/Context.test.js | 10 ++++++ test/token-helper.test.js | 14 ++++++-- 3 files changed, 77 insertions(+), 4 deletions(-) diff --git a/test/ctx/ConfigCliContext.test.js b/test/ctx/ConfigCliContext.test.js index 0f53e3c..62dfdbb 100644 --- a/test/ctx/ConfigCliContext.test.js +++ b/test/ctx/ConfigCliContext.test.js @@ -20,7 +20,8 @@ const keyNames = { CONFIG: 'b', CONTEXTS: 'c', CURRENT: 'd', - CLI: 'd' + CLI: 'd', + PLUGINS: 'e' } let context @@ -127,3 +128,57 @@ describe('contextKeys', () => { expect(aioConfig.get).toHaveBeenCalledWith(`${keyNames.IMS}.${keyNames.CONTEXTS}`) }) }) + +describe('getPlugins', () => { + test('(), plugins=undefined', async () => { + context.getConfigValue = jest.fn().mockResolvedValue(undefined) + const ret = await context.getPlugins() + expect(ret).toEqual(undefined) + expect(context.getConfigValue).toHaveBeenCalledWith(keyNames.PLUGINS) + }) + test('(), plugins=[]', async () => { + context.getConfigValue = jest.fn().mockResolvedValue([]) + const ret = await context.getPlugins() + expect(ret).toEqual([]) + expect(context.getConfigValue).toHaveBeenCalledWith(keyNames.PLUGINS) + }) + test('(), plugins=45', async () => { + context.getConfigValue = jest.fn().mockResolvedValue(45) + const ret = await context.getPlugins() + expect(ret).toEqual(45) + expect(context.getConfigValue).toHaveBeenCalledWith(keyNames.PLUGINS) + }) + test('(), plugins=[\'@adobe/internal_plugin\']', async () => { + context.getConfigValue = jest.fn().mockResolvedValue(['@adobe/internal_plugin']) + const ret = await context.getPlugins() + expect(ret).toEqual(['@adobe/internal_plugin']) + expect(context.getConfigValue).toHaveBeenCalledWith(keyNames.PLUGINS) + }) +}) + +describe('setPlugins', () => { + test('(undefined, false)', async () => { + await expect(context.setPlugins()).resolves.toEqual(undefined) + expect(aioConfig.set).toHaveBeenCalledTimes(0) + }) + test('([], false)', async () => { + await expect(context.setPlugins([])).resolves.toEqual(undefined) + expect(aioConfig.set).toHaveBeenCalledWith(`${keyNames.IMS}.${keyNames.CONFIG}.${keyNames.PLUGINS}`, [], false) + }) + test('([\'@adobe/internal_plugin\'], false)', async () => { + await expect(context.setPlugins(['@adobe/internal_plugin'])).resolves.toEqual(undefined) + expect(aioConfig.set).toHaveBeenCalledWith(`${keyNames.IMS}.${keyNames.CONFIG}.${keyNames.PLUGINS}`, ['@adobe/internal_plugin'], false) + }) + test('(undefined, true)', async () => { + await expect(context.setPlugins(undefined, true)).resolves.toEqual(undefined) + expect(aioConfig.set).toHaveBeenCalledTimes(0) + }) + test('([], true)', async () => { + await expect(context.setPlugins([], true)).resolves.toEqual(undefined) + expect(aioConfig.set).toHaveBeenCalledWith(`${keyNames.IMS}.${keyNames.CONFIG}.${keyNames.PLUGINS}`, [], true) + }) + test('([\'@adobe/internal_plugin\'], true)', async () => { + await expect(context.setPlugins(['@adobe/internal_plugin'], true)).resolves.toEqual(undefined) + expect(aioConfig.set).toHaveBeenCalledWith(`${keyNames.IMS}.${keyNames.CONFIG}.${keyNames.PLUGINS}`, ['@adobe/internal_plugin'], true) + }) +}) diff --git a/test/ctx/Context.test.js b/test/ctx/Context.test.js index 315241b..769a40a 100644 --- a/test/ctx/Context.test.js +++ b/test/ctx/Context.test.js @@ -47,6 +47,9 @@ describe('not implemented methods', () => { test('Context.contextKeys', async () => { await expect(context.contextKeys('key', 'value')).rejects.toThrow('abstract method is not implemented') }) + test('Context.setPlugins', async () => { + await expect(context.setPlugins(['plugin'])).rejects.toThrow('abstract method is not implemented') + }) }) describe('getCurrent', () => { @@ -141,3 +144,10 @@ describe('keys', () => { expect(context.contextKeys).toHaveBeenCalledWith() }) }) + +describe('getPlugins', () => { + test('()', async () => { + const ret = await context.getPlugins() + expect(ret).toEqual([]) + }) +}) diff --git a/test/token-helper.test.js b/test/token-helper.test.js index 948b4fe..b6c75e5 100644 --- a/test/token-helper.test.js +++ b/test/token-helper.test.js @@ -63,7 +63,7 @@ afterEach(() => { }) /** @private */ -function createHandlerForContext (context = {}) { +function createHandlerForContext (context = {}, withPlugin) { const mappedContext = Object.keys(context) // prefix ims. to all the keys .map(key => { @@ -76,6 +76,14 @@ function createHandlerForContext (context = {}) { return Object.assign(acc, cur) }, {}) + // add unresolvable plugin or illegale configuration value depending + // on withPlugin parameter (default is unsupported value) + if (withPlugin) { + mappedContext['ims.config.plugins'] = ['@adobe/__non_existing_login_plugin__'] + } else { + mappedContext['ims.config.plugins'] = '__unsupported_value__' + } + const store = { ...mappedContext } @@ -139,7 +147,7 @@ test('getToken - string (oauth)', async () => { setImsPluginMock('oauth', 'abc123') config.get.mockImplementation( - createHandlerForContext(context) + createHandlerForContext(context, true) ) // no force @@ -229,7 +237,7 @@ test('getToken - object (refresh token expired, coverage)', async () => { setImsPluginMock('jwt', result) config.get.mockImplementation( - createHandlerForContext(context) + createHandlerForContext(context, true) ) // no force From e8a60961ced41dc7e5f5fd3f10babbaae7961c69 Mon Sep 17 00:00:00 2001 From: Felix Meschberger Date: Wed, 7 Jul 2021 11:28:21 +0200 Subject: [PATCH 8/8] Improve on documentation and add description of plugins the README.md --- README.md | 31 ++++++++++++++++++++++++++++--- src/context.js | 2 +- src/ctx/ConfigCliContext.js | 13 +++++++++---- src/ctx/Context.js | 7 +++---- 4 files changed, 41 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index e56b55d..60421a5 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![Downloads/week](https://img.shields.io/npm/dw/@adobe/aio-lib-ims.svg)](https://npmjs.org/package/@adobe/aio-lib-ims) [![Build Status](https://travis-ci.com/adobe/aio-lib-ims.svg?branch=master)](https://travis-ci.com/adobe/aio-lib-ims) [![License](https://img.shields.io/npm/l/@adobe/aio-lib-ims.svg)](https://github.com/adobe/aio-lib-ims/blob/master/package.json) -[![Codecov Coverage](https://img.shields.io/codecov/c/github/adobe/aio-lib-ims/master.svg?style=flat-square)](https://codecov.io/gh/adobe/aio-lib-ims/) +[![Codecov Coverage](https://img.shields.io/codecov/c/github/adobe/aio-lib-ims/master.svg?style=flat-square)](https://codecov.io/gh/adobe/aio-lib-ims/) # Adobe I/O IMS Library @@ -126,7 +126,7 @@ In general, you do not need to deal with this property. ## Set Current Context (Advanced) -The default context can be set locally with `await context.setCurrent('contextname')`. +The default context can be set locally with `await context.setCurrent('contextname')`. This will write the following configuration to the `ims` key in the `.aio` file of the current working directory: ```js @@ -166,7 +166,7 @@ JWT (service to service integration) configuration requires the following proper ## Setting the Private Key -For a JWT configuration, your private key is generated in Adobe I/O Console, and is downloaded to your computer when you generate it. +For a JWT configuration, your private key is generated in Adobe I/O Console, and is downloaded to your computer when you generate it. Adobe I/O Console does not keep the private key (only your corresponding public key) so you will have to set the private key that was downloaded manually in your IMS context configuration. @@ -197,6 +197,31 @@ OAuth2 configuration requires the following properties: | redirect_uri | The _Default redirect URI_ from the integration overview screen in the I/O Console. Alternatively, any URI matching one of the _Redirect URI patterns_ may be used. | | scope | Scopes to assign to the tokens. This is a string of space separated scope names which depends on the services this integration is subscribed to. Adobe I/O Console does not currently expose the list of scopes defined for OAuth2 integrations, a good list of scopes by service can be found in [OAuth 2.0 Scopes](https://www.adobe.io/authentication/auth-methods.html#!AdobeDocs/adobeio-auth/master/OAuth/Scopes.md). At the very least you may want to enter `openid`. | + +## Token Creation Plugins + +Additional token creation plugins can be registered with the Adobe I/O IMS library configuration. +Such token creation plugins must comply with the following contract: + +* Implemented as a JavaScript module which can be `require()`-ed by the IMS library +* Registered with the module path used by the `require()` function +* Implementing 3 functions as follows: + * `Promise canSupport(config)` -- Receives the configuration properties of the selected context and returns a promise as to whether the token creation plugin can be used with this configuration. The promise must resolve to `true` if supported or be rejected with an `Error` explaining why the configuration is not supported by the plugin. + * `boolean supports(config)` -- Receives the configuration properties of the selected context and returns `true` if supported or `false` if not supported. This is a syncronous function. + * `Promise imsLogin(ims, config)` -- Receives an instance of the IMS token manager class and the configuration properties of the selected context to create an access token from. If the token creation plugin does not support the configuration or if an error occurs creating the token, the function must return a `Promise` rejecting with an `Error` explaining the problem. Otherwise the promise should resolve to an `object` containing the access token and optionally a refresh token. + +The `Context` class offers two methods to manage token creation plugins: + +* `Context.getPlugins()` returning a `Promise` listing the additional token creation plugins. +* `Context.setPlugins(string[])` taking the list of additional token creation plugins to configure. + +**Note 1**: the `setPlugins` method completely replaces the current list of plugins. +Any selective addition of removal must be done in a three step process: Get the plugins, update the list, set the plugins. + +**Note 2**: The token creation plugins supporting [JWT](https://github.com/adobe/aio-lib-ims-jwt/blob/master/src/ims-jwt.js), [OAuth2](https://github.com/adobe/aio-lib-ims-oauth/blob/master/src/ims-oauth.js), and [CLI](https://github.com/adobe/aio-lib-ims-oauth/blob/master/src/ims-cli.js) login, are always present. +As such they are note returned by the `getPlugins` method and should not be provided in the `setPlugins` method. + + # Contributing Contributions are welcomed! Read the [Contributing Guide](CONTRIBUTING.md) for more information. diff --git a/src/context.js b/src/context.js index d9f0754..6056202 100644 --- a/src/context.js +++ b/src/context.js @@ -35,7 +35,7 @@ const CLI = 'cli' /** Property holding the current context name */ const CURRENT = 'current' -/** Property holding the list of additional plugins */ +/** Property holding the list of additional token creation plugins */ const PLUGINS = 'plugins' /** @private */ diff --git a/src/ctx/ConfigCliContext.js b/src/ctx/ConfigCliContext.js index d799e00..bf91889 100644 --- a/src/ctx/ConfigCliContext.js +++ b/src/ctx/ConfigCliContext.js @@ -57,8 +57,8 @@ class ConfigCliContext extends Context { } /** - * Override super class implementation to prevent return the - * plugins if configured at all + * Override super class implementation to return the plugins configured + * in the `plugins` configuration property. * * @override */ @@ -68,8 +68,11 @@ class ConfigCliContext extends Context { } /** - * Override super class implementation to prevent return the - * plugins if configured at all + * Override super class implementation to persist the provided plugins + * in the `plugins` configuration property. + * + * This implementation silently ignores a `plugins` parameter which is + * neither and array nor `null`. * * @override */ @@ -78,6 +81,8 @@ class ConfigCliContext extends Context { if (plugins instanceof Array || plugins === null) { this.setConfigValue(this.keyNames.PLUGINS, plugins, !!local) + } else { + aioLogger.debug(' > Ignoring unexpected plugins parameter \'%o\'', plugins) } } diff --git a/src/ctx/Context.js b/src/ctx/Context.js index 5933946..b3ae396 100644 --- a/src/ctx/Context.js +++ b/src/ctx/Context.js @@ -119,7 +119,7 @@ class Context { * token creation plugins must override to return the list of * plugins which can be require-d. * - * @returns {Promise} the token creation plugins + * @returns {Promise} empty list of token creation plugins */ async getPlugins () { aioLogger.debug('getPlugins() (none)') @@ -128,10 +128,9 @@ class Context { /** * Sets the list of configured token creation plugins. This base - * implementation throws an Error as plugins are not support. + * implementation throws an Error as plugins are not supported. * Extensions supporting token creation plugins must override to - * return check and persist the list of plugins which can be - * require-d. + * check and persist the list of plugins which can be require-d. * * If the plugins parameter is an empty array or null, the current * plugins configuration is removed. Otherwise the current