Skip to content
4 changes: 2 additions & 2 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ jobs:
source awsenvconf
./buildenv.sh -e DEV -b dev-submissions-api-deployvar
source buildenvvar
./master_deploy.sh -d ECS -e DEV -t latest -s dev-global-appvar,dev-submissions-api-appvar -i submissions-api
./master_deploy.sh -d ECS -e DEV -t latest -s dev-global-appvar,dev-submissions-api-appvar -i submissions-api -p FARGATE

"build-prod":
<<: *defaults
Expand Down Expand Up @@ -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:
Expand Down
74 changes: 74 additions & 0 deletions src/common/helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
*/
async function getChallengeAccessLevel (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
Expand Down Expand Up @@ -922,6 +995,7 @@ module.exports = {
setPaginationHeaders,
getSubmissionPhaseId,
checkCreateAccess,
getChallengeAccessLevel,
checkGetAccess,
checkReviewGetAccess,
createS3ReadStream,
Expand Down
18 changes: 18 additions & 0 deletions src/constants/index.js
Original file line number Diff line number Diff line change
@@ -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
}
4 changes: 2 additions & 2 deletions src/controllers/ArtifactController.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand All @@ -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))
}

/**
Expand Down
47 changes: 33 additions & 14 deletions src/services/ArtifactService.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -39,16 +40,29 @@ async function _uploadToS3 (file, name) {
* @param {String} fileName File name which need to be downloaded from S3
* @return {Promise<Object>} 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 })

const { hasFullAccess, isSubmitter, hasNoAccess } = await commonHelper.getChallengeAccessLevel(authUser, submission.challengeId)

if (hasNoAccess || (isSubmitter && 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}`)
}

Expand All @@ -59,6 +73,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()
Expand All @@ -68,14 +83,23 @@ downloadArtifact.schema = joi.object({
* @param {String} submissionId Submission ID
* @return {Promise<Object>} 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 })

const { hasFullAccess, isSubmitter, hasNoAccess } = await commonHelper.getChallengeAccessLevel(authUser, submission.challengeId)

if (hasNoAccess || (isSubmitter && 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()

Expand All @@ -94,7 +118,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)
Expand Down Expand Up @@ -127,11 +151,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,
Expand Down
4 changes: 4 additions & 0 deletions src/services/HelperService.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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
}
}

Expand Down