From cc4d55e3599ebf548ec86d11db6678cbe2639975 Mon Sep 17 00:00:00 2001 From: sukamat Date: Mon, 4 Mar 2024 16:02:25 -0800 Subject: [PATCH 1/7] app config update to read new configs --- actions/appConfig.js | 4 +++- app.config.yaml | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/actions/appConfig.js b/actions/appConfig.js index ed39cf9..8389bf5 100644 --- a/actions/appConfig.js +++ b/actions/appConfig.js @@ -46,7 +46,7 @@ class AppConfig { payload.draftsOnly = params.draftsOnly; payload.experienceName = params.experienceName; - // These are from configs and not activation related + // These are from params set in the github configs this.configMap.spSite = params.spSite; this.configMap.spClientId = params.spClientId; this.configMap.spAuthority = params.spAuthority; @@ -56,6 +56,8 @@ class AppConfig { this.configMap.certKey = params.certKey; this.configMap.certThumbprint = params.certThumbprint; this.configMap.helixAdminApiKeys = this.getJsonFromStr(params.helixAdminApiKeys); + this.configMap.groupCheckUrl = params.groupCheckUrl || 'https://graph.microsoft.com/v1.0/groups/{groupOid}/members?$count=true'; + this.configMap.grayboxUserGroups = this.getJsonFromStr(params.grayboxUserGroups, []); this.extractPrivateKey(); payload.ext = { diff --git a/app.config.yaml b/app.config.yaml index 581db7f..d87026c 100644 --- a/app.config.yaml +++ b/app.config.yaml @@ -12,7 +12,9 @@ application: tenantId: $TENANT_ID certPassword: $CERT_PASSWORD certKey: $CERT_KEY - certThumbprint: $CERT_THUMB_PRINT + certThumbprint: $CERT_THUMB_PRINT + groupCheckUrl: $GROUP_CHECK_URL + grayboxUserGroups: $GRAYBOX_USER_GROUPS actions: promote: function: actions/graybox/promote.js From 2ecd4799de8749f832cbc1f3b6ec1cc143474b0f Mon Sep 17 00:00:00 2001 From: sukamat Date: Mon, 4 Mar 2024 16:05:59 -0800 Subject: [PATCH 2/7] rename utils.js to validateAction.js --- .../graybox/{utils.js => validateAction.js} | 29 ++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) rename actions/graybox/{utils.js => validateAction.js} (62%) diff --git a/actions/graybox/utils.js b/actions/graybox/validateAction.js similarity index 62% rename from actions/graybox/utils.js rename to actions/graybox/validateAction.js index 4fa71b9..9c0e307 100644 --- a/actions/graybox/utils.js +++ b/actions/graybox/validateAction.js @@ -15,6 +15,8 @@ * from Adobe. ************************************************************************* */ +const GrayboxUser = require('../grayboxUser'); + function isGrayboxParamsValid(params) { const { rootFolder, @@ -34,6 +36,31 @@ function isGrayboxParamsValid(params) { return !requiredParams.some((param) => !param); } +async function isUserAuthorized(params, grpIds) { + const { spToken } = params; + const grayboxUser = new GrayboxUser({ at: spToken }); + const found = await grayboxUser.isInGroups(grpIds); + return found; +} + +async function validateAction(params, grpIds) { + if (!isGrayboxParamsValid(params)) { + return { + code: 400, + payload: 'Required data is not available to proceed with Graybox Promote action.' + }; + } + if (!await isUserAuthorized(params, grpIds)) { + return { + code: 401, + payload: 'Additional permissions required to proceed with Graybox Promote action.' + }; + } + return { + code: 200 + }; +} + module.exports = { - isGrayboxParamsValid + validateAction }; From a5a17c6c77246ef6990150ce894fcc68cc133408 Mon Sep 17 00:00:00 2001 From: sukamat Date: Mon, 4 Mar 2024 16:06:37 -0800 Subject: [PATCH 3/7] define grayboxuser --- actions/grayboxUser.js | 74 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 actions/grayboxUser.js diff --git a/actions/grayboxUser.js b/actions/grayboxUser.js new file mode 100644 index 0000000..eaa03e1 --- /dev/null +++ b/actions/grayboxUser.js @@ -0,0 +1,74 @@ +/* *********************************************************************** + * ADOBE CONFIDENTIAL + * ___________________ + * + * Copyright 2024 Adobe + * All Rights Reserved. + * + * NOTICE: All information contained herein is, and remains + * the property of Adobe and its suppliers, if any. The intellectual + * and technical concepts contained herein are proprietary to Adobe + * and its suppliers and are protected by all applicable intellectual + * property laws, including trade secret and copyright laws. + * Dissemination of this information or reproduction of this material + * is strictly forbidden unless prior written permission is obtained + * from Adobe. + ************************************************************************* */ + +const fetch = require('node-fetch'); +const sharepointAuth = require('./sharepointAuth'); +const appConfig = require('./appConfig'); +const { getAioLogger } = require('./utils'); + +const logger = getAioLogger(); + +/** + * GrayboxUser is based on the SP token and is used to check if the user is part of the required groups. + * It uses the graph API to check the group membership of the user based on the group OID and user OID. + * The group OID is configured in github env configs and read in the appConfig. + * The user OID is obtained from the SP token. + */ +class GrayboxUser { + constructor({ at }) { + this.at = at; + this.userDetails = sharepointAuth.getUserDetails(at); + this.userOid = this.userDetails?.oid; + } + + /** + * Check if the user is part of the required groups. + * @param {Array} grpIds - Array of group OIDs + */ + async isInGroups(grpIds) { + if (!grpIds?.length) return false; + const appAt = await sharepointAuth.getAccessToken(); + // eslint-disable-next-line max-len + const numGrps = grpIds.length; + let url = appConfig.getConfig().groupCheckUrl || ''; + url += `&$filter=id eq '${this.userOid}'`; + let found = false; + for (let c = 0; c < numGrps; c += 1) { + const grpUrl = url.replace('{groupOid}', grpIds[c]); + logger.debug(`isInGroups-URL- ${grpUrl}`); + // eslint-disable-next-line no-await-in-loop + found = await fetch(grpUrl, { + headers: { + Authorization: `Bearer ${appAt}` + } + }).then((d) => d.json()).then((d1) => { + if (d1.error) { + // When user does not have access to group an error is also returned + logger.debug(`Error while getting member info ${JSON.stringify(d1)}`); + } + return d1?.value?.length && true; + }).catch((err) => { + logger.warn(err); + return false; + }); + if (found) break; + } + return found === true; + } +} + +module.exports = GrayboxUser; From 80c910c5fc46549cbfa02587e557b3f20e440e0d Mon Sep 17 00:00:00 2001 From: sukamat Date: Mon, 4 Mar 2024 16:06:56 -0800 Subject: [PATCH 4/7] refactor promote-worker --- actions/graybox/promote-worker.js | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/actions/graybox/promote-worker.js b/actions/graybox/promote-worker.js index c0b8d4d..ffa6c41 100644 --- a/actions/graybox/promote-worker.js +++ b/actions/graybox/promote-worker.js @@ -16,7 +16,6 @@ ************************************************************************* */ const { getAioLogger, isFilePatternMatched, toUTCStr } = require('../utils'); -const { isGrayboxParamsValid } = require('./utils'); const appConfig = require('../appConfig'); const { getConfig } = require('../config'); const { getAuthorizedRequestOption, fetchWithRetry, updateExcelTable } = require('../sharepoint'); @@ -26,18 +25,8 @@ const MAX_CHILDREN = 1000; const IS_GRAYBOX = true; async function main(params) { - let responsePayload; logger.info('Graybox Promote Worker invoked'); - if (!isGrayboxParamsValid(params)) { - responsePayload = 'Required data is not available to proceed with Graybox Promote action.'; - logger.error(responsePayload); - return exitAction({ - code: 400, - payload: responsePayload - }); - } - appConfig.setAppConfig(params); const { gbRootFolder, experienceName } = appConfig.getPayload(); @@ -66,7 +55,7 @@ async function main(params) { await updateExcelTable(projectExcelPath, 'PROMOTE_STATUS', excelValues, IS_GRAYBOX); logger.info('Project excel file updated with promote status.'); - responsePayload = 'Graybox Promote Worker action completed.'; + const responsePayload = 'Graybox Promote Worker action completed.'; return exitAction({ body: responsePayload, }); From 3b85fcaad9b13c0f2e34bc9d6d08f30736a49430 Mon Sep 17 00:00:00 2001 From: sukamat Date: Mon, 4 Mar 2024 16:07:13 -0800 Subject: [PATCH 5/7] Validate user before proceeding --- actions/graybox/promote.js | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/actions/graybox/promote.js b/actions/graybox/promote.js index 08994ce..de795da 100644 --- a/actions/graybox/promote.js +++ b/actions/graybox/promote.js @@ -18,8 +18,8 @@ // eslint-disable-next-line import/no-extraneous-dependencies const openwhisk = require('openwhisk'); const { getAioLogger } = require('../utils'); +const { validateAction } = require('./validateAction'); const appConfig = require('../appConfig'); -const { isGrayboxParamsValid } = require('./utils'); async function main(params) { const logger = getAioLogger(); @@ -27,16 +27,13 @@ async function main(params) { let responsePayload; logger.info('Graybox Promote action invoked'); try { - if (!isGrayboxParamsValid(params)) { - responsePayload = 'Required data is not available to proceed with Graybox Promote action.'; - logger.error(responsePayload); - return exitAction({ - code: 400, - payload: responsePayload - }); - } - appConfig.setAppConfig(params); + const grpIds = appConfig.getConfig().grayboxUserGroups; + const vActData = await validateAction(params, grpIds); + if (vActData && vActData.code !== 200) { + logger.info(`Validation failed: ${JSON.stringify(vActData)}`); + return exitAction(vActData); + } return exitAction(ow.actions.invoke({ name: 'graybox/promote-worker', From bc7063d047d9abe9ba9eb715eeea2431a27e335c Mon Sep 17 00:00:00 2001 From: sukamat Date: Tue, 5 Mar 2024 13:12:16 -0800 Subject: [PATCH 6/7] Add ignore user validation check --- actions/appConfig.js | 6 ++++++ actions/graybox/promote.js | 2 +- actions/graybox/validateAction.js | 15 +++++++++------ 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/actions/appConfig.js b/actions/appConfig.js index 8389bf5..63694c9 100644 --- a/actions/appConfig.js +++ b/actions/appConfig.js @@ -58,6 +58,8 @@ class AppConfig { this.configMap.helixAdminApiKeys = this.getJsonFromStr(params.helixAdminApiKeys); this.configMap.groupCheckUrl = params.groupCheckUrl || 'https://graph.microsoft.com/v1.0/groups/{groupOid}/members?$count=true'; this.configMap.grayboxUserGroups = this.getJsonFromStr(params.grayboxUserGroups, []); + this.configMap.ignoreUserCheck = (params.ignoreUserCheck || '').trim().toLowerCase() === 'true'; + this.extractPrivateKey(); payload.ext = { @@ -164,6 +166,10 @@ class AppConfig { } return draftsOnly; } + + ignoreUserCheck() { + return true && this.configMap.ignoreUserCheck; + } } module.exports = new AppConfig(); diff --git a/actions/graybox/promote.js b/actions/graybox/promote.js index de795da..54aef9e 100644 --- a/actions/graybox/promote.js +++ b/actions/graybox/promote.js @@ -29,7 +29,7 @@ async function main(params) { try { appConfig.setAppConfig(params); const grpIds = appConfig.getConfig().grayboxUserGroups; - const vActData = await validateAction(params, grpIds); + const vActData = await validateAction(params, grpIds, appConfig.ignoreUserCheck()); if (vActData && vActData.code !== 200) { logger.info(`Validation failed: ${JSON.stringify(vActData)}`); return exitAction(vActData); diff --git a/actions/graybox/validateAction.js b/actions/graybox/validateAction.js index 9c0e307..8200afa 100644 --- a/actions/graybox/validateAction.js +++ b/actions/graybox/validateAction.js @@ -43,18 +43,21 @@ async function isUserAuthorized(params, grpIds) { return found; } -async function validateAction(params, grpIds) { +async function validateAction(params, grpIds, ignoreUserCheck = false) { if (!isGrayboxParamsValid(params)) { return { code: 400, payload: 'Required data is not available to proceed with Graybox Promote action.' }; } - if (!await isUserAuthorized(params, grpIds)) { - return { - code: 401, - payload: 'Additional permissions required to proceed with Graybox Promote action.' - }; + if (!ignoreUserCheck) { + const isUserAuth = await isUserAuthorized(params, grpIds); + if (!isUserAuth) { + return { + code: 401, + payload: 'User is not authorized to perform this action.' + }; + } } return { code: 200 From 29114a2f8821dcb208dd498e26eae0eeb1d8ac56 Mon Sep 17 00:00:00 2001 From: sukamat Date: Wed, 6 Mar 2024 13:43:12 -0800 Subject: [PATCH 7/7] stage deploy script updated to read env vars for group check --- .github/workflows/deploy_stage.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/deploy_stage.yml b/.github/workflows/deploy_stage.yml index d10b717..14968b7 100644 --- a/.github/workflows/deploy_stage.yml +++ b/.github/workflows/deploy_stage.yml @@ -18,6 +18,9 @@ jobs: run: | echo "GROUP_CHECK_URL : $GROUP_CHECK_URL" echo "GRAYBOX_USER_GROUPS : $GRAYBOX_USER_GROUPS" + env: + GROUP_CHECK_URL: ${{ vars.GROUP_CHECK_URL }} + GRAYBOX_USER_GROUPS: ${{ vars.GRAYBOX_USER_GROUPS }} - name: Checkout uses: actions/checkout@v4 - name: Use Node.js ${{ matrix.node-version }} @@ -35,6 +38,8 @@ jobs: env: AIO_RUNTIME_NAMESPACE: ${{ secrets.AIO_RUNTIME_NAMESPACE_STAGE }} AIO_RUNTIME_AUTH: ${{ secrets.AIO_RUNTIME_AUTH_STAGE }} + GROUP_CHECK_URL: ${{ vars.GROUP_CHECK_URL }} + GRAYBOX_USER_GROUPS: ${{ vars.GRAYBOX_USER_GROUPS }} uses: adobe/aio-apps-action@3.3.0 with: os: ${{ matrix.os }}