diff --git a/src/utils.js b/src/utils.js index 08c805d..e14d11f 100644 --- a/src/utils.js +++ b/src/utils.js @@ -23,6 +23,7 @@ const archiver = require('archiver') const SupportedRuntimes = ['sequence', 'blackbox', 'nodejs:10', 'nodejs:12', 'nodejs:14', 'nodejs:16', 'nodejs:18', 'nodejs:20', 'nodejs:22', 'nodejs:24'] const { HttpProxyAgent } = require('http-proxy-agent') const PatchedHttpsProxyAgent = require('./PatchedHttpsProxyAgent.js') +const { getCliEnv, DEFAULT_ENV } = require('@adobe/aio-lib-env') // must cover 'deploy-service[-region][.env].app-builder[.int|.corp].adp.adobe.io/runtime const SUPPORTED_ADOBE_ANNOTATION_ENDPOINT_REGEXES = [ @@ -41,6 +42,7 @@ const ANNOTATION_WEB_EXPORT = 'web-export' const ANNOTATION_RAW_HTTP = 'raw-http' const ANNOTATION_REQUIRE_ADOBE_AUTH = 'require-adobe-auth' const ANNOTATION_REQUIRE_WHISK_AUTH = 'require-whisk-auth' +const ANNOTATION_INCLUDE_IMS_CREDENTIALS = 'include-ims-credentials' const VALUE_YES = 'yes' const VALUE_RAW = 'raw' @@ -1186,6 +1188,68 @@ function rewriteActionsWithAdobeAuthAnnotation (packages, deploymentPackages) { } } +/** + * This function implements the support for the `include-ims-credentials` annotation. + * It will expand the IMS_OAUTH_S2S environment variable into an input object stored under params.__ims_oauth_s2s and params.__ims_env + * + * @access private + * @param {ManifestPackages} packages the manifest packages + * @returns {ManifestPackages} newPackages, rewritten package with added inputs + */ +function rewriteActionsWithAdobeIncludeIMSCredentialsAnnotation (packages) { + // avoid side effects, do not modify input packages + const newPackages = cloneDeep(packages) + + // constants + const IMS_OAUTH_S2S_ENV_KEY = 'IMS_OAUTH_S2S' + + let imsAuthObject = null + try { + imsAuthObject = JSON.parse(process.env[IMS_OAUTH_S2S_ENV_KEY]) + } catch (e) {} + + // traverse all actions in all packages + Object.keys(newPackages).forEach((key) => { + if (newPackages[key].actions) { + Object.keys(newPackages[key].actions).forEach((actionName) => { + const thisAction = newPackages[key].actions[actionName] + const newInputs = getIncludeIMSCredentialsAnnotationInputs(thisAction, imsAuthObject) + if (newInputs) { + Object.entries(newInputs).forEach(([k, v]) => { thisAction.inputs[k] = v }) + aioLogger.debug(`processed annotation '${ANNOTATION_INCLUDE_IMS_CREDENTIALS}' for action '${key}/${actionName}'.`) + } + }) + } + }) + return newPackages +} + +/** + * Get the inputs for the include-ims-credentials annotation. + * + * @param {object} thisAction the action to process + * @param {object} imsAuthObject the IMS auth object + * @returns {object|undefined} the inputs + */ +function getIncludeIMSCredentialsAnnotationInputs (thisAction, imsAuthObject) { + const env = getCliEnv() || DEFAULT_ENV + + const IMS_OAUTH_S2S_INPUT = '__ims_oauth_s2s' + const IMS_ENV_INPUT = '__ims_env' + const IMS_OAUTH_S2S_ENV_KEY = 'IMS_OAUTH_S2S' + + // check if the annotation is defined + if (thisAction.annotations?.[ANNOTATION_INCLUDE_IMS_CREDENTIALS]) { + // check if the action is a web action + if (!imsAuthObject) { + aioLogger.warn(`The project has no credentials attached (missing the '${IMS_OAUTH_S2S_ENV_KEY}' environment variable). The annotation '${ANNOTATION_INCLUDE_IMS_CREDENTIALS}' will be ignored.`) + return + } + + return { [IMS_OAUTH_S2S_INPUT]: { ...imsAuthObject }, [IMS_ENV_INPUT]: env } + } +} + /** * * Process the manifest and deployment content and returns deployment entities. @@ -1210,11 +1274,14 @@ function processPackage (packages, const isAdobeEndpoint = SUPPORTED_ADOBE_ANNOTATION_ENDPOINT_REGEXES.some(regex => regex.test(owOptions.apihost)) if (isAdobeEndpoint) { + // rewrite packages in case there are any `include-ims-credentials` annotations + const newPackages = rewriteActionsWithAdobeIncludeIMSCredentialsAnnotation(pkgs) + // rewrite packages in case there are any `require-adobe-auth` annotations // this is a temporary feature and will be replaced by a native support in Adobe I/O Runtime - const { newPackages, newDeploymentPackages } = rewriteActionsWithAdobeAuthAnnotation(pkgs, deploymentPkgs) - pkgs = newPackages - deploymentPkgs = newDeploymentPackages + const ret = rewriteActionsWithAdobeAuthAnnotation(newPackages, deploymentPkgs) + pkgs = ret.newPackages + deploymentPkgs = ret.newDeploymentPackages } const pkgAndDeps = [] @@ -2185,5 +2252,6 @@ module.exports = { dumpActionsBuiltInfo, safeParse, isSupportedActionKind, + getIncludeIMSCredentialsAnnotationInputs, DEFAULT_PACKAGE_RESERVED_NAME } diff --git a/test/utils.test.js b/test/utils.test.js index 1107f71..db53394 100644 --- a/test/utils.test.js +++ b/test/utils.test.js @@ -2710,3 +2710,138 @@ describe('getProxyAgent', () => { expect(result).toBeInstanceOf(PatchedHttpsProxyAgent) }) }) + +describe('include-ims-credentials annotation', () => { + const fakeCode = 'fake action code' + let spy + let originalEnv + + beforeEach(() => { + spy = jest.spyOn(fs, 'readFileSync') + spy.mockImplementation(() => fakeCode) + originalEnv = process.env.IMS_OAUTH_S2S + }) + + afterEach(() => { + spy.mockRestore() + libEnv.getCliEnv.mockReturnValue(PROD_ENV) + if (originalEnv !== undefined) { + process.env.IMS_OAUTH_S2S = originalEnv + } else { + delete process.env.IMS_OAUTH_S2S + } + }) + + test('action with include-ims-credentials annotation gets IMS inputs added', () => { + const imsCredentials = { client_id: 'test-client', client_secret: 'test-secret' } + process.env.IMS_OAUTH_S2S = JSON.stringify(imsCredentials) + libEnv.getCliEnv.mockReturnValue(PROD_ENV) + + const packages = { + pkg1: { + actions: { + theaction: { + function: 'fake.js', + web: 'yes', + inputs: { existingInput: 'value' }, + annotations: { + 'include-ims-credentials': true + } + } + } + } + } + + const res = utils.processPackage(packages, {}, {}, {}, false, { apihost: 'https://adobeioruntime.net' }) + expect(res.actions[0].params).toEqual({ + existingInput: 'value', + __ims_oauth_s2s: imsCredentials, + __ims_env: PROD_ENV + }) + }) + + test('action with include-ims-credentials annotation and no credentials logs warning', () => { + delete process.env.IMS_OAUTH_S2S + const loggerSpy = jest.spyOn(aioLogger, 'warn') + + const packages = { + pkg1: { + actions: { + theaction: { + function: 'fake.js', + web: 'yes', + annotations: { + 'include-ims-credentials': true + } + } + } + } + } + + utils.processPackage(packages, {}, {}, {}, false, { apihost: 'https://adobeioruntime.net' }) + expect(loggerSpy).toHaveBeenCalledWith("The project has no credentials attached (missing the 'IMS_OAUTH_S2S' environment variable). The annotation 'include-ims-credentials' will be ignored.") + }) +}) + +describe('getIncludeIMSCredentialsAnnotationInputs', () => { + afterEach(() => { + libEnv.getCliEnv.mockReturnValue(PROD_ENV) + }) + + test('returns undefined if annotation is not set', () => { + const action = { + annotations: {} + } + const result = utils.getIncludeIMSCredentialsAnnotationInputs(action, { client_id: 'test' }) + expect(result).toBeUndefined() + }) + + test('returns undefined and warns if annotation is set but imsAuthObject is null', () => { + const action = { + annotations: { 'include-ims-credentials': true } + } + const loggerSpy = jest.spyOn(aioLogger, 'warn') + const result = utils.getIncludeIMSCredentialsAnnotationInputs(action, null) + expect(result).toBeUndefined() + expect(loggerSpy).toHaveBeenCalledWith("The project has no credentials attached (missing the 'IMS_OAUTH_S2S' environment variable). The annotation 'include-ims-credentials' will be ignored.") + }) + + test('returns inputs with ims credentials and prod env', () => { + libEnv.getCliEnv.mockReturnValue(PROD_ENV) + const action = { + annotations: { 'include-ims-credentials': true } + } + const imsAuthObject = { client_id: 'test-client', client_secret: 'test-secret' } + const result = utils.getIncludeIMSCredentialsAnnotationInputs(action, imsAuthObject) + expect(result).toEqual({ + __ims_oauth_s2s: { client_id: 'test-client', client_secret: 'test-secret' }, + __ims_env: PROD_ENV + }) + }) + + test('returns inputs with ims credentials and stage env', () => { + libEnv.getCliEnv.mockReturnValue(STAGE_ENV) + const action = { + annotations: { 'include-ims-credentials': true } + } + const imsAuthObject = { client_id: 'test-client' } + const result = utils.getIncludeIMSCredentialsAnnotationInputs(action, imsAuthObject) + expect(result).toEqual({ + __ims_oauth_s2s: { client_id: 'test-client' }, + __ims_env: STAGE_ENV + }) + }) + + test('returns inputs with default env when getCliEnv returns null', () => { + libEnv.getCliEnv.mockReturnValue(null) + const action = { + annotations: { 'include-ims-credentials': true } + } + const imsAuthObject = { client_id: 'test-client' } + const result = utils.getIncludeIMSCredentialsAnnotationInputs(action, imsAuthObject) + expect(result).toEqual({ + __ims_oauth_s2s: { client_id: 'test-client' }, + __ims_env: PROD_ENV + }) + }) +})