Skip to content

Commit

Permalink
MWPW-143580: User validation (#3)
Browse files Browse the repository at this point in the history
- Validates if the user is part of the graybox IAM group
- Only authorized users allowed to trigger the API
  • Loading branch information
sukamat authored Mar 7, 2024
2 parents 7cda802 + b0cfdf0 commit d87f121
Show file tree
Hide file tree
Showing 7 changed files with 130 additions and 25 deletions.
5 changes: 5 additions & 0 deletions .github/workflows/deploy_stage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
Expand All @@ -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 }}
Expand Down
10 changes: 9 additions & 1 deletion actions/appConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 = {
Expand Down Expand Up @@ -162,6 +166,10 @@ class AppConfig {
}
return draftsOnly;
}

ignoreUserCheck() {
return true && this.configMap.ignoreUserCheck;
}
}

module.exports = new AppConfig();
13 changes: 1 addition & 12 deletions actions/graybox/promote-worker.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand All @@ -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();

Expand Down Expand Up @@ -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,
});
Expand Down
17 changes: 7 additions & 10 deletions actions/graybox/promote.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,25 +18,22 @@
// 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();
const ow = openwhisk();
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',
Expand Down
32 changes: 31 additions & 1 deletion actions/graybox/utils.js → actions/graybox/validateAction.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
* from Adobe.
************************************************************************* */

const GrayboxUser = require('../grayboxUser');

function isGrayboxParamsValid(params) {
const {
rootFolder,
Expand All @@ -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
};
74 changes: 74 additions & 0 deletions actions/grayboxUser.js
Original file line number Diff line number Diff line change
@@ -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;
4 changes: 3 additions & 1 deletion app.config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit d87f121

Please sign in to comment.