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 }} diff --git a/actions/appConfig.js b/actions/appConfig.js index ed39cf9..63694c9 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,10 @@ 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.configMap.ignoreUserCheck = (params.ignoreUserCheck || '').trim().toLowerCase() === 'true'; + this.extractPrivateKey(); payload.ext = { @@ -162,6 +166,10 @@ class AppConfig { } return draftsOnly; } + + ignoreUserCheck() { + return true && this.configMap.ignoreUserCheck; + } } module.exports = new AppConfig(); 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, }); diff --git a/actions/graybox/promote.js b/actions/graybox/promote.js index 08994ce..54aef9e 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, appConfig.ignoreUserCheck()); + if (vActData && vActData.code !== 200) { + logger.info(`Validation failed: ${JSON.stringify(vActData)}`); + return exitAction(vActData); + } return exitAction(ow.actions.invoke({ name: 'graybox/promote-worker', diff --git a/actions/graybox/utils.js b/actions/graybox/validateAction.js similarity index 59% rename from actions/graybox/utils.js rename to actions/graybox/validateAction.js index 4fa71b9..8200afa 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,34 @@ 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, ignoreUserCheck = false) { + if (!isGrayboxParamsValid(params)) { + return { + code: 400, + payload: 'Required data is not available 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 + }; +} + module.exports = { - isGrayboxParamsValid + validateAction }; 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; 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