Skip to content

v1.0.2 Release #292

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 17 commits into from
Sep 15, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# Topcoder Challenge API

This microservice provides access and interaction with all sorts of Challenge data.
## Devlopment status
[![Total alerts](https://img.shields.io/lgtm/alerts/g/topcoder-platform/challenge-api.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/topcoder-platform/challenge-api/alerts/)[![Language grade: JavaScript](https://img.shields.io/lgtm/grade/javascript/g/topcoder-platform/challenge-api.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/topcoder-platform/challenge-api/context:javascript)

### Deployment status
Dev: [![CircleCI](https://circleci.com/gh/topcoder-platform/challenge-api/tree/develop.svg?style=svg)](https://circleci.com/gh/topcoder-platform/challenge-api/tree/develop) Prod: [![CircleCI](https://circleci.com/gh/topcoder-platform/challenge-api/tree/master.svg?style=svg)](https://circleci.com/gh/topcoder-platform/challenge-api/tree/master)
Expand Down
13 changes: 10 additions & 3 deletions app-constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@
* App constants
*/
const UserRoles = {
Admin: 'Administrator',
Copilot: 'Copilot',
Admin: 'administrator',
Copilot: 'copilot',
Manager: 'Connect Manager',
User: 'Topcoder User'
}

Expand Down Expand Up @@ -70,6 +71,11 @@ const challengeTracks = {
QA: 'QA'
}

const challengeTextSortField = {
Name: 'name',
TypeId: 'typeId'
}

module.exports = {
UserRoles,
prizeSetTypes,
Expand All @@ -78,5 +84,6 @@ module.exports = {
EVENT_ORIGINATOR,
EVENT_MIME_TYPE,
Topics,
challengeTracks
challengeTracks,
challengeTextSortField
}
22 changes: 22 additions & 0 deletions docs/swagger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,28 @@ paths:
type: array
items:
type: string
- name: includeAllTags
in: query
description: >-
Require all provided tags to be present on a challenge for a match
required: false
default: true
type: boolean
- name: events
in: query
description: >-
Filter by multiple event keys (ie: tco21)
required: false
type: array
items:
type: number
- name: includeAllEvents
in: query
description: >-
Require all provided events to be present on a challenge for a match
required: false
default: true
type: boolean
- name: projectId
in: query
description: 'Filter by v5 project id, exact match.'
Expand Down
32 changes: 16 additions & 16 deletions src/routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,14 @@ module.exports = {
get: {
controller: 'ChallengeController',
method: 'searchChallenges',
access: [constants.UserRoles.Admin, constants.UserRoles.Copilot, constants.UserRoles.User],
access: [constants.UserRoles.Admin, constants.UserRoles.Copilot, constants.UserRoles.Manager, constants.UserRoles.User],
scopes: [READ, ALL]
},
post: {
controller: 'ChallengeController',
method: 'createChallenge',
auth: 'jwt',
access: [constants.UserRoles.Admin, constants.UserRoles.Copilot],
access: [constants.UserRoles.Admin, constants.UserRoles.Copilot, constants.UserRoles.Manager],
scopes: [CREATE, ALL]
}
},
Expand All @@ -43,14 +43,14 @@ module.exports = {
controller: 'ChallengeController',
method: 'fullyUpdateChallenge',
auth: 'jwt',
access: [constants.UserRoles.Admin, constants.UserRoles.Copilot],
access: [constants.UserRoles.Admin, constants.UserRoles.Copilot, constants.UserRoles.Manager],
scopes: [UPDATE, ALL]
},
patch: {
controller: 'ChallengeController',
method: 'partiallyUpdateChallenge',
auth: 'jwt',
access: [constants.UserRoles.Admin, constants.UserRoles.Copilot],
access: [constants.UserRoles.Admin, constants.UserRoles.Copilot, constants.UserRoles.Manager],
scopes: [UPDATE, ALL]
}
},
Expand All @@ -63,7 +63,7 @@ module.exports = {
controller: 'ChallengeTypeController',
method: 'createChallengeType',
auth: 'jwt',
access: [constants.UserRoles.Admin, constants.UserRoles.Copilot],
access: [constants.UserRoles.Admin, constants.UserRoles.Copilot, constants.UserRoles.Manager],
scopes: [CREATE, ALL]
}
},
Expand All @@ -76,14 +76,14 @@ module.exports = {
controller: 'ChallengeTypeController',
method: 'fullyUpdateChallengeType',
auth: 'jwt',
access: [constants.UserRoles.Admin, constants.UserRoles.Copilot],
access: [constants.UserRoles.Admin, constants.UserRoles.Copilot, constants.UserRoles.Manager],
scopes: [UPDATE, ALL]
},
patch: {
controller: 'ChallengeTypeController',
method: 'partiallyUpdateChallengeType',
auth: 'jwt',
access: [constants.UserRoles.Admin, constants.UserRoles.Copilot],
access: [constants.UserRoles.Admin, constants.UserRoles.Copilot, constants.UserRoles.Manager],
scopes: [UPDATE, ALL]
}
},
Expand All @@ -96,7 +96,7 @@ module.exports = {
controller: 'ChallengeTrackController',
method: 'createChallengeTrack',
auth: 'jwt',
access: [constants.UserRoles.Admin, constants.UserRoles.Copilot],
access: [constants.UserRoles.Admin, constants.UserRoles.Copilot, constants.UserRoles.Manager],
scopes: [CREATE, ALL]
}
},
Expand All @@ -109,14 +109,14 @@ module.exports = {
controller: 'ChallengeTrackController',
method: 'fullyUpdateChallengeTrack',
auth: 'jwt',
access: [constants.UserRoles.Admin, constants.UserRoles.Copilot],
access: [constants.UserRoles.Admin, constants.UserRoles.Copilot, constants.UserRoles.Manager],
scopes: [UPDATE, ALL]
},
patch: {
controller: 'ChallengeTrackController',
method: 'partiallyUpdateChallengeTrack',
auth: 'jwt',
access: [constants.UserRoles.Admin, constants.UserRoles.Copilot],
access: [constants.UserRoles.Admin, constants.UserRoles.Copilot, constants.UserRoles.Manager],
scopes: [UPDATE, ALL]
}
},
Expand All @@ -132,7 +132,7 @@ module.exports = {
controller: 'ChallengeTimelineTemplateController',
method: 'createChallengeTimelineTemplate',
auth: 'jwt',
access: [constants.UserRoles.Admin, constants.UserRoles.Copilot],
access: [constants.UserRoles.Admin, constants.UserRoles.Copilot, constants.UserRoles.Manager],
scopes: [CREATE, ALL]
}
},
Expand All @@ -148,14 +148,14 @@ module.exports = {
controller: 'ChallengeTimelineTemplateController',
method: 'fullyUpdateChallengeTimelineTemplate',
auth: 'jwt',
access: [constants.UserRoles.Admin, constants.UserRoles.Copilot],
access: [constants.UserRoles.Admin, constants.UserRoles.Copilot, constants.UserRoles.Manager],
scopes: [UPDATE, ALL]
},
delete: {
controller: 'ChallengeTimelineTemplateController',
method: 'deleteChallengeTimelineTemplate',
auth: 'jwt',
access: [constants.UserRoles.Admin, constants.UserRoles.Copilot],
access: [constants.UserRoles.Admin, constants.UserRoles.Copilot, constants.UserRoles.Manager],
scopes: [DELETE, ALL]
}
},
Expand Down Expand Up @@ -187,7 +187,7 @@ module.exports = {
controller: 'ChallengePhaseController',
method: 'getPhase',
auth: 'jwt',
access: [constants.UserRoles.Admin, constants.UserRoles.Copilot],
access: [constants.UserRoles.Admin, constants.UserRoles.Copilot, constants.UserRoles.Manager],
scopes: [READ, ALL]
},
put: {
Expand Down Expand Up @@ -231,7 +231,7 @@ module.exports = {
controller: 'TimelineTemplateController',
method: 'getTimelineTemplate',
auth: 'jwt',
access: [constants.UserRoles.Admin, constants.UserRoles.Copilot],
access: [constants.UserRoles.Admin, constants.UserRoles.Copilot, constants.UserRoles.Manager],
scopes: [READ, ALL]
},
put: {
Expand Down Expand Up @@ -261,7 +261,7 @@ module.exports = {
controller: 'AttachmentController',
method: 'uploadAttachment',
auth: 'jwt',
access: [constants.UserRoles.Admin, constants.UserRoles.Copilot],
access: [constants.UserRoles.Admin, constants.UserRoles.Copilot, constants.UserRoles.Manager],
scopes: [CREATE, ALL]
}
},
Expand Down
2 changes: 1 addition & 1 deletion src/scripts/seed/ChallengeType.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,6 @@
"description": "A piece of work assigned to one person",
"isActive": true,
"isTask": true,
"abbreviation": "T"
"abbreviation": "TSK"
}
]
98 changes: 63 additions & 35 deletions src/services/ChallengeService.js
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ async function searchChallenges (currentUser, criteria) {
_.forIn(_.omit(criteria, ['types', 'tracks', 'typeIds', 'trackIds', 'type', 'name', 'trackId', 'typeId', 'description', 'page', 'perPage', 'tag',
'group', 'groups', 'memberId', 'ids', 'createdDateStart', 'createdDateEnd', 'updatedDateStart', 'updatedDateEnd', 'startDateStart', 'startDateEnd', 'endDateStart', 'endDateEnd',
'tags', 'registrationStartDateStart', 'registrationStartDateEnd', 'currentPhaseName', 'submissionStartDateStart', 'submissionStartDateEnd',
'registrationEndDateStart', 'registrationEndDateEnd', 'submissionEndDateStart', 'submissionEndDateEnd',
'registrationEndDateStart', 'registrationEndDateEnd', 'submissionEndDateStart', 'submissionEndDateEnd', 'includeAllEvents', 'events',
'forumId', 'track', 'reviewType', 'confidentialityType', 'directProjectId', 'sortBy', 'sortOrder', 'isLightweight', 'isTask', 'taskIsAssigned', 'taskMemberId']), (value, key) => {
if (!_.isUndefined(value)) {
const filter = { match_phrase: {} }
Expand Down Expand Up @@ -251,7 +251,10 @@ async function searchChallenges (currentUser, criteria) {
if (criteria.endDateEnd) {
boolQuery.push({ range: { endDate: { lte: criteria.endDateEnd } } })
}
const sortByProp = criteria.sortBy ? criteria.sortBy : 'created'

let sortByProp = criteria.sortBy ? criteria.sortBy : 'created'
// If property to sort is text, then use its sub-field 'keyword' for sorting
sortByProp = _.includes(constants.challengeTextSortField, sortByProp) ? sortByProp + '.keyword' : sortByProp
const sortOrderProp = criteria.sortOrder ? criteria.sortOrder : 'desc'

const mustQuery = []
Expand All @@ -271,6 +274,18 @@ async function searchChallenges (currentUser, criteria) {
}
}

if (criteria.events) {
if (criteria.includeAllEvents) {
for (const e of criteria.events) {
boolQuery.push({ match_phrase: { 'events.key': e } })
}
} else {
for (const e of criteria.events) {
shouldQuery.push({ match: { 'events.key': e } })
}
}
}

const mustNotQuery = []

let groupsToFilter = []
Expand Down Expand Up @@ -339,6 +354,29 @@ async function searchChallenges (currentUser, criteria) {
}
}

const accessQuery = []
let memberChallengeIds

// FIXME: This is wrong!
// if (!_.isUndefined(currentUser) && currentUser.handle) {
// accessQuery.push({ match_phrase: { createdBy: currentUser.handle } })
// }

if (criteria.memberId) {
// logger.error(`memberId ${criteria.memberId}`)
memberChallengeIds = await helper.listChallengesByMember(criteria.memberId)
// logger.error(`response ${JSON.stringify(ids)}`)
accessQuery.push({ terms: { _id: memberChallengeIds } })
}

if (accessQuery.length > 0) {
mustQuery.push({
bool: {
should: accessQuery
}
})
}

// FIXME: Tech Debt
let excludeTasks = true
// if you're an admin or m2m, security rules wont be applied
Expand Down Expand Up @@ -369,6 +407,8 @@ async function searchChallenges (currentUser, criteria) {
mustQuery.push({
bool: {
should: [
...(_.get(memberChallengeIds, 'length', 0) > 0 ? [{ terms: { _id: memberChallengeIds } }] : []),
{ bool: { must_not: { exists: { field: 'task.isTask' } } } },
{ match_phrase: { 'task.isTask': false } },
{
bool: {
Expand Down Expand Up @@ -396,28 +436,6 @@ async function searchChallenges (currentUser, criteria) {
})
}

const accessQuery = []

// FIXME: This is wrong!
// if (!_.isUndefined(currentUser) && currentUser.handle) {
// accessQuery.push({ match_phrase: { createdBy: currentUser.handle } })
// }

if (criteria.memberId) {
// logger.error(`memberId ${criteria.memberId}`)
const ids = await helper.listChallengesByMember(criteria.memberId)
// logger.error(`response ${JSON.stringify(ids)}`)
accessQuery.push({ terms: { _id: ids } })
}

if (accessQuery.length > 0) {
mustQuery.push({
bool: {
should: accessQuery
}
})
}

if (boolQuery.length > 0) {
mustQuery.push({
bool: {
Expand Down Expand Up @@ -465,7 +483,7 @@ async function searchChallenges (currentUser, criteria) {
docs = await esClient.search(esQuery)
} catch (e) {
// Catch error when the ES is fresh and has no data
// logger.error(`Query Error from ES ${JSON.stringify(e)}`)
logger.error(`Query Error from ES ${JSON.stringify(e)}`)
docs = {
hits: {
total: 0,
Expand Down Expand Up @@ -573,7 +591,9 @@ searchChallenges.schema = {
ids: Joi.array().items(Joi.optionalId()).unique().min(1),
isTask: Joi.boolean(),
taskIsAssigned: Joi.boolean(),
taskMemberId: Joi.string()
taskMemberId: Joi.string(),
events: Joi.array().items(Joi.number()),
includeAllEvents: Joi.boolean().default(true)
})
}

Expand Down Expand Up @@ -718,6 +738,12 @@ async function createChallenge (currentUser, challenge, userToken) {
const { track, type } = await validateChallengeData(challenge)
if (_.get(type, 'isTask')) {
_.set(challenge, 'task.isTask', true)
if (_.isUndefined(_.get(challenge, 'task.isAssigned'))) {
_.set(challenge, 'task.isAssigned', false)
}
if (_.isUndefined(_.get(challenge, 'task.memberId'))) {
_.set(challenge, 'task.memberId', null)
}
}
if (challenge.phases && challenge.phases.length > 0) {
await helper.validatePhases(challenge.phases)
Expand Down Expand Up @@ -921,25 +947,27 @@ async function getChallenge (currentUser, id) {
// }
// delete challenge.typeId

// Check if challenge is task and apply security rules
if (_.get(challenge, 'task.isTask', false) && _.get(challenge, 'task.isAssigned', false)) {
if (!currentUser || (!currentUser.isMachine && !helper.hasAdminRole(currentUser) && _.toString(currentUser.userId) !== _.toString(_.get(challenge, 'task.memberId')))) {
throw new errors.ForbiddenError(`You don't have access to view this challenge`)
}
}

let memberChallengeIds
// Remove privateDescription for unregistered users
if (currentUser) {
if (!currentUser.isMachine) {
const ids = await helper.listChallengesByMember(currentUser.userId)
if (!_.includes(ids, challenge.id)) {
memberChallengeIds = await helper.listChallengesByMember(currentUser.userId)
if (!_.includes(memberChallengeIds, challenge.id)) {
_.unset(challenge, 'privateDescription')
}
}
} else {
_.unset(challenge, 'privateDescription')
}

// Check if challenge is task and apply security rules
if (_.get(challenge, 'task.isTask', false) && _.get(challenge, 'task.isAssigned', false)) {
const canAccesChallenge = _.isUndefined(currentUser) ? false : _.includes((memberChallengeIds || []), challenge.id) || currentUser.isMachine || helper.hasAdminRole(currentUser)
if (!canAccesChallenge) {
throw new errors.ForbiddenError(`You don't have access to view this challenge`)
}
}

if (challenge.phases && challenge.phases.length > 0) {
await getPhasesAndPopulate(challenge)
}
Expand Down