diff --git a/README.md b/README.md index fdcee779..004a20b8 100644 --- a/README.md +++ b/README.md @@ -70,10 +70,40 @@ Alternatively, if you have selected an organization using `aio console:org:selec To use a service account authentication, an integration (aka project) must be created in the [Adobe I/O Console](https://console.adobe.io) which has the Cloud Manager service. -***The required type of server-to-server authentication should be [Service Account (JWT)](https://developer.adobe.com/developer-console/docs/guides/authentication/ServerToServerAuthentication/#service-account-jwt-credential-deprecated).*** +***The required type of server-to-server authentication should be [Service Account (JWT/OAuth)](https://developer.adobe.com/developer-console/docs/guides/authentication/ServerToServerAuthentication).*** +***NOTE:*** The JWT mode of authentication is deprecated and will be completely removed by Jan,2025. So if you are using JWT integration, it is recommended to migrate to OAuth + +#### Setup for OAuth integration After you've created the integration, create a `config.json` file on your computer and navigate to the integration Overview page. From this page, copy the values into the file as described below. +``` +//config.json +{ + "client_id": "value from your CLI integration (String)", + "client_secret": "value from your CLI integration (String)", + "technical_account_id": "value from your CLI integration (String)", + "technical_account_email": "value from your CLI integration (String)", + "ims_org_id": "value from your CLI integration (String)", + "scopes": [ + 'openid', + 'AdobeID', + 'read_organizations', + 'additional_info.projectedProductContext', + 'read_pc.dma_aem_ams' + ], + "oauth_enabled": true +} +``` + +Configure the credentials: +``` +aio config:set ims.contexts.aio-cli-plugin-cloudmanager PATH_TO_CONFIG_JSON_FILE --file --json +``` + +#### Setup for JWT integration + +After you've created the integration, create a `config.json` file on your computer and navigate to the integration Overview page. From this page, copy the values into the file as described below. ``` //config.json { @@ -83,7 +113,8 @@ After you've created the integration, create a `config.json` file on your comput "ims_org_id": "value from your CLI integration (String)", "meta_scopes": [ "ent_cloudmgr_sdk" - ] + ], + "oauth_enabled": false } ``` @@ -1371,6 +1402,16 @@ Note that the private key **must** be base64 encoded, e.g. by running $ base64 -i private.key ``` +To run tests with OAuth credentials, add the following to `.env`: + +``` +OAUTH_E2E_CLIENT_ID= +OAUTH_E2E_CLIENT_SECRET= +OAUTH_E2E_TA_ID= +OAUTH_E2E_TA_EMAIL= +OAUTH_E2E_IMS_ORG_ID= +``` + With this in place the end-to-end tests can be run with ``` diff --git a/e2e/e2e.js b/e2e/e2e.js index d4a72b1d..89b27e74 100644 --- a/e2e/e2e.js +++ b/e2e/e2e.js @@ -43,8 +43,9 @@ const exec = (cmd, args) => { /* Used in list-programs, which is stubbed out. Env vars need to be defined and code enabled + Test using JWT integrations; will be removed when JWT is discontinued */ -const bootstrapAuthContext = async () => { +const bootstrapAuthContextWithJWTIntegration = async () => { const contextObj = { client_id: process.env.E2E_CLIENT_ID, client_secret: process.env.E2E_CLIENT_SECRET, @@ -54,6 +55,32 @@ const bootstrapAuthContext = async () => { 'ent_cloudmgr_sdk', ], // private_key: Buffer.from(process.env.E2E_PRIVATE_KEY_B64, 'base64').toString(), + oauth_enabled: false, + } + + await context.set(CONTEXT_NAME, contextObj) +} + +/* + Used in list-programs, which is stubbed out. + Env vars need to be defined and code enabled + Test using OAuth integrations +*/ +const bootstrapAuthContextWithOAuthIntegration = async () => { + const contextObj = { + client_id: process.env.OAUTH_E2E_CLIENT_ID, + client_secrets: [process.env.OAUTH_E2E_CLIENT_SECRET], + technical_account_id: process.env.OAUTH_E2E_TA_ID, + technical_account_email: process.env.OAUTH_E2E_TA_EMAIL, + ims_org_id: process.env.OAUTH_E2E_IMS_ORG_ID, + scopes: [ + 'openid', + 'AdobeID', + 'read_organizations', + 'additional_info.projectedProductContext', + 'read_pc.dma_aem_ams', + ], + oauth_enabled: true, } await context.set(CONTEXT_NAME, contextObj) @@ -76,11 +103,38 @@ test('plugin-cloudmanager help test', async () => { */ /* * Note: this test cannot be run by the bot, since it requires setup which the bot can't provide - * If wanting to rn the test, the evironment variables have to be set with the required authentication information + * If wanting to run the test, the environment variables have to be set with the required authentication information + * Uses JWT integration which is deprecated; will be removed when JWT is discontinued */ -test('plugin-cloudmanager list-programs', async () => { - await bootstrapAuthContext() +test('plugin-cloudmanager list-programs using JWT integration', async () => { + await bootstrapAuthContextWithJWTIntegration() + const packagejson = JSON.parse(fs.readFileSync('package.json').toString()) + const name = `${packagejson.name}` + console.log(chalk.blue(`> e2e tests for ${chalk.bold(name)}`)) + + console.log(chalk.dim(' - plugin-cloudmanager list-programs ..')) + + // let result + // expect(() => { result = exec('./bin/run', ['cloudmanager:list-programs', ...CONTEXT_ARGS, '--json']) }).not.toThrow() + // const parsed = JSON.parse(result.stdout) + const parsed = '{}' + expect(parsed).toSatisfy(arr => arr.length > 0) + + console.log(chalk.green(` - done for ${chalk.bold(name)}`)) +}) + +/* + Side condition: debug log output must not be enabled (DEBUG=* or LOG_LEVEL=debug), + or else the result in result.stdout is not valid JSON and cannot be parsed (line: JSON.parse...) +*/ +/* + * Note: this test cannot be run by the bot, since it requires setup which the bot can't provide + * If wanting to run the test, the environment variables have to be set with the required authentication information + * Uses OAuth integrations + */ +test('plugin-cloudmanager list-programs using OAuth integration', async () => { + await bootstrapAuthContextWithOAuthIntegration() const packagejson = JSON.parse(fs.readFileSync('package.json').toString()) const name = `${packagejson.name}` console.log(chalk.blue(`> e2e tests for ${chalk.bold(name)}`)) diff --git a/package.json b/package.json index 5dcb0136..fd0bae9c 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "@adobe/aio-lib-core-errors": "^3.1.1", "@adobe/aio-lib-core-logging": "^2.0.0", "@adobe/aio-lib-core-networking": "^3.0.0", - "@adobe/aio-lib-ims": "^6.0.1", + "@adobe/aio-lib-ims": "^6.5.0", "@oclif/command": "^1.6.1", "@oclif/config": "^1.15.1", "@oclif/parser": "^3.8.5", @@ -203,4 +203,4 @@ "@semantic-release/github" ] } -} \ No newline at end of file +} diff --git a/src/ConfigurationErrors.js b/src/ConfigurationErrors.js index 2d42e03b..2a9a95a9 100644 --- a/src/ConfigurationErrors.js +++ b/src/ConfigurationErrors.js @@ -55,6 +55,7 @@ E('CLI_AUTH_NO_ORG', 'The CLI has been authenticated, but no organization has be E('NO_DEFAULT_IMS_CONTEXT', 'There is no IMS context configuration defined for %s. Either define this context configuration or authenticate using "aio auth:login" and select an organization using "aio cloudmanager:org:select".') E('IMS_CONTEXT_MISSING_FIELDS', 'One or more of the required fields in %s were not set. Missing keys were %s.') E('IMS_CONTEXT_MISSING_METASCOPE', 'The configuration %s is missing the required metascope %s.') +E('IMS_CONTEXT_MISSING_OAUTH_SCOPES', 'The configuration %s is missing the required OAuth scopes %s.') E('CLI_AUTH_EXPLICIT_NO_AUTH', 'cli context explicitly enabled, but not authenticated. You must run "aio auth:login" first.') E('CLI_AUTH_EXPLICIT_NO_ORG', 'cli context explicitly enabled but no org id specified. Configure using either "cloudmanager_orgid" or by running "aio cloudmanager:org:select"') E('CLI_AUTH_CONTEXT_CANNOT_DECODE', 'The access token configured for cli authentication cannot be decoded.') diff --git a/src/hooks/prerun/check-ims-context-config.js b/src/hooks/prerun/check-ims-context-config.js index a615435a..93c0fbab 100644 --- a/src/hooks/prerun/check-ims-context-config.js +++ b/src/hooks/prerun/check-ims-context-config.js @@ -16,9 +16,10 @@ const { isThisPlugin } = require('../../cloudmanager-hook-helpers') const { defaultImsContextName: defaultContextName } = require('../../constants') const { codes: configurationCodes } = require('../../ConfigurationErrors') -const requiredKeys = ['client_id', 'client_secret', 'technical_account_id', 'meta_scopes', 'ims_org_id', 'private_key'] - -const requiredMetaScope = 'ent_cloudmgr_sdk' +const requiredKeysForJWTIntegration = ['client_id', 'client_secret', 'technical_account_id', 'meta_scopes', 'ims_org_id', 'private_key'] +const requiredKeysForOAuthIntegration = ['client_id', 'client_secrets', 'technical_account_email', 'technical_account_id', 'scopes', 'ims_org_id'] +const requiredMetaScopeForJWTIntegration = 'ent_cloudmgr_sdk' +const requiredScopesForOAuthIntegration = ['openid', 'AdobeID', 'read_organizations', 'additional_info.projectedProductContext', 'read_pc.dma_aem_ams'] function getContextName (options) { if (options.Command.flags && options.Command.flags.imsContextName) { @@ -46,6 +47,7 @@ module.exports = function (hookOptions) { } const missingKeys = [] + const requiredKeys = config.oauth_enabled ? requiredKeysForOAuthIntegration : requiredKeysForJWTIntegration requiredKeys.forEach(key => { if (!config[key]) { @@ -57,9 +59,16 @@ module.exports = function (hookOptions) { throw new configurationCodes.IMS_CONTEXT_MISSING_FIELDS({ messageValues: [configKey, missingKeys.join(', ')] }) } - const metaScopes = config.meta_scopes - if (!metaScopes.includes || !metaScopes.includes(requiredMetaScope)) { - throw new configurationCodes.IMS_CONTEXT_MISSING_METASCOPE({ messageValues: [configKey, requiredMetaScope] }) + if (config.oauth_enabled) { + const oauthScopes = config.scopes + if (!oauthScopes.includes || !requiredScopesForOAuthIntegration.every(scope => oauthScopes.includes(scope))) { + throw new configurationCodes.IMS_CONTEXT_MISSING_OAUTH_SCOPES({ messageValues: [configKey, requiredScopesForOAuthIntegration.join(', ')] }) + } + } else { + const metaScopes = config.meta_scopes + if (!metaScopes.includes || !metaScopes.includes(requiredMetaScopeForJWTIntegration)) { + throw new configurationCodes.IMS_CONTEXT_MISSING_METASCOPE({ messageValues: [configKey, requiredMetaScopeForJWTIntegration] }) + } } } diff --git a/test/hooks/prerun/check-ims-context-config.test.js b/test/hooks/prerun/check-ims-context-config.test.js index f5f233ef..4998a4e1 100644 --- a/test/hooks/prerun/check-ims-context-config.test.js +++ b/test/hooks/prerun/check-ims-context-config.test.js @@ -49,7 +49,7 @@ test('hook -- command from other plugin', async () => { })).not.toThrowError() }) -test('hook -- ok', async () => { +test('hook -- ok with JWT', async () => { setStore({ 'ims.contexts.aio-cli-plugin-cloudmanager': { client_id: 'test-client-id', @@ -67,6 +67,27 @@ test('hook -- ok', async () => { expect(invoke()).not.toThrowError() }) +test('hook -- ok with OAuth', async () => { + setStore({ + 'ims.contexts.aio-cli-plugin-cloudmanager': { + client_id: 'test-client-id', + client_secrets: ['5678'], + ims_org_id: 'someorg@AdobeOrg', + technical_account_id: '4321@techacct.adobe.com', + technical_account_email: 'unused', + scopes: [ + 'openid', + 'AdobeID', + 'read_organizations', + 'additional_info.projectedProductContext', + 'read_pc.dma_aem_ams', + ], + oauth_enabled: true, + }, + }) + expect(invoke()).not.toThrowError() +}) + test('hook -- fully configured cli auth enables cli auth mode', async () => { setStore({ 'ims.contexts.cli': { @@ -272,7 +293,7 @@ test('hook -- missing some fields', async () => { expect(invoke()).toThrowError('One or more of the required fields in ims.contexts.aio-cli-plugin-cloudmanager were not set. Missing keys were technical_account_id, meta_scopes, private_key.') }) -test('hook -- missing scope', async () => { +test('hook -- missing metascope for JWT', async () => { setStore({ 'ims.contexts.aio-cli-plugin-cloudmanager': { client_id: 'test-client-id', @@ -289,6 +310,21 @@ test('hook -- missing scope', async () => { expect(invoke()).toThrowError('The configuration ims.contexts.aio-cli-plugin-cloudmanager is missing the required metascope ent_cloudmgr_sdk.') }) +test('hook -- missing scope for OAuth', async () => { + setStore({ + 'ims.contexts.aio-cli-plugin-cloudmanager': { + client_id: 'test-client-id', + client_secrets: ['5678'], + ims_org_id: 'someorg@AdobeOrg', + technical_account_id: '4321@techacct.adobe.com', + technical_account_email: 'unused', + scopes: [], + oauth_enabled: true, + }, + }) + expect(invoke()).toThrowError('[CloudManagerCLI:IMS_CONTEXT_MISSING_OAUTH_SCOPES] The configuration ims.contexts.aio-cli-plugin-cloudmanager is missing the required OAuth scopes openid, AdobeID, read_organizations, additional_info.projectedProductContext, read_pc.dma_aem_ams') +}) + test('hook -- scope is a number', async () => { setStore({ 'ims.contexts.aio-cli-plugin-cloudmanager': {