Skip to content

Commit

Permalink
Merge 53f8e96 into 66acf9b
Browse files Browse the repository at this point in the history
  • Loading branch information
kark authored Nov 24, 2022
2 parents 66acf9b + 53f8e96 commit f5ee525
Show file tree
Hide file tree
Showing 18 changed files with 423 additions and 62 deletions.
11 changes: 11 additions & 0 deletions .changeset/fresh-coins-shout.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
'@commercetools-frontend/application-config': minor
'@commercetools-frontend/application-shell': minor
'@commercetools-frontend/constants': minor
'@commercetools-frontend/cypress': minor
'@commercetools-frontend/mc-scripts': minor
---

Enable configuring granular permissions in Custom Applications.

Additional permissions are defined by adding permission groups in the Custom Application config. [See docs](https://docs.commercetools.com/custom-applications/concepts/oauth-scopes-and-user-permissions#permission-groups).
37 changes: 37 additions & 0 deletions packages/application-config/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,43 @@
"additionalProperties": false,
"required": ["view", "manage"]
},
"additionalOAuthScopes": {
"description": "See https://docs.commercetools.com/custom-applications/api-reference/application-config#additionaloauthscopes",
"type": "array",
"default": [],
"uniqueItems": true,
"items": {
"type": "object",
"properties": {
"name": {
"description": "See https://docs.commercetools.com/custom-applications/api-reference/application-config#additionaloauthscopesname",
"type": "string"
},
"view": {
"description": "See https://docs.commercetools.com/custom-applications/api-reference/application-config#additionaloauthscopesview",
"type": "array",
"default": [],
"items": {
"type": "string",
"pattern": "view_(.*)"
},
"uniqueItems": true
},
"manage": {
"description": "See https://docs.commercetools.com/custom-applications/api-reference/application-config#additionaloauthscopesmanage",
"type": "array",
"default": [],
"items": {
"type": "string",
"pattern": "manage_(.*)"
},
"uniqueItems": true
}
},
"additionalProperties": false,
"required": ["name", "view", "manage"]
}
},
"env": {
"description": "See https://docs.commercetools.com/custom-applications/api-reference/application-config#env",
"type": "object",
Expand Down
8 changes: 7 additions & 1 deletion packages/application-config/src/constants.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
/**
* The entryPointUriPath may be between 2 and 64 characters and only contain alphanumeric lowercase characters,
* The entryPointUriPath may be between 2 and 64 characters and only contain alphabetic lowercase characters,
* non-consecutive underscores and hyphens. Leading and trailing underscore and hyphens are also not allowed.
*/
export const ENTRY_POINT_URI_PATH_REGEX =
/^[^-_#]([0-9a-z]|[-_](?![-_])){0,62}[^-_#]$/g;

/**
* The permission group name may be between 2 and 64 characters and only contain alphanumeric lowercase characters and non-consecutive hyphens. Leading and trailing hyphens are also not allowed.
*/
export const PERMISSION_GROUP_NAME_REGEX =
/^[^-#]([a-z]|[-](?![-])){0,62}[^-#]$/g;

export const CLOUD_IDENTIFIERS = {
GCP_AU: 'gcp-au',
GCP_EU: 'gcp-eu',
Expand Down
2 changes: 2 additions & 0 deletions packages/application-config/src/formatters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,4 +143,6 @@ function entryPointUriPathToPermissionKeys<PermissionGroupName extends string>(
export {
entryPointUriPathToResourceAccesses,
entryPointUriPathToPermissionKeys,
formatEntryPointUriPathToResourceAccessKey,
formatPermissionGroupNameToResourceAccessKey,
};
1 change: 1 addition & 0 deletions packages/application-config/src/process-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ const processConfig = ({
: appConfig.env.development.initialProjectKey,
teamId: appConfig.env.development?.teamId,
oAuthScopes: appConfig.oAuthScopes,
additionalOAuthScopes: appConfig?.additionalOAuthScopes,
}),
menuLinks: {
icon: customApplicationData.icon,
Expand Down
17 changes: 17 additions & 0 deletions packages/application-config/src/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,23 @@ export interface JSONSchemaForCustomApplicationConfigurationFiles {
*/
manage: string[];
};
/**
* See https://docs.commercetools.com/custom-applications/api-reference/application-config#additionaloauthscopes
*/
additionalOAuthScopes?: {
/**
* See https://docs.commercetools.com/custom-applications/api-reference/application-config#additionaloauthscopesname
*/
name: string;
/**
* See https://docs.commercetools.com/custom-applications/api-reference/application-config#additionaloauthscopesview
*/
view: string[];
/**
* See https://docs.commercetools.com/custom-applications/api-reference/application-config#additionaloauthscopesmanage
*/
manage: string[];
}[];
/**
* See https://docs.commercetools.com/custom-applications/api-reference/application-config#env
*/
Expand Down
71 changes: 55 additions & 16 deletions packages/application-config/src/transformers.ts
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -18,32 +25,64 @@ 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<string, string[]>);

const additionalPermissionNames =
appConfig.additionalOAuthScopes?.map(({ name }) => name) || [];

const permissionKeys = entryPointUriPathToResourceAccesses(
appConfig.entryPointUriPath,
additionalPermissionNames
) as Record<string, string>;

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,
name: appConfig.name,
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) => ({
Expand Down
26 changes: 24 additions & 2 deletions packages/application-config/src/validations.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import Ajv, { type ErrorObject } from 'ajv';
import schemaJson from '../schema.json';
import { ENTRY_POINT_URI_PATH_REGEX } from './constants';
import {
ENTRY_POINT_URI_PATH_REGEX,
PERMISSION_GROUP_NAME_REGEX,
} from './constants';
import type { JSONSchemaForCustomApplicationConfigurationFiles } from './schema';

type ErrorAdditionalProperty = ErrorObject<
Expand Down Expand Up @@ -52,7 +55,7 @@ export const validateEntryPointUriPath = (
) => {
if (!config.entryPointUriPath.match(ENTRY_POINT_URI_PATH_REGEX)) {
throw new Error(
'Invalid "entryPointUriPath". 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.'
'Invalid "entryPointUriPath". The value may be between 2 and 64 characters and only contain alphanumeric lowercase characters, non-consecutive underscores and hyphens. Leading and trailing underscores and hyphens are also not allowed.'
);
}
};
Expand All @@ -70,3 +73,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 group name "${name}". Every additional permission must have a unique name`
);
}
if (!name.match(PERMISSION_GROUP_NAME_REGEX)) {
throw new Error(
`Additional permission group name "${name}" is invalid. The value may be between 2 and 64 characters and only contain alphabetic lowercase characters and non-consecutive hyphens. Leading and trailing hyphens are also not allowed`
);
}
additionalPermissionNames.add(name);
});
};
12 changes: 12 additions & 0 deletions packages/application-config/test/fixtures/config-full.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
Expand Down
12 changes: 12 additions & 0 deletions packages/application-config/test/fixtures/config-oidc.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,18 @@
"view": ["view_orders", "view_states"],
"manage": ["manage_orders"]
},
"additionalOAuthScopes": [
{
"name": "movies",
"view": ["view_products"],
"manage": []
},
{
"name": "merch",
"view": [],
"manage": ["manage_channels"]
}
],
"icon": "<svg><path fill=\"#000000\" /></svg>",
"mainMenuLink": {
"defaultLabel": "Avengers",
Expand Down
Loading

0 comments on commit f5ee525

Please sign in to comment.