Skip to content

Commit 73e47a8

Browse files
authored
Merge pull request #369 from topcoder-platform/PM-809_artifact-endpoint-update
PM-809 artifact endpoint update
2 parents bde93ae + f2d631a commit 73e47a8

File tree

6 files changed

+146
-17
lines changed

6 files changed

+146
-17
lines changed

.circleci/config.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ workflows:
6969
context: org-global
7070
filters:
7171
branches:
72-
only: ["develop", "PLAT-3383"]
72+
only: ["develop", "PM-809_artifact-endpoint-update"]
7373
- "build-prod":
7474
context: org-global
7575
filters:

src/common/helper.js

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ const errors = require('common-errors')
1414
const { validate: uuidValidate } = require('uuid')
1515
const NodeCache = require('node-cache')
1616
const { axiosInstance } = require('./axiosInstance')
17+
const { UserRoles, ProjectRoles } = require('../constants')
1718

1819
AWS.config.region = config.get('aws.AWS_REGION')
1920
const s3 = new AWS.S3()
@@ -312,6 +313,44 @@ function setPaginationHeaders (req, res, data) {
312313
res.json(data.rows)
313314
}
314315

316+
/**
317+
* Get challenge resources
318+
* @param {String} challengeId the challenge id
319+
* @param {String} userId specific userId for which to check roles
320+
*/
321+
const getChallengeResources = async (challengeId, userId) => {
322+
let resourcesResponse
323+
324+
// Get map of role id to role name
325+
const resourceRolesMap = await getRoleIdToRoleNameMap()
326+
327+
// Check if role id to role name mapping is available. If not user's role cannot be determined.
328+
if (resourceRolesMap == null || _.size(resourceRolesMap) === 0) {
329+
throw new errors.HttpStatusError(503, `Could not determine the user's role in the challenge with id ${challengeId}`)
330+
}
331+
332+
const resourcesUrl = `${config.RESOURCEAPI_V5_BASE_URL}/resources?challengeId=${challengeId}${userId ? `&memberId=${userId}` : ''}`
333+
try {
334+
resourcesResponse = _.get(await axiosInstance.get(resourcesUrl), 'data', [])
335+
} catch (ex) {
336+
logger.error(`Error while accessing ${resourcesUrl}`)
337+
throw new errors.HttpStatusError(503, `Could not determine the user's role in the challenge with id ${challengeId}`)
338+
}
339+
340+
const resources = {}
341+
_.each((resourcesResponse || []), (resource) => {
342+
if (!resources[resource.memberId]) {
343+
resources[resource.memberId] = {
344+
memberId: resource.memberId,
345+
memberHandle: resource.memberHandle,
346+
roles: []
347+
}
348+
}
349+
resources[resource.memberId].roles.push(resourceRolesMap[resource.roleId])
350+
})
351+
return resources
352+
}
353+
315354
/**
316355
* Function to get challenge by id
317356
* @param {String} challengeId Challenge id
@@ -506,6 +545,40 @@ async function checkCreateAccess (authUser, memberId, challengeDetails) {
506545
}
507546
}
508547

548+
/**
549+
* Check the user's access to a challenge
550+
* @param {Object} authUser the user
551+
* @param {Array} resources the challenge resources
552+
*/
553+
const getChallengeAccessLevel = async (authUser, challengeId) => {
554+
if (authUser.isMachine) {
555+
return { hasFullAccess: true }
556+
}
557+
558+
const resources = await getChallengeResources(challengeId, authUser.userId)
559+
560+
// Case Insensitive Role checks
561+
const hasFullAccess = authUser.roles.findIndex(item => UserRoles.Admin.toLowerCase() === item.toLowerCase()) > -1 || _.intersectionWith(_.get(resources[authUser.userId], 'roles', []), [
562+
ProjectRoles.Manager,
563+
ProjectRoles.Copilot,
564+
ProjectRoles.Observer,
565+
ProjectRoles.Client_Manager
566+
], (act, exp) => act.toLowerCase() === exp.toLowerCase()).length > 0
567+
568+
const isReviewer = !hasFullAccess && _.intersectionWith(_.get(resources[authUser.userId], 'roles', []), [
569+
ProjectRoles.Reviewer,
570+
ProjectRoles.Iterative_Reviewer
571+
], (act, exp) => act.toLowerCase() === exp.toLowerCase()).length > 0
572+
573+
const isSubmitter = !hasFullAccess && !isReviewer && _.intersectionWith(_.get(resources[authUser.userId], 'roles', []), [
574+
ProjectRoles.Submitter
575+
], (act, exp) => act.toLowerCase() === exp.toLowerCase()).length > 0
576+
577+
const hasNoAccess = !hasFullAccess && !isReviewer && !isSubmitter
578+
579+
return { hasFullAccess, isReviewer, isSubmitter, hasNoAccess }
580+
}
581+
509582
/**
510583
* Function to check user access to get a submission
511584
* @param authUser Authenticated user
@@ -922,6 +995,7 @@ module.exports = {
922995
setPaginationHeaders,
923996
getSubmissionPhaseId,
924997
checkCreateAccess,
998+
getChallengeAccessLevel,
925999
checkGetAccess,
9261000
checkReviewGetAccess,
9271001
createS3ReadStream,

src/constants/index.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
const UserRoles = {
2+
Admin: 'Administrator'
3+
}
4+
5+
const ProjectRoles = {
6+
Manager: 'Manager',
7+
Copilot: 'Copilot',
8+
Observer: 'Observer',
9+
Reviewer: 'Reviewer',
10+
Submitter: 'Submitter',
11+
Client_Manager: 'Client Manager',
12+
Iterative_Reviewer: 'Iterative Reviewer'
13+
}
14+
15+
module.exports = {
16+
UserRoles,
17+
ProjectRoles
18+
}

src/controllers/ArtifactController.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ const ArtifactService = require('../services/ArtifactService')
1010
* @param res the http response
1111
*/
1212
async function downloadArtifact (req, res) {
13-
const result = await ArtifactService.downloadArtifact(req.params.submissionId, req.params.file)
13+
const result = await ArtifactService.downloadArtifact(req.authUser, req.params.submissionId, req.params.file)
1414
res.attachment(result.fileName)
1515
res.send(result.file)
1616
}
@@ -21,7 +21,7 @@ async function downloadArtifact (req, res) {
2121
* @param res the http response
2222
*/
2323
async function listArtifacts (req, res) {
24-
res.json(await ArtifactService.listArtifacts(req.params.submissionId))
24+
res.json(await ArtifactService.listArtifacts(req.authUser, req.params.submissionId))
2525
}
2626

2727
/**

src/services/ArtifactService.js

Lines changed: 47 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ const _ = require('lodash')
1212
const s3 = new AWS.S3()
1313
const logger = require('../common/logger')
1414
const HelperService = require('./HelperService')
15+
const commonHelper = require('../common/helper')
1516

1617
/*
1718
* Function to upload file to S3
@@ -39,16 +40,36 @@ async function _uploadToS3 (file, name) {
3940
* @param {String} fileName File name which need to be downloaded from S3
4041
* @return {Promise<Object>} File downloaded from S3
4142
*/
42-
async function downloadArtifact (submissionId, fileName) {
43+
async function downloadArtifact (authUser, submissionId, fileName) {
4344
// Check the validness of Submission ID
44-
await HelperService._checkRef({ submissionId })
45-
const artifacts = await s3.listObjects({ Bucket: config.aws.ARTIFACT_BUCKET, Prefix: `${submissionId}/${fileName}` }).promise()
45+
const submission = await HelperService._checkRef({ submissionId })
46+
47+
let challenge
48+
try {
49+
challenge = await commonHelper.getChallenge(submission.challengeId)
50+
} catch (e) {
51+
throw new errors.NotFoundError(`Could not load challenge: ${submission.challengeId}.\n Details: ${_.get(e, 'message')}`)
52+
}
53+
54+
const { hasFullAccess, isSubmitter, hasNoAccess } = await commonHelper.getChallengeAccessLevel(authUser, submission.challengeId)
55+
56+
if (hasNoAccess || (isSubmitter && challenge.isMM && submission.memberId.toString() !== authUser.userId.toString())) {
57+
throw new errors.HttpStatusError(403, 'You are not allowed to download this submission artifact.')
58+
}
59+
60+
if (fileName.includes('internal') && !hasFullAccess) {
61+
throw new errors.HttpStatusError(403, 'Could not access artifact.')
62+
}
63+
64+
const prefix = submissionId + '/' + fileName
65+
const artifacts = await s3.listObjects({ Bucket: config.aws.ARTIFACT_BUCKET, Prefix: prefix }).promise()
66+
4667
if (artifacts.Contents.length === 0) {
4768
throw new errors.HttpStatusError(400, `Artifact ${fileName} doesn't exist for ${submissionId}`)
4869
}
4970

50-
const key = submissionId + '/' + fileName + '.zip'
51-
if (!_.includes(_.map(artifacts.Contents, 'Key'), key)) {
71+
const key = _.get(_.find(artifacts.Contents, { Key: `${prefix}.zip` }) || (artifacts.Contents.length === 1 ? artifacts.Contents[0] : {}), 'Key', null)
72+
if (!key) {
5273
throw new errors.HttpStatusError(400, `Artifact ${fileName} doesn't exist for ${submissionId}`)
5374
}
5475

@@ -59,6 +80,7 @@ async function downloadArtifact (submissionId, fileName) {
5980
}
6081

6182
downloadArtifact.schema = joi.object({
83+
authUser: joi.object().required(),
6284
submissionId: joi.string().uuid().required(),
6385
fileName: joi.string().trim().required()
6486
}).required()
@@ -68,14 +90,30 @@ downloadArtifact.schema = joi.object({
6890
* @param {String} submissionId Submission ID
6991
* @return {Promise<Object>} List of files present in S3 bucket under submissionId directory
7092
*/
71-
async function listArtifacts (submissionId) {
93+
async function listArtifacts (authUser, submissionId) {
7294
// Check the validness of Submission ID
73-
await HelperService._checkRef({ submissionId })
95+
const submission = await HelperService._checkRef({ submissionId })
96+
97+
let challenge
98+
try {
99+
challenge = await commonHelper.getChallenge(submission.challengeId)
100+
} catch (e) {
101+
throw new errors.NotFoundError(`Could not load challenge: ${submission.challengeId}.\n Details: ${_.get(e, 'message')}`)
102+
}
103+
104+
const { hasFullAccess, isSubmitter, hasNoAccess } = await commonHelper.getChallengeAccessLevel(authUser, submission.challengeId)
105+
106+
if (hasNoAccess || (isSubmitter && challenge.isMM && submission.memberId.toString() !== authUser.userId.toString())) {
107+
throw new errors.HttpStatusError(403, 'You are not allowed to access this submission artifact.')
108+
}
109+
74110
const artifacts = await s3.listObjects({ Bucket: config.aws.ARTIFACT_BUCKET, Prefix: submissionId }).promise()
75-
return { artifacts: _.map(artifacts.Contents, (at) => path.parse(at.Key).name) }
111+
const artifactsContents = _.map(artifacts.Contents, (at) => path.parse(at.Key).name)
112+
return { artifacts: hasFullAccess ? artifactsContents : _.filter(artifactsContents, artifactName => !artifactName.includes('internal')) }
76113
}
77114

78115
listArtifacts.schema = joi.object({
116+
authUser: joi.object().required(),
79117
submissionId: joi.string().uuid().required()
80118
}).required()
81119

@@ -94,7 +132,7 @@ async function createArtifact (files, submissionId, entity) {
94132
logger.info('Creating a new Artifact')
95133
if (files && files.artifact) {
96134
const uFileType = (await FileType.fromBuffer(files.artifact.data)).ext // File type of uploaded file
97-
fileName = `${submissionId}/${files.artifact.name}.${uFileType}`
135+
fileName = `${submissionId}/${files.artifact.name.split('.').slice(0, -1)}.${uFileType}`
98136

99137
// Upload the artifact to S3
100138
await _uploadToS3(files.artifact, fileName)
@@ -127,11 +165,6 @@ async function deleteArtifact (submissionId, fileName) {
127165
logger.info(`deleteArtifact: deleted artifact ${fileName} of Submission ID: ${submissionId}`)
128166
}
129167

130-
downloadArtifact.schema = joi.object({
131-
submissionId: joi.string().uuid().required(),
132-
fileName: joi.string().trim().required()
133-
}).required()
134-
135168
module.exports = {
136169
downloadArtifact,
137170
listArtifacts,

src/services/HelperService.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ async function _checkRef (entity) {
1919
if (!existReviewType) {
2020
throw new errors.HttpStatusError(400, `Review type with ID = ${entity.typeId} does not exist`)
2121
}
22+
23+
return existReviewType
2224
}
2325

2426
if (entity.submissionId) {
@@ -27,6 +29,8 @@ async function _checkRef (entity) {
2729
if (!existSubmission) {
2830
throw new errors.HttpStatusError(400, `Submission with ID = ${entity.submissionId} does not exist`)
2931
}
32+
33+
return existSubmission
3034
}
3135
}
3236

0 commit comments

Comments
 (0)