Skip to content

Commit a7ea4a7

Browse files
committed
Merge branch 'develop' of https://github.com/topcoder-platform/challenge-api into develop
2 parents 7e7d748 + 6c89714 commit a7ea4a7

File tree

3 files changed

+102
-16
lines changed

3 files changed

+102
-16
lines changed

config/default.js

+2
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ module.exports = {
4747
FILE_UPLOAD_SIZE_LIMIT: process.env.FILE_UPLOAD_SIZE_LIMIT
4848
? Number(process.env.FILE_UPLOAD_SIZE_LIMIT) : 50 * 1024 * 1024, // 50M
4949
RESOURCES_API_URL: process.env.RESOURCES_API_URL || 'http://localhost:4000/v5/resources',
50+
// TODO: change this to localhost
51+
RESOURCE_ROLES_API_URL: process.env.RESOURCE_ROLES_API_URL || 'http://api.topcoder-dev.com/v5/resource-roles',
5052
GROUPS_API_URL: process.env.GROUPS_API_URL || 'http://localhost:4000/v5/groups',
5153
PROJECTS_API_URL: process.env.PROJECTS_API_URL || 'http://localhost:4000/v5/projects',
5254
TERMS_API_URL: process.env.TERMS_API_URL || 'http://localhost:4000/v5/terms',

src/common/helper.js

+62-7
Original file line numberDiff line numberDiff line change
@@ -392,11 +392,46 @@ async function getM2MToken () {
392392
*/
393393
async function getChallengeResources (challengeId) {
394394
const token = await getM2MToken()
395-
const url = `${config.RESOURCES_API_URL}?challengeId=${challengeId}`
396-
const res = await axios.get(url, { headers: { Authorization: `Bearer ${token}` } })
395+
const perPage = 100
396+
let page = 1
397+
let result = []
398+
while (true) {
399+
const url = `${config.RESOURCES_API_URL}?challengeId=${challengeId}&perPage=${perPage}&page=${page}`
400+
const res = await axios.get(url, { headers: { Authorization: `Bearer ${token}` } })
401+
if (!res.data || res.data.length === 0) {
402+
break
403+
}
404+
result = result.concat(res.data)
405+
page += 1
406+
if (res.headers['x-total-pages'] && page > Number(res.headers['x-total-pages'])) {
407+
break
408+
}
409+
}
410+
return result
411+
}
412+
413+
/**
414+
* Get resource roles
415+
* @returns {Promise<Array>} the challenge resources
416+
*/
417+
async function getResourceRoles () {
418+
const token = await getM2MToken()
419+
const res = await axios.get(config.RESOURCE_ROLES_API_URL, { headers: { Authorization: `Bearer ${token}` } })
397420
return res.data || []
398421
}
399422

423+
/**
424+
* Check if a user has full access on a challenge
425+
* @param {String} challengeId the challenge UUID
426+
* @param {String} userId the user ID
427+
*/
428+
async function userHasFullAccess (challengeId, userId) {
429+
const resourceRoles = await getResourceRoles()
430+
const rolesWithFullAccess = _.map(_.filter(resourceRoles, r => r.fullAccess), 'id')
431+
const challengeResources = await getChallengeResources(challengeId)
432+
return _.filter(challengeResources, r => _.toString(r.memberId) === _.toString(userId) && _.includes(rolesWithFullAccess, r.roleId)).length > 0
433+
}
434+
400435
/**
401436
* Get all user groups
402437
* @param {String} userId the user id
@@ -647,8 +682,17 @@ async function validateESRefreshMethod (method) {
647682
async function getProjectDefaultTerms (projectId) {
648683
const token = await getM2MToken()
649684
const projectUrl = `${config.PROJECTS_API_URL}/${projectId}`
650-
const res = await axios.get(projectUrl, { headers: { Authorization: `Bearer ${token}` } })
651-
return res.data.terms || []
685+
try {
686+
const res = await axios.get(projectUrl, { headers: { Authorization: `Bearer ${token}` } })
687+
return res.data.terms || []
688+
} catch (err) {
689+
if (_.get(err, 'response.status') === HttpStatus.NOT_FOUND) {
690+
throw new errors.BadRequestError(`Project with id: ${projectId} doesn't exist`)
691+
} else {
692+
// re-throw other error
693+
throw err
694+
}
695+
}
652696
}
653697

654698
/**
@@ -660,8 +704,17 @@ async function getProjectDefaultTerms (projectId) {
660704
async function getProjectBillingAccount (projectId) {
661705
const token = await getM2MToken()
662706
const projectUrl = `${config.V3_PROJECTS_API_URL}/${projectId}`
663-
const res = await axios.get(projectUrl, { headers: { Authorization: `Bearer ${token}` } })
664-
return _.get(res, 'data.result.content.billingAccountIds[0]', null)
707+
try {
708+
const res = await axios.get(projectUrl, { headers: { Authorization: `Bearer ${token}` } })
709+
return _.get(res, 'data.result.content.billingAccountIds[0]', null)
710+
} catch (err) {
711+
if (_.get(err, 'response.status') === HttpStatus.NOT_FOUND) {
712+
throw new errors.BadRequestError(`Project with id: ${projectId} doesn't exist`)
713+
} else {
714+
// re-throw other error
715+
throw err
716+
}
717+
}
665718
}
666719

667720
/**
@@ -723,5 +776,7 @@ module.exports = {
723776
getProjectBillingAccount,
724777
expandWithSubGroups,
725778
getCompleteUserGroupTreeIds,
726-
expandWithParentGroups
779+
expandWithParentGroups,
780+
getResourceRoles,
781+
userHasFullAccess
727782
}

src/services/ChallengeService.js

+38-9
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,15 @@ async function searchChallenges (currentUser, criteria) {
109109
const page = criteria.page || 1
110110
const perPage = criteria.perPage || 20
111111
const boolQuery = []
112+
const matchPhraseKeys = [
113+
'id',
114+
'timelineTemplateId',
115+
'projectId',
116+
'legacyId',
117+
'status',
118+
'createdBy',
119+
'updatedBy'
120+
]
112121

113122
const includedTrackIds = _.isArray(criteria.trackIds) ? criteria.trackIds : []
114123

@@ -151,18 +160,24 @@ async function searchChallenges (currentUser, criteria) {
151160
includedTrackIds.push(criteria.trackId)
152161
}
153162

154-
_.forIn(_.omit(criteria, ['types', 'tracks', 'typeIds', 'trackIds', 'type', 'name', 'trackId', 'typeId', 'description', 'page', 'perPage', 'tag',
155-
'group', 'groups', 'memberId', 'ids', 'createdDateStart', 'createdDateEnd', 'updatedDateStart', 'updatedDateEnd', 'startDateStart', 'startDateEnd', 'endDateStart', 'endDateEnd',
156-
'tags', 'registrationStartDateStart', 'registrationStartDateEnd', 'currentPhaseName', 'submissionStartDateStart', 'submissionStartDateEnd',
157-
'registrationEndDateStart', 'registrationEndDateEnd', 'submissionEndDateStart', 'submissionEndDateEnd', 'includeAllEvents', 'events',
158-
'forumId', 'track', 'reviewType', 'confidentialityType', 'directProjectId', 'sortBy', 'sortOrder', 'isLightweight', 'isTask', 'taskIsAssigned', 'taskMemberId']), (value, key) => {
163+
_.forIn(_.pick(criteria, matchPhraseKeys), (value, key) => {
159164
if (!_.isUndefined(value)) {
160165
const filter = { match_phrase: {} }
161166
filter.match_phrase[key] = value
162167
boolQuery.push(filter)
163168
}
164169
})
165170

171+
_.forEach(_.keys(criteria), (key) => {
172+
if (_.toString(key).indexOf('meta.') > -1) {
173+
// Parse and use metadata key
174+
if (!_.isUndefined(criteria[key])) {
175+
const metaKey = key.split('meta.')[1]
176+
boolQuery.push({ match_phrase: { [`metadata.${metaKey}`]: criteria[key] } })
177+
}
178+
}
179+
})
180+
166181
if (includedTypeIds.length > 0) {
167182
boolQuery.push({
168183
bool: {
@@ -594,7 +609,7 @@ searchChallenges.schema = {
594609
taskMemberId: Joi.string(),
595610
events: Joi.array().items(Joi.number()),
596611
includeAllEvents: Joi.boolean().default(true)
597-
})
612+
}).unknown(true)
598613
}
599614

600615
/**
@@ -743,6 +758,8 @@ async function createChallenge (currentUser, challenge, userToken) {
743758
}
744759
if (_.isUndefined(_.get(challenge, 'task.memberId'))) {
745760
_.set(challenge, 'task.memberId', null)
761+
} else {
762+
throw new errors.BadRequestError(`Cannot assign a member before the challenge gets created.`)
746763
}
747764
}
748765
if (challenge.phases && challenge.phases.length > 0) {
@@ -1164,8 +1181,9 @@ async function update (currentUser, challengeId, data, userToken, isFull) {
11641181
newAttachments = await helper.getByIds('Attachment', data.attachmentIds || [])
11651182
}
11661183

1167-
if (!currentUser.isMachine && !helper.hasAdminRole(currentUser) && challenge.createdBy.toLowerCase() !== currentUser.handle.toLowerCase()) {
1168-
throw new errors.ForbiddenError(`Only M2M, admin or challenge's copilot can perform modification.`)
1184+
const userHasFullAccess = await helper.userHasFullAccess(challengeId, currentUser.userId)
1185+
if (!currentUser.isMachine && !helper.hasAdminRole(currentUser) && challenge.createdBy.toLowerCase() !== currentUser.handle.toLowerCase() && !userHasFullAccess) {
1186+
throw new errors.ForbiddenError(`Only M2M, admin, challenge's copilot or users with full access can perform modification.`)
11691187
}
11701188

11711189
// Validate the challenge terms
@@ -1424,6 +1442,18 @@ async function update (currentUser, challengeId, data, userToken, isFull) {
14241442
data.winners = null
14251443
}
14261444

1445+
const { track, type } = await validateChallengeData(_.pick(challenge, ['trackId', 'typeId']))
1446+
1447+
if (_.get(type, 'isTask')) {
1448+
if (!_.isEmpty(_.get(data, 'task.memberId'))) {
1449+
const challengeResources = await helper.getChallengeResources(challengeId)
1450+
const registrants = _.filter(challengeResources, r => r.roleId === config.SUBMITTER_ROLE_ID)
1451+
if (!_.find(registrants, r => _.toString(r.memberId) === _.toString(_.get(data, 'task.memberId')))) {
1452+
throw new errors.BadRequestError(`Member ${_.get(data, 'task.memberId')} is not a submitter resource of challenge ${challengeId}`)
1453+
}
1454+
}
1455+
}
1456+
14271457
logger.debug(`Challenge.update id: ${challengeId} Details: ${JSON.stringify(updateDetails)}`)
14281458
await models.Challenge.update({ id: challengeId }, updateDetails)
14291459

@@ -1464,7 +1494,6 @@ async function update (currentUser, challengeId, data, userToken, isFull) {
14641494
}
14651495

14661496
// Populate challenge.track and challenge.type based on the track/type IDs
1467-
const { track, type } = await validateChallengeData(_.pick(challenge, ['trackId', 'typeId']))
14681497

14691498
if (track) {
14701499
challenge.track = track.name

0 commit comments

Comments
 (0)