diff --git a/.circleci/config.yml b/.circleci/config.yml index 0f2510af..b4f8d839 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -69,7 +69,7 @@ workflows: context: org-global filters: branches: - only: ["develop", "PLAT-3383"] + only: ["develop", "PM-809_artifact-endpoint-update"] - "build-prod": context: org-global filters: diff --git a/src/common/helper.js b/src/common/helper.js index 6391d79d..26e48fab 100755 --- a/src/common/helper.js +++ b/src/common/helper.js @@ -14,6 +14,7 @@ const errors = require('common-errors') const { validate: uuidValidate } = require('uuid') const NodeCache = require('node-cache') const { axiosInstance } = require('./axiosInstance') +const { UserRoles, ProjectRoles } = require('../constants') AWS.config.region = config.get('aws.AWS_REGION') const s3 = new AWS.S3() @@ -312,6 +313,44 @@ function setPaginationHeaders (req, res, data) { res.json(data.rows) } +/** + * Get challenge resources + * @param {String} challengeId the challenge id + * @param {String} userId specific userId for which to check roles + */ +const getChallengeResources = async (challengeId, userId) => { + let resourcesResponse + + // Get map of role id to role name + const resourceRolesMap = await getRoleIdToRoleNameMap() + + // Check if role id to role name mapping is available. If not user's role cannot be determined. + if (resourceRolesMap == null || _.size(resourceRolesMap) === 0) { + throw new errors.HttpStatusError(503, `Could not determine the user's role in the challenge with id ${challengeId}`) + } + + const resourcesUrl = `${config.RESOURCEAPI_V5_BASE_URL}/resources?challengeId=${challengeId}${userId ? `&memberId=${userId}` : ''}` + try { + resourcesResponse = _.get(await axiosInstance.get(resourcesUrl), 'data', []) + } catch (ex) { + logger.error(`Error while accessing ${resourcesUrl}`) + throw new errors.HttpStatusError(503, `Could not determine the user's role in the challenge with id ${challengeId}`) + } + + const resources = {} + _.each((resourcesResponse || []), (resource) => { + if (!resources[resource.memberId]) { + resources[resource.memberId] = { + memberId: resource.memberId, + memberHandle: resource.memberHandle, + roles: [] + } + } + resources[resource.memberId].roles.push(resourceRolesMap[resource.roleId]) + }) + return resources +} + /** * Function to get challenge by id * @param {String} challengeId Challenge id @@ -506,6 +545,40 @@ async function checkCreateAccess (authUser, memberId, challengeDetails) { } } +/** + * Check the user's access to a challenge + * @param {Object} authUser the user + * @param {Array} resources the challenge resources + */ +const getChallengeAccessLevel = async (authUser, challengeId) => { + if (authUser.isMachine) { + return { hasFullAccess: true } + } + + const resources = await getChallengeResources(challengeId, authUser.userId) + + // Case Insensitive Role checks + const hasFullAccess = authUser.roles.findIndex(item => UserRoles.Admin.toLowerCase() === item.toLowerCase()) > -1 || _.intersectionWith(_.get(resources[authUser.userId], 'roles', []), [ + ProjectRoles.Manager, + ProjectRoles.Copilot, + ProjectRoles.Observer, + ProjectRoles.Client_Manager + ], (act, exp) => act.toLowerCase() === exp.toLowerCase()).length > 0 + + const isReviewer = !hasFullAccess && _.intersectionWith(_.get(resources[authUser.userId], 'roles', []), [ + ProjectRoles.Reviewer, + ProjectRoles.Iterative_Reviewer + ], (act, exp) => act.toLowerCase() === exp.toLowerCase()).length > 0 + + const isSubmitter = !hasFullAccess && !isReviewer && _.intersectionWith(_.get(resources[authUser.userId], 'roles', []), [ + ProjectRoles.Submitter + ], (act, exp) => act.toLowerCase() === exp.toLowerCase()).length > 0 + + const hasNoAccess = !hasFullAccess && !isReviewer && !isSubmitter + + return { hasFullAccess, isReviewer, isSubmitter, hasNoAccess } +} + /** * Function to check user access to get a submission * @param authUser Authenticated user @@ -922,6 +995,7 @@ module.exports = { setPaginationHeaders, getSubmissionPhaseId, checkCreateAccess, + getChallengeAccessLevel, checkGetAccess, checkReviewGetAccess, createS3ReadStream, diff --git a/src/constants/index.js b/src/constants/index.js new file mode 100644 index 00000000..44c35d80 --- /dev/null +++ b/src/constants/index.js @@ -0,0 +1,18 @@ +const UserRoles = { + Admin: 'Administrator' +} + +const ProjectRoles = { + Manager: 'Manager', + Copilot: 'Copilot', + Observer: 'Observer', + Reviewer: 'Reviewer', + Submitter: 'Submitter', + Client_Manager: 'Client Manager', + Iterative_Reviewer: 'Iterative Reviewer' +} + +module.exports = { + UserRoles, + ProjectRoles +} diff --git a/src/controllers/ArtifactController.js b/src/controllers/ArtifactController.js index e16680a3..56976f8b 100644 --- a/src/controllers/ArtifactController.js +++ b/src/controllers/ArtifactController.js @@ -10,7 +10,7 @@ const ArtifactService = require('../services/ArtifactService') * @param res the http response */ async function downloadArtifact (req, res) { - const result = await ArtifactService.downloadArtifact(req.params.submissionId, req.params.file) + const result = await ArtifactService.downloadArtifact(req.authUser, req.params.submissionId, req.params.file) res.attachment(result.fileName) res.send(result.file) } @@ -21,7 +21,7 @@ async function downloadArtifact (req, res) { * @param res the http response */ async function listArtifacts (req, res) { - res.json(await ArtifactService.listArtifacts(req.params.submissionId)) + res.json(await ArtifactService.listArtifacts(req.authUser, req.params.submissionId)) } /** diff --git a/src/services/ArtifactService.js b/src/services/ArtifactService.js index bba4961d..dec97133 100644 --- a/src/services/ArtifactService.js +++ b/src/services/ArtifactService.js @@ -12,6 +12,7 @@ const _ = require('lodash') const s3 = new AWS.S3() const logger = require('../common/logger') const HelperService = require('./HelperService') +const commonHelper = require('../common/helper') /* * Function to upload file to S3 @@ -39,16 +40,36 @@ async function _uploadToS3 (file, name) { * @param {String} fileName File name which need to be downloaded from S3 * @return {Promise} File downloaded from S3 */ -async function downloadArtifact (submissionId, fileName) { +async function downloadArtifact (authUser, submissionId, fileName) { // Check the validness of Submission ID - await HelperService._checkRef({ submissionId }) - const artifacts = await s3.listObjects({ Bucket: config.aws.ARTIFACT_BUCKET, Prefix: `${submissionId}/${fileName}` }).promise() + const submission = await HelperService._checkRef({ submissionId }) + + let challenge + try { + challenge = await commonHelper.getChallenge(submission.challengeId) + } catch (e) { + throw new errors.NotFoundError(`Could not load challenge: ${submission.challengeId}.\n Details: ${_.get(e, 'message')}`) + } + + const { hasFullAccess, isSubmitter, hasNoAccess } = await commonHelper.getChallengeAccessLevel(authUser, submission.challengeId) + + if (hasNoAccess || (isSubmitter && challenge.isMM && submission.memberId.toString() !== authUser.userId.toString())) { + throw new errors.HttpStatusError(403, 'You are not allowed to download this submission artifact.') + } + + if (fileName.includes('internal') && !hasFullAccess) { + throw new errors.HttpStatusError(403, 'Could not access artifact.') + } + + const prefix = submissionId + '/' + fileName + const artifacts = await s3.listObjects({ Bucket: config.aws.ARTIFACT_BUCKET, Prefix: prefix }).promise() + if (artifacts.Contents.length === 0) { throw new errors.HttpStatusError(400, `Artifact ${fileName} doesn't exist for ${submissionId}`) } - const key = submissionId + '/' + fileName + '.zip' - if (!_.includes(_.map(artifacts.Contents, 'Key'), key)) { + const key = _.get(_.find(artifacts.Contents, { Key: `${prefix}.zip` }) || (artifacts.Contents.length === 1 ? artifacts.Contents[0] : {}), 'Key', null) + if (!key) { throw new errors.HttpStatusError(400, `Artifact ${fileName} doesn't exist for ${submissionId}`) } @@ -59,6 +80,7 @@ async function downloadArtifact (submissionId, fileName) { } downloadArtifact.schema = joi.object({ + authUser: joi.object().required(), submissionId: joi.string().uuid().required(), fileName: joi.string().trim().required() }).required() @@ -68,14 +90,30 @@ downloadArtifact.schema = joi.object({ * @param {String} submissionId Submission ID * @return {Promise} List of files present in S3 bucket under submissionId directory */ -async function listArtifacts (submissionId) { +async function listArtifacts (authUser, submissionId) { // Check the validness of Submission ID - await HelperService._checkRef({ submissionId }) + const submission = await HelperService._checkRef({ submissionId }) + + let challenge + try { + challenge = await commonHelper.getChallenge(submission.challengeId) + } catch (e) { + throw new errors.NotFoundError(`Could not load challenge: ${submission.challengeId}.\n Details: ${_.get(e, 'message')}`) + } + + const { hasFullAccess, isSubmitter, hasNoAccess } = await commonHelper.getChallengeAccessLevel(authUser, submission.challengeId) + + if (hasNoAccess || (isSubmitter && challenge.isMM && submission.memberId.toString() !== authUser.userId.toString())) { + throw new errors.HttpStatusError(403, 'You are not allowed to access this submission artifact.') + } + const artifacts = await s3.listObjects({ Bucket: config.aws.ARTIFACT_BUCKET, Prefix: submissionId }).promise() - return { artifacts: _.map(artifacts.Contents, (at) => path.parse(at.Key).name) } + const artifactsContents = _.map(artifacts.Contents, (at) => path.parse(at.Key).name) + return { artifacts: hasFullAccess ? artifactsContents : _.filter(artifactsContents, artifactName => !artifactName.includes('internal')) } } listArtifacts.schema = joi.object({ + authUser: joi.object().required(), submissionId: joi.string().uuid().required() }).required() @@ -94,7 +132,7 @@ async function createArtifact (files, submissionId, entity) { logger.info('Creating a new Artifact') if (files && files.artifact) { const uFileType = (await FileType.fromBuffer(files.artifact.data)).ext // File type of uploaded file - fileName = `${submissionId}/${files.artifact.name}.${uFileType}` + fileName = `${submissionId}/${files.artifact.name.split('.').slice(0, -1)}.${uFileType}` // Upload the artifact to S3 await _uploadToS3(files.artifact, fileName) @@ -127,11 +165,6 @@ async function deleteArtifact (submissionId, fileName) { logger.info(`deleteArtifact: deleted artifact ${fileName} of Submission ID: ${submissionId}`) } -downloadArtifact.schema = joi.object({ - submissionId: joi.string().uuid().required(), - fileName: joi.string().trim().required() -}).required() - module.exports = { downloadArtifact, listArtifacts, diff --git a/src/services/HelperService.js b/src/services/HelperService.js index 7e949964..edad7beb 100644 --- a/src/services/HelperService.js +++ b/src/services/HelperService.js @@ -19,6 +19,8 @@ async function _checkRef (entity) { if (!existReviewType) { throw new errors.HttpStatusError(400, `Review type with ID = ${entity.typeId} does not exist`) } + + return existReviewType } if (entity.submissionId) { @@ -27,6 +29,8 @@ async function _checkRef (entity) { if (!existSubmission) { throw new errors.HttpStatusError(400, `Submission with ID = ${entity.submissionId} does not exist`) } + + return existSubmission } }