From a0c4efca51ef244b5625c4fbee8516acfbfb0a68 Mon Sep 17 00:00:00 2001 From: Yemitan Isaiah Olurotimi Date: Tue, 27 Sep 2022 15:29:50 +0200 Subject: [PATCH] feat(cli): Use additional permissions with config-sync command (#2822) * feat(config): include additional permissions when using config-sync * refactor(cli): validate additional oathScope * refactor(cli): implement getPermissions function, update tests * refactor(cli): move getPermission function to transformer.ts * refactor(cli): add validation for additional permission name * refactor(cli): show duplicated permission name in error thrown --- .changeset/dirty-queens-obey.md | 6 + packages/application-config/src/formatters.ts | 1 + .../application-config/src/transformers.ts | 71 ++++++++--- .../application-config/src/validations.ts | 19 +++ .../test/fixtures/config-full.json | 12 ++ .../test/json-schema.spec.js | 39 +++++++ .../test/process-config.spec.js | 110 +++++++++++++----- .../src/utils/get-config-diff.spec.ts | 45 +++++-- 8 files changed, 244 insertions(+), 59 deletions(-) create mode 100644 .changeset/dirty-queens-obey.md diff --git a/.changeset/dirty-queens-obey.md b/.changeset/dirty-queens-obey.md new file mode 100644 index 0000000000..2b096e67a3 --- /dev/null +++ b/.changeset/dirty-queens-obey.md @@ -0,0 +1,6 @@ +--- +'@commercetools-frontend/application-config': patch +'@commercetools-frontend/application-shell': patch +--- + +Include additional permissions when creating and updating custom application diff --git a/packages/application-config/src/formatters.ts b/packages/application-config/src/formatters.ts index e4b09e0e96..2887cc9f5b 100644 --- a/packages/application-config/src/formatters.ts +++ b/packages/application-config/src/formatters.ts @@ -143,4 +143,5 @@ function entryPointUriPathToPermissionKeys( export { entryPointUriPathToResourceAccesses, entryPointUriPathToPermissionKeys, + formatEntryPointUriPathToResourceAccessKey, }; diff --git a/packages/application-config/src/transformers.ts b/packages/application-config/src/transformers.ts index 1ddd45c806..b8d54f0c33 100644 --- a/packages/application-config/src/transformers.ts +++ b/packages/application-config/src/transformers.ts @@ -1,7 +1,14 @@ import type { JSONSchemaForCustomApplicationConfigurationFiles } from './schema'; import type { CustomApplicationData } from './types'; -import { entryPointUriPathToResourceAccesses } from './formatters'; -import { validateEntryPointUriPath, validateSubmenuLinks } from './validations'; +import { + entryPointUriPathToResourceAccesses, + formatEntryPointUriPathToResourceAccessKey, +} from './formatters'; +import { + validateEntryPointUriPath, + validateSubmenuLinks, + validateAdditionalOAuthScopes, +} from './validations'; // The `uriPath` of each submenu link is supposed to be defined relative // to the `entryPointUriPath`. Computing the full path is done internally to keep @@ -18,15 +25,56 @@ const computeUriPath = (uriPath: string, entryPointUriPath: string) => { return `${entryPointUriPath}/${uriPath}`; }; +const getPermissions = ( + appConfig: JSONSchemaForCustomApplicationConfigurationFiles +) => { + const additionalResourceAccessKeyToOauthScopeMap = ( + appConfig.additionalOAuthScopes || [] + ).reduce((previousOauthScope, { name, view, manage }) => { + const formattedResourceKey = + formatEntryPointUriPathToResourceAccessKey(name); + return { + ...previousOauthScope, + [`view${formattedResourceKey}`]: view, + [`manage${formattedResourceKey}`]: manage, + }; + }, {} as Record); + + const additionalPermissionNames = + appConfig.additionalOAuthScopes?.map(({ name }) => name) || []; + + const permissionKeys = entryPointUriPathToResourceAccesses( + appConfig.entryPointUriPath, + additionalPermissionNames + ) as Record; + + const additionalPermissions = Object.keys( + additionalResourceAccessKeyToOauthScopeMap + ).map((additionalResourceAccessKey) => ({ + name: permissionKeys[additionalResourceAccessKey], + oAuthScopes: + additionalResourceAccessKeyToOauthScopeMap[additionalResourceAccessKey], + })); + + return [ + { + name: permissionKeys.view, + oAuthScopes: appConfig.oAuthScopes.view, + }, + { + name: permissionKeys.manage, + oAuthScopes: appConfig.oAuthScopes.manage, + }, + ...additionalPermissions, + ]; +}; + function transformCustomApplicationConfigToData( appConfig: JSONSchemaForCustomApplicationConfigurationFiles ): CustomApplicationData { validateEntryPointUriPath(appConfig); validateSubmenuLinks(appConfig); - - const permissionKeys = entryPointUriPathToResourceAccesses( - appConfig.entryPointUriPath - ); + validateAdditionalOAuthScopes(appConfig); return { id: appConfig.env.production.applicationId, @@ -34,16 +82,7 @@ function transformCustomApplicationConfigToData( description: appConfig.description, entryPointUriPath: appConfig.entryPointUriPath, url: appConfig.env.production.url, - permissions: [ - { - name: permissionKeys.view, - oAuthScopes: appConfig.oAuthScopes.view, - }, - { - name: permissionKeys.manage, - oAuthScopes: appConfig.oAuthScopes.manage, - }, - ], + permissions: getPermissions(appConfig), icon: appConfig.icon, mainMenuLink: appConfig.mainMenuLink, submenuLinks: appConfig.submenuLinks.map((submenuLink) => ({ diff --git a/packages/application-config/src/validations.ts b/packages/application-config/src/validations.ts index 6417700143..a780d07174 100644 --- a/packages/application-config/src/validations.ts +++ b/packages/application-config/src/validations.ts @@ -70,3 +70,22 @@ export const validateSubmenuLinks = ( uriPathSet.add(uriPath); }); }; + +export const validateAdditionalOAuthScopes = ( + config: JSONSchemaForCustomApplicationConfigurationFiles +) => { + const additionalPermissionNames = new Set(); + config.additionalOAuthScopes?.forEach(({ name }) => { + if (additionalPermissionNames.has(name)) { + throw new Error( + `Duplicate additional permission "${name}". Every additional permission must have a unique name` + ); + } + if (!name.match(ENTRY_POINT_URI_PATH_REGEX)) { + throw new Error( + `Additional permission name "${name}" is invalid. The value may be between 2 and 64 characters and only contain alphanumeric lowercase characters, non-consecutive underscores and hyphens. Leading and trailing underscore and hyphens are also not allowed` + ); + } + additionalPermissionNames.add(name); + }); +}; diff --git a/packages/application-config/test/fixtures/config-full.json b/packages/application-config/test/fixtures/config-full.json index 4e39eb4986..7fda3f35ac 100644 --- a/packages/application-config/test/fixtures/config-full.json +++ b/packages/application-config/test/fixtures/config-full.json @@ -20,6 +20,18 @@ "view": ["view_products"], "manage": [] }, + "additionalOAuthScopes": [ + { + "name": "movies", + "view": ["view_products"], + "manage": [] + }, + { + "name": "merch", + "view": ["view_channels"], + "manage": ["manage_channels"] + } + ], "headers": { "csp": { "script-src": ["https://track.avengers.app"], diff --git a/packages/application-config/test/json-schema.spec.js b/packages/application-config/test/json-schema.spec.js index 9d4572c1d6..2f34282bdb 100644 --- a/packages/application-config/test/json-schema.spec.js +++ b/packages/application-config/test/json-schema.spec.js @@ -2,6 +2,7 @@ import { validateConfig, validateEntryPointUriPath, validateSubmenuLinks, + validateAdditionalOAuthScopes, } from '../src/validations'; import fixtureConfigSimple from './fixtures/config-simple.json'; import fixtureConfigFull from './fixtures/config-full.json'; @@ -252,4 +253,42 @@ describe('invalid configurations', () => { `"Duplicate URI path. Every submenu link must have a unique URI path value"` ); }); + it('should validate that additionalOauthScope name is unique', () => { + expect(() => + validateAdditionalOAuthScopes({ + ...fixtureConfigSimple, + additionalOAuthScopes: [ + { + name: 'movies', + view: ['view_products'], + manage: [], + }, + + { + name: 'movies', + view: ['view_channels'], + manage: [], + }, + ], + }) + ).toThrowErrorMatchingInlineSnapshot( + `"Duplicate additional permission \\"movies\\". Every additional permission must have a unique name"` + ); + }); + it('should validate the additional permission names matches the entry point regex', () => { + expect(() => + validateAdditionalOAuthScopes({ + ...fixtureConfigSimple, + additionalOAuthScopes: [ + { + name: '-movies', + view: ['view_products'], + manage: [], + }, + ], + }) + ).toThrowErrorMatchingInlineSnapshot( + `"Additional permission name \\"-movies\\" is invalid. The value may be between 2 and 64 characters and only contain alphanumeric lowercase characters, non-consecutive underscores and hyphens. Leading and trailing underscore and hyphens are also not allowed"` + ); + }); }); diff --git a/packages/application-config/test/process-config.spec.js b/packages/application-config/test/process-config.spec.js index fa1e0587ac..acbe87f8dd 100644 --- a/packages/application-config/test/process-config.spec.js +++ b/packages/application-config/test/process-config.spec.js @@ -305,14 +305,12 @@ describe('processing a full config', () => { entryPointUriPath: 'avengers', url: 'https://avengers.app', permissions: [ - { - name: 'viewAvengers', - oAuthScopes: ['view_products'], - }, - { - name: 'manageAvengers', - oAuthScopes: [], - }, + { name: 'viewAvengers', oAuthScopes: ['view_products'] }, + { name: 'manageAvengers', oAuthScopes: [] }, + { name: 'viewAvengersMovies', oAuthScopes: ['view_products'] }, + { name: 'manageAvengersMovies', oAuthScopes: [] }, + { name: 'viewAvengersMerch', oAuthScopes: ['view_channels'] }, + { name: 'manageAvengersMerch', oAuthScopes: ['manage_channels'] }, ], icon: '', mainMenuLink: { @@ -350,6 +348,17 @@ describe('processing a full config', () => { oAuthScopes: { view: ['view_products'], }, + additionalOAuthScopes: [ + { + name: 'movies', + view: ['view_products'], + }, + { + name: 'merch', + view: ['view_channels'], + manage: ['manage_channels'], + }, + ], }, }, }, @@ -389,14 +398,12 @@ describe('processing a full config', () => { entryPointUriPath: 'avengers', url: 'https://avengers.app', permissions: [ - { - name: 'viewAvengers', - oAuthScopes: ['view_products'], - }, - { - name: 'manageAvengers', - oAuthScopes: [], - }, + { name: 'viewAvengers', oAuthScopes: ['view_products'] }, + { name: 'manageAvengers', oAuthScopes: [] }, + { name: 'viewAvengersMovies', oAuthScopes: ['view_products'] }, + { name: 'manageAvengersMovies', oAuthScopes: [] }, + { name: 'viewAvengersMerch', oAuthScopes: ['view_channels'] }, + { name: 'manageAvengersMerch', oAuthScopes: ['manage_channels'] }, ], icon: '', mainMenuLink: { @@ -462,14 +469,12 @@ describe('processing a full config', () => { entryPointUriPath: 'avengers', url: 'https://avengers.app', permissions: [ - { - name: 'viewAvengers', - oAuthScopes: ['view_products'], - }, - { - name: 'manageAvengers', - oAuthScopes: [], - }, + { name: 'viewAvengers', oAuthScopes: ['view_products'] }, + { name: 'manageAvengers', oAuthScopes: [] }, + { name: 'viewAvengersMovies', oAuthScopes: ['view_products'] }, + { name: 'manageAvengersMovies', oAuthScopes: [] }, + { name: 'viewAvengersMerch', oAuthScopes: ['view_channels'] }, + { name: 'manageAvengersMerch', oAuthScopes: ['manage_channels'] }, ], icon: '', mainMenuLink: { @@ -507,6 +512,17 @@ describe('processing a full config', () => { oAuthScopes: { view: ['view_products'], }, + additionalOAuthScopes: [ + { + name: 'movies', + view: ['view_products'], + }, + { + name: 'merch', + view: ['view_channels'], + manage: ['manage_channels'], + }, + ], }, }, }, @@ -548,14 +564,12 @@ describe('processing a full config', () => { entryPointUriPath: 'avengers', url: 'https://avengers.app', permissions: [ - { - name: 'viewAvengers', - oAuthScopes: ['view_products'], - }, - { - name: 'manageAvengers', - oAuthScopes: [], - }, + { name: 'viewAvengers', oAuthScopes: ['view_products'] }, + { name: 'manageAvengers', oAuthScopes: [] }, + { name: 'viewAvengersMovies', oAuthScopes: ['view_products'] }, + { name: 'manageAvengersMovies', oAuthScopes: [] }, + { name: 'viewAvengersMerch', oAuthScopes: ['view_channels'] }, + { name: 'manageAvengersMerch', oAuthScopes: ['manage_channels'] }, ], icon: '', mainMenuLink: { @@ -1111,6 +1125,22 @@ describe('processing a config with OIDC', () => { name: 'manageAvengers', oAuthScopes: ['manage_orders'], }, + { + name: 'viewAvengersMovies', + oAuthScopes: ['view_products'], + }, + { + name: 'manageAvengersMovies', + oAuthScopes: [], + }, + { + name: 'viewAvengersMerch', + oAuthScopes: [], + }, + { + name: 'manageAvengersMerch', + oAuthScopes: ['manage_channels'], + }, ], icon: '', mainMenuLink: { @@ -1197,6 +1227,22 @@ describe('processing a config with OIDC', () => { name: 'manageAvengers', oAuthScopes: ['manage_orders'], }, + { + name: 'viewAvengersMovies', + oAuthScopes: ['view_products'], + }, + { + name: 'manageAvengersMovies', + oAuthScopes: [], + }, + { + name: 'viewAvengersMerch', + oAuthScopes: [], + }, + { + name: 'manageAvengersMerch', + oAuthScopes: ['manage_channels'], + }, ], icon: '', mainMenuLink: { diff --git a/packages/mc-scripts/src/utils/get-config-diff.spec.ts b/packages/mc-scripts/src/utils/get-config-diff.spec.ts index 03644c2482..6f2cd1e32b 100644 --- a/packages/mc-scripts/src/utils/get-config-diff.spec.ts +++ b/packages/mc-scripts/src/utils/get-config-diff.spec.ts @@ -50,36 +50,59 @@ describe('when there are changes', () => { const oldConfig = createTestConfig({ permissions: [ { - oAuthScopes: ['manage_product'], - name: 'manageMyTestApp', + name: 'viewMyTestApp', + oAuthScopes: ['view_products', 'view_customers'], }, + { name: 'manageMyTestApp', oAuthScopes: ['manage_product'] }, { - oAuthScopes: ['view_products', 'view_customers'], - name: 'viewMyTestApp', + name: 'viewMyTestAppMovies', + oAuthScopes: ['view_products'], + }, + { name: 'manageMyTestAppMovies', oAuthScopes: [] }, + { + name: 'viewMyTestAppMerch', + oAuthScopes: ['view_channels'], }, + { name: 'manageMyTestAppMerch', oAuthScopes: [] }, ], }); const newConfig = createTestConfig({ permissions: [ { - oAuthScopes: ['manage_customer'], + name: 'viewMyTestApp', + oAuthScopes: ['view_products', 'view_channel'], + }, + { name: 'manageMyTestApp', + oAuthScopes: ['manage_product', 'manage_channel'], }, { - oAuthScopes: ['view_products', 'view_customers', 'view_orders'], - name: 'viewMyTestApp', + name: 'viewMyTestAppMovies', + oAuthScopes: ['view_products', 'view_channel'], }, + { name: 'manageMyTestAppMovies', oAuthScopes: [] }, + { + name: 'viewMyTestAppCharacters', + oAuthScopes: ['view_channels'], + }, + { name: 'manageMyTestAppCharacters', oAuthScopes: [] }, ], }); expect(getConfigDiff(oldConfig, newConfig)).toMatchInlineSnapshot(` "permissions changed - \\"manageMyTestApp\\" changed - oauth scope added: manage_customer - oauth scope removed: manage_product \\"viewMyTestApp\\" changed - oauth scope added: view_orders" + oauth scope added: view_channel + oauth scope removed: view_customers + \\"manageMyTestApp\\" changed + oauth scope added: manage_channel + \\"viewMyTestAppMovies\\" changed + oauth scope added: view_channel + \\"viewMyTestAppCharacters\\" was added + \\"manageMyTestAppCharacters\\" was added + \\"viewMyTestAppMerch\\" was removed + \\"manageMyTestAppMerch\\" was removed" `); });