diff --git a/package.json b/package.json index 9ac542738..6603975ba 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "@adobe/aio-lib-core-networking": "^5", "@adobe/aio-lib-env": "^3", "@adobe/aio-lib-ims": "^7", - "@adobe/aio-lib-runtime": "^7.0.1", + "@adobe/aio-lib-runtime": "^7.1.0", "@adobe/aio-lib-templates": "^3", "@adobe/aio-lib-web": "^7", "@adobe/generator-aio-app": "^7", diff --git a/src/commands/app/config/set/log-forwarding.js b/src/commands/app/config/set/log-forwarding.js index a805a9480..d47e5cfde 100644 --- a/src/commands/app/config/set/log-forwarding.js +++ b/src/commands/app/config/set/log-forwarding.js @@ -12,10 +12,16 @@ governing permissions and limitations under the License. const BaseCommand = require('../../../../BaseCommand') const LogForwarding = require('../../../../lib/log-forwarding') const aioLogger = require('@adobe/aio-lib-core-logging')('@adobe/aio-cli-plugin-app:lf:set', { provider: 'debug' }) +const { setRuntimeApiHostAndAuthHandler } = require('../../../../lib/auth-helper') class LogForwardingCommand extends BaseCommand { async run () { - const lf = await LogForwarding.init((await this.getFullConfig()).aio) + let aioConfig = (await this.getFullConfig()).aio + // TODO: remove this check once the deploy service is enabled by default + if (process.env.IS_DEPLOY_SERVICE_ENABLED === 'true') { + aioConfig = setRuntimeApiHostAndAuthHandler(aioConfig) + } + const lf = await LogForwarding.init(aioConfig) const destination = await this.promptDestination(lf.getSupportedDestinations()) const destinationSettingsConfig = lf.getSettingsConfig(destination) diff --git a/src/commands/app/deploy.js b/src/commands/app/deploy.js index e28141048..59b903052 100644 --- a/src/commands/app/deploy.js +++ b/src/commands/app/deploy.js @@ -22,6 +22,7 @@ const { runInProcess, buildExtensionPointPayloadWoMetadata, buildExcShellViewExt const rtLib = require('@adobe/aio-lib-runtime') const LogForwarding = require('../../lib/log-forwarding') const { sendAuditLogs, getAuditLogEvent, getFilesCountWithExtension } = require('../../lib/audit-logger') +const { setRuntimeApiHostAndAuthHandler } = require('../../lib/auth-helper') const logActions = require('../../lib/log-actions') const PRE_DEPLOY_EVENT_REG = 'pre-deploy-event-reg' @@ -100,7 +101,8 @@ class Deploy extends BuildCommand { // - break into smaller pieces deploy, allowing to first deploy all actions then all web assets for (let i = 0; i < keys.length; ++i) { const k = keys[i] - const v = values[i] + const v = setRuntimeApiHostAndAuthHandler(values[i]) + await this.deploySingleConfig(k, v, flags, spinner) if (v.app.hasFrontend && flags['web-assets']) { const opItems = getFilesCountWithExtension(v.web.distProd) diff --git a/src/commands/app/undeploy.js b/src/commands/app/undeploy.js index feed4acc7..d83ad879c 100644 --- a/src/commands/app/undeploy.js +++ b/src/commands/app/undeploy.js @@ -20,6 +20,7 @@ const webLib = require('@adobe/aio-lib-web') const { runInProcess, buildExtensionPointPayloadWoMetadata, getCliInfo } = require('../../lib/app-helper') const rtLib = require('@adobe/aio-lib-runtime') const { sendAuditLogs, getAuditLogEvent } = require('../../lib/audit-logger') +const { setRuntimeApiHostAndAuthHandler } = require('../../lib/auth-helper') class Undeploy extends BaseCommand { async run () { @@ -55,7 +56,9 @@ class Undeploy extends BaseCommand { for (let i = 0; i < keys.length; ++i) { const k = keys[i] - const v = values[i] + // TODO: remove this check once the deploy service is enabled by default + const v = process.env.IS_DEPLOY_SERVICE_ENABLED === 'true' ? setRuntimeApiHostAndAuthHandler(values[i]) : values[i] + await this.undeployOneExt(k, v, flags, spinner) const assetUndeployLogEvent = getAuditLogEvent(flags, aioConfig.project, 'AB_APP_ASSETS_UNDEPLOYED') // send logs for case of web-assets undeployment diff --git a/src/lib/auth-helper.js b/src/lib/auth-helper.js new file mode 100644 index 000000000..73c987365 --- /dev/null +++ b/src/lib/auth-helper.js @@ -0,0 +1,63 @@ +/* +Copyright 2024 Adobe. All rights reserved. +This file is licensed to you 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 REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ + +const { getToken, context } = require('@adobe/aio-lib-ims') +const { CLI } = require('@adobe/aio-lib-ims/src/context') +const { getCliEnv } = require('@adobe/aio-lib-env') +const defaultRuntimeUrl = 'https://adobeioruntime.net' + +/** + * For use with the openwhisk client js library to send a bearer token instead of basic + * auth to the openwhisk service. Set this to the auth_handler option when initializing + */ +const bearerAuthHandler = { + getAuthHeader: async () => { + await context.setCli({ 'cli.bare-output': true }, false) // set this globally + + const env = getCliEnv() + + console.debug(`Retrieving CLI Token using env=${env}`) + const accessToken = await getToken(CLI) + + return `Bearer ${accessToken}` + } +} + +const setRuntimeApiHostAndAuthHandler = (config) => { + // TODO: remove this check once the deploy service is enabled by default + if (process.env.IS_DEPLOY_SERVICE_ENABLED === 'true') { + const aioConfig = (config && 'runtime' in config) ? config : null + if (aioConfig) { + aioConfig.runtime.apihost = process.env.AIO_RUNTIME_APIHOST ?? defaultRuntimeUrl + aioConfig.runtime.auth_handler = bearerAuthHandler + return aioConfig + } + const owConfig = (config && 'ow' in config) ? config : null + if (owConfig) { + owConfig.ow.apihost = process.env.AIO_RUNTIME_APIHOST ?? defaultRuntimeUrl + owConfig.ow.auth_handler = bearerAuthHandler + return owConfig + } + } else { + if (config && config.runtime) { + config.runtime.apihost = process.env.AIO_RUNTIME_APIHOST ?? defaultRuntimeUrl + } + if (config && config.ow) { + config.ow.apihost = process.env.AIO_RUNTIME_APIHOST ?? defaultRuntimeUrl + } + } + return config +} + +module.exports = { + bearerAuthHandler, + setRuntimeApiHostAndAuthHandler +} diff --git a/test/commands/app/config/set/log-forwarding.test.js b/test/commands/app/config/set/log-forwarding.test.js index 040224863..14efbafd9 100644 --- a/test/commands/app/config/set/log-forwarding.test.js +++ b/test/commands/app/config/set/log-forwarding.test.js @@ -14,6 +14,9 @@ const { stdout } = require('stdout-stderr') const TheCommand = require('../../../../../src/commands/app/config/set/log-forwarding.js') const LogForwarding = require('../../../../../src/lib/log-forwarding') +jest.mock('../../../../../src/lib/auth-helper') +const authHelper = require('../../../../../src/lib/auth-helper') + jest.mock('../../../../../src/lib/log-forwarding', () => { const orig = jest.requireActual('../../../../../src/lib/log-forwarding') return { @@ -46,6 +49,7 @@ beforeEach(async () => { getConfigFromJson: jest.fn() } LogForwarding.init.mockResolvedValue(lf) + authHelper.setRuntimeApiHostAndAuthHandler.mockImplementation(aioConfig => aioConfig) }) test('set log forwarding destination and save local', async () => { @@ -86,6 +90,50 @@ test('set log forwarding destination and save local', async () => { expect(setCall).toHaveBeenCalledWith(new LogForwarding.LogForwardingConfig(destination, input)) expect(localSetCall).toHaveBeenCalledTimes(1) expect(localSetCall).toHaveBeenCalledWith(new LogForwarding.LogForwardingConfig(destination, fullSanitizedSettings)) + expect(authHelper.setRuntimeApiHostAndAuthHandler).not.toHaveBeenCalled() +}) + +test('should Invoke setRuntimeApiHostAndAuthHandler if IS_DEPLOY_SERVICE_ENABLED = ture and set log forwarding destination', async () => { + process.env.IS_DEPLOY_SERVICE_ENABLED = true + const destination = 'destination' + const input = { + field_one: 'val_one', + field_two: 'val_two', + secret: 'val_secret' + } + command.prompt.mockResolvedValueOnce({ type: destination }) + command.prompt.mockResolvedValueOnce(input) + const serverSanitizedSettings = { + field_one: 'val_one', + field_two: 'val_two sanitized' + } + const fullSanitizedSettings = { + field_one: 'val_one', + field_two: 'val_two sanitized', + secret: 'val_secret' + } + const setCall = jest.fn().mockResolvedValue({ + destination: serverSanitizedSettings + }) + const localSetCall = jest.fn() + lf.updateServerConfig = setCall + lf.updateLocalConfig = localSetCall.mockResolvedValue() + lf.getConfigFromJson.mockReturnValue(new LogForwarding.LogForwardingConfig(destination, serverSanitizedSettings)) + + await expect(command.run()).resolves.not.toThrow() + expect(command.prompt).toHaveBeenNthCalledWith(1, [{ + name: 'type', + message: 'select log forwarding destination', + type: 'list', + choices: [{ name: 'Destination', value: 'destination' }] + }]) + expect(stdout.output).toMatch(`Log forwarding is set to '${destination}'\nLog forwarding settings are saved to the local configuration`) + expect(setCall).toHaveBeenCalledTimes(1) + expect(setCall).toHaveBeenCalledWith(new LogForwarding.LogForwardingConfig(destination, input)) + expect(localSetCall).toHaveBeenCalledTimes(1) + expect(localSetCall).toHaveBeenCalledWith(new LogForwarding.LogForwardingConfig(destination, fullSanitizedSettings)) + expect(authHelper.setRuntimeApiHostAndAuthHandler).toHaveBeenCalledTimes(1) + process.env.IS_DEPLOY_SERVICE_ENABLED = false }) test('set log forwarding destination and fail save local', async () => { diff --git a/test/commands/app/deploy.test.js b/test/commands/app/deploy.test.js index e35db7292..7e63499fe 100644 --- a/test/commands/app/deploy.test.js +++ b/test/commands/app/deploy.test.js @@ -24,6 +24,9 @@ const helpers = require('../../../src/lib/app-helper.js') jest.mock('../../../src/lib/audit-logger.js') const auditLogger = require('../../../src/lib/audit-logger.js') +jest.mock('../../../src/lib/auth-helper') +const authHelper = require('../../../src/lib/auth-helper') + const mockWebLib = require('@adobe/aio-lib-web') const mockRuntimeLib = require('@adobe/aio-lib-runtime') @@ -191,6 +194,7 @@ beforeEach(() => { '1 HTML page(s)' ] }) + authHelper.setRuntimeApiHostAndAuthHandler.mockImplementation((aioConfig) => aioConfig) helpers.getCliInfo.mockImplementation(() => { return { accessToken: 'mocktoken', @@ -1293,6 +1297,45 @@ describe('run', () => { expect(command.error).toHaveBeenCalledTimes(1) }) + test('Should invoke setRuntimeApiHostAndAuthHandler if IS_DEPLOY_SERVICE_ENABLED = true', async () => { + process.env.IS_DEPLOY_SERVICE_ENABLED = true + + const mockToken = 'mocktoken' + const mockEnv = 'stage' + const mockOrg = 'mockorg' + const mockProject = 'mockproject' + const mockWorkspaceId = 'mockworkspaceid' + const mockWorkspaceName = 'mockworkspacename' + helpers.getCliInfo.mockResolvedValueOnce({ + accessToken: mockToken, + env: mockEnv + }) + command.getFullConfig = jest.fn().mockReturnValue({ + aio: { + project: { + id: mockProject, + org: { + id: mockOrg + }, + workspace: { + id: mockWorkspaceId, + name: mockWorkspaceName + } + } + } + }) + command.getAppExtConfigs.mockResolvedValueOnce(createAppConfig(command.appConfig)) + + await command.run() + expect(command.error).toHaveBeenCalledTimes(0) + expect(mockRuntimeLib.deployActions).toHaveBeenCalledTimes(1) + expect(mockWebLib.deployWeb).toHaveBeenCalledTimes(1) + expect(auditLogger.sendAuditLogs).toHaveBeenCalledTimes(1) + expect(authHelper.setRuntimeApiHostAndAuthHandler).toHaveBeenCalledTimes(1) + expect(auditLogger.sendAuditLogs).toHaveBeenCalledWith(mockToken, expect.objectContaining({ orgId: mockOrg, projectId: mockProject, workspaceId: mockWorkspaceId, workspaceName: mockWorkspaceName }), mockEnv) + process.env.IS_DEPLOY_SERVICE_ENABLED = false + }) + test('Send audit logs for successful app deploy', async () => { const mockToken = 'mocktoken' const mockEnv = 'stage' diff --git a/test/commands/app/undeploy.test.js b/test/commands/app/undeploy.test.js index ba908bb03..fd96e91aa 100644 --- a/test/commands/app/undeploy.test.js +++ b/test/commands/app/undeploy.test.js @@ -20,6 +20,9 @@ const helpers = require('../../../src/lib/app-helper.js') jest.mock('../../../src/lib/audit-logger.js') const auditLogger = require('../../../src/lib/audit-logger.js') +jest.mock('../../../src/lib/auth-helper.js') +const authHelper = require('../../../src/lib/auth-helper.js') + const mockFS = require('fs-extra') jest.mock('fs-extra') @@ -78,6 +81,7 @@ beforeEach(() => { env: 'stage' } }) + authHelper.setRuntimeApiHostAndAuthHandler.mockImplementation(aioConfig => aioConfig) jest.clearAllMocks() }) @@ -472,6 +476,38 @@ describe('run', () => { expect(command.error).toHaveBeenCalledTimes(1) }) + test('Should invoke setRuntimeApiHostAndAuthHandler if IS_DEPLOY_SERVICE_ENABLED = true', async () => { + const mockOrg = 'mockorg' + const mockProject = 'mockproject' + const mockWorkspaceId = 'mockworkspaceid' + const mockWorkspaceName = 'mockworkspacename' + + process.env.IS_DEPLOY_SERVICE_ENABLED = true + + command.getFullConfig = jest.fn().mockReturnValue({ + aio: { + project: { + id: mockProject, + org: { + id: mockOrg + }, + workspace: { + id: mockWorkspaceId, + name: mockWorkspaceName + } + } + } + }) + command.getAppExtConfigs.mockResolvedValueOnce(createAppConfig(command.appConfig)) + + await command.run() + expect(command.error).toHaveBeenCalledTimes(0) + expect(mockRuntimeLib.undeployActions).toHaveBeenCalledTimes(1) + expect(mockWebLib.undeployWeb).toHaveBeenCalledTimes(1) + expect(authHelper.setRuntimeApiHostAndAuthHandler).toHaveBeenCalledTimes(1) + process.env.IS_DEPLOY_SERVICE_ENABLED = false + }) + test('Send audit logs for successful app undeploy', async () => { const mockToken = 'mocktoken' const mockEnv = 'stage' diff --git a/test/commands/lib/auth-helper.test.js b/test/commands/lib/auth-helper.test.js new file mode 100644 index 000000000..0fc41011e --- /dev/null +++ b/test/commands/lib/auth-helper.test.js @@ -0,0 +1,81 @@ +const { bearerAuthHandler, setRuntimeApiHostAndAuthHandler } = require('../../../src/lib/auth-helper') +const { getToken, context } = require('@adobe/aio-lib-ims') +const { CLI } = require('@adobe/aio-lib-ims/src/context') +const { getCliEnv } = require('@adobe/aio-lib-env') + +jest.mock('@adobe/aio-lib-ims') +jest.mock('@adobe/aio-lib-env') + +describe('bearerAuthHandler', () => { + beforeEach(() => { + jest.clearAllMocks() + }) + + test('getAuthHeader should return a Bearer token', async () => { + const mockToken = 'mocked-token' + getToken.mockResolvedValue(mockToken) + getCliEnv.mockReturnValue('test-env') + + const result = await bearerAuthHandler.getAuthHeader() + + expect(context.setCli).toHaveBeenCalledWith({ 'cli.bare-output': true }, false) + expect(getCliEnv).toHaveBeenCalled() + expect(getToken).toHaveBeenCalledWith(CLI) + expect(result).toBe(`Bearer ${mockToken}`) + }) +}) + +describe('setRuntimeApiHostAndAuthHandler', () => { + const defaultRuntimeUrl = 'https://adobeioruntime.net' + beforeEach(() => { + jest.clearAllMocks() + process.env.IS_DEPLOY_SERVICE_ENABLED = 'true' + }) + + test('should set runtime.apihost and runtime.auth_handler when config has runtime', () => { + const config = { runtime: {} } + const result = setRuntimeApiHostAndAuthHandler(config) + + expect(result.runtime.apihost).toBe(process.env.AIO_RUNTIME_APIHOST ?? defaultRuntimeUrl) + expect(result.runtime.auth_handler).toBe(bearerAuthHandler) + }) + + test('should set ow.apihost and ow.auth_handler when config has ow', () => { + const config = { ow: {} } + const result = setRuntimeApiHostAndAuthHandler(config) + + expect(result.ow.apihost).toBe(process.env.AIO_RUNTIME_APIHOST ?? defaultRuntimeUrl) + expect(result.ow.auth_handler).toBe(bearerAuthHandler) + }) + + test('should return config unchanged when config has neither runtime nor ow', () => { + const config = { other: {} } + const result = setRuntimeApiHostAndAuthHandler(config) + + expect(result).toBe(config) + }) + + test('should return null when config is null', () => { + const result = setRuntimeApiHostAndAuthHandler(null) + + expect(result).toBeNull() + }) + + test('should set default runtime.apihost only config has runtime', () => { + process.env.IS_DEPLOY_SERVICE_ENABLED = 'false' + const config = { runtime: {} } + const result = setRuntimeApiHostAndAuthHandler(config) + + expect(result.runtime.apihost).toBe(defaultRuntimeUrl) + expect(result.runtime.auth_handler).toBeUndefined() + }) + + test('should set default ow.apihost only config has openwhisk', () => { + process.env.IS_DEPLOY_SERVICE_ENABLED = 'false' + const config = { ow: {} } + const result = setRuntimeApiHostAndAuthHandler(config) + + expect(result.ow.apihost).toBe(defaultRuntimeUrl) + expect(result.ow.auth_handler).toBeUndefined() + }) +})