Skip to content

Commit 4b92bf5

Browse files
Revert "Revert "Issue Reviewer Role""
1 parent 580acf4 commit 4b92bf5

File tree

13 files changed

+348
-55
lines changed

13 files changed

+348
-55
lines changed

README.md

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,84 @@ This script will load the data from `scripts/data` directory into ES
115115
npm run start
116116
```
117117

118+
### Local Deployment
119+
0. Make sure to use Node v10+ by command `node -v`. We recommend using [NVM](https://github.com/nvm-sh/nvm) to quickly switch to the right version:
120+
121+
```bash
122+
nvm use
123+
```
124+
125+
1. 📦 Install npm dependencies
126+
127+
```bash
128+
npm install
129+
```
130+
131+
2. ⚙ Local config
132+
In the `submissions-api` root directory create `.env` file with the next environment variables. Values for **Auth0 config** should be shared with you on the forum.<br>
133+
```bash
134+
# AWS related config
135+
AWS_ACCESS_KEY_ID=
136+
AWS_SECRET_ACCESS_KEY=
137+
AWS_REGION=
138+
S3_BUCKET=
139+
ARTIFACT_BUCKET=
140+
141+
# Auth0 config
142+
AUTH0_URL=
143+
AUTH0_PROXY_SERVER_URL=
144+
TOKEN_CACHE_TIME=
145+
AUTH0_AUDIENCE=
146+
AUTH0_CLIENT_ID=
147+
AUTH0_CLIENT_SECRET=
148+
149+
# Locally deployed services (via docker-compose)
150+
ES_HOST=localhost:9200
151+
```
152+
153+
- Values from this file would be automatically used by many `npm` commands.
154+
- ⚠️ Never commit this file or its copy to the repository!
155+
156+
3. 🚢 Start docker-compose with services which are required to start Topcoder Submissions API locally
157+
158+
```bash
159+
npm run services:up
160+
```
161+
- `npm run services:down` can be used to shutdown the docker services
162+
- `npm run services:logs` can be used to view the logs from the docker services
163+
164+
4. ♻ Create tables.
165+
166+
```bash
167+
npm run create-tables
168+
```
169+
5. ♻ Create ES index.
170+
171+
```bash
172+
npm run create-index
173+
```
174+
175+
6. ♻ Init DB, ES
176+
177+
```bash
178+
npm run local:init
179+
```
180+
181+
This command will do 2 things:
182+
- Import the data to the database and index it to ElasticSearch
183+
- Note, to migrate the existing data from DynamoDB to ES, run the following script
184+
```
185+
npm run db-to-es
186+
```
187+
188+
7. 🚀 Start Topcoder Submissions API
189+
190+
```bash
191+
npm run start
192+
```
193+
The Topcoder Submissions API will be served on `http://localhost:3000`
194+
195+
118196
#### Linting JS files
119197

120198
```

app.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,19 @@ _.each(routes, (verbs, url) => {
127127
})
128128
}
129129

130+
if (def.blockByIp) {
131+
actions.push((req, res, next) => {
132+
req.authUser.blockIP = _.find(req.authUser, (value, key) => {
133+
return (key.indexOf('blockIP') !== -1)
134+
})
135+
if (req.authUser.blockIP) {
136+
throw new errors.HttpStatusError(403, 'Access denied')
137+
} else {
138+
next()
139+
}
140+
})
141+
}
142+
130143
actions.push(method)
131144
winston.info(`API : ${verb.toLocaleUpperCase()} ${config.API_VERSION}${url}`)
132145
apiRouter[verb](`${config.API_VERSION}${url}`, helper.autoWrapExpress(actions))

config/default.js

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/**
22
* Default configuration file
33
*/
4-
4+
require('dotenv').config()
55
module.exports = {
66
DISABLE_LOGGING: process.env.DISABLE_LOGGING || false, // If true, logging will be disabled
77
LOG_LEVEL: process.env.LOG_LEVEL || 'debug',
@@ -42,5 +42,15 @@ module.exports = {
4242
AUTH0_PROXY_SERVER_URL: process.env.AUTH0_PROXY_SERVER_URL,
4343
FETCH_CREATED_DATE_START: process.env.FETCH_CREATED_DATE_START || '2021-01-01',
4444
FETCH_PAGE_SIZE: process.env.FETCH_PAGE_SIZE || 500,
45-
MIGRATE_CHALLENGES: process.env.MIGRATE_CHALLENGES || []
45+
MIGRATE_CHALLENGES: process.env.MIGRATE_CHALLENGES || [],
46+
47+
V5TOLEGACYSCORECARDMAPPING: {
48+
'c56a4180-65aa-42ec-a945-5fd21dec0501': 30001363,
49+
'c56a4180-65aa-42ec-a945-5fd21dec0502': 123456789,
50+
'c56a4180-65aa-42ec-a945-5fd21dec0503': 30001031,
51+
'c56a4180-65aa-42ec-a945-5fd21dec0504': 987654321,
52+
'c56a4180-65aa-42ec-a945-5fd21dec0505': 987123456,
53+
'9ecc88e5-a4ee-44a4-8ec1-70bd98022510': 123789456,
54+
'd6d31f34-8ee5-4589-ae65-45652fcc01a6': 30000720
55+
}
4656
}

docs/swagger.yaml

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1757,7 +1757,9 @@ components:
17571757
description: The scoreCardId filter of the reviews associated with the submission.
17581758
required: false
17591759
schema:
1760-
type: integer
1760+
oneOf:
1761+
- type: integer
1762+
- type: string
17611763
filterSubmissionReviewSubmissionId:
17621764
in: query
17631765
name: review.submissionId
@@ -1871,7 +1873,9 @@ components:
18711873
description: The score card id filter for reviews.
18721874
required: false
18731875
schema:
1874-
type: integer
1876+
oneOf:
1877+
- type: integer
1878+
- type: string
18751879
filterReviewSubmissionId:
18761880
in: query
18771881
name: submissionId
@@ -2147,7 +2151,9 @@ components:
21472151
example: a12bc280-65ab-42ec-a945-5fd21dec1567
21482152
description: The review reviewer id.
21492153
scoreCardId:
2150-
type: integer
2154+
oneOf:
2155+
- type: integer
2156+
- type: string
21512157
description: The review score card id.
21522158
example: 123456789
21532159
submissionId:

local/docker-compose.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
version: '3'
2+
services:
3+
esearch:
4+
image: "docker.elastic.co/elasticsearch/elasticsearch:6.8.0"
5+
ports:
6+
- "9200:9200"
7+
environment:
8+
- "discovery.type=single-node"

package.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,11 @@
1919
"test": "mocha test/unit/*.test.js --require test/unit/prepare.js --exit",
2020
"e2e": "mocha test/e2e/*.test.js --require test/e2e/prepare.js --exit",
2121
"cov": "nyc --reporter=html --reporter=text mocha test/unit/*.test.js --require test/unit/prepare.js --exit",
22-
"cov-e2e": "nyc --reporter=html --reporter=text mocha test/e2e/*.test.js --require test/e2e/prepare.js --exit"
22+
"cov-e2e": "nyc --reporter=html --reporter=text mocha test/e2e/*.test.js --require test/e2e/prepare.js --exit",
23+
"services:up": "docker-compose -f ./local/docker-compose.yml up -d",
24+
"services:down": "docker-compose -f ./local/docker-compose.yml down",
25+
"services:logs": "docker-compose -f ./local/docker-compose.yml logs",
26+
"local:init": "npm run init-db && npm run init-es"
2327
},
2428
"dependencies": {
2529
"amazon-s3-uri": "0.0.3",
@@ -30,6 +34,7 @@
3034
"common-errors": "^1.0.4",
3135
"config": "^1.26.2",
3236
"cors": "^2.8.4",
37+
"dotenv": "^8.2.0",
3338
"elasticsearch": "^15.1.1",
3439
"express": "^4.15.4",
3540
"express-fileupload": "^0.4.0",

src/common/helper.js

Lines changed: 89 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,10 @@ function * getLegacyChallengeId (challengeId) {
313313
const response = yield request.get(`${config.CHALLENGEAPI_V5_URL}/${challengeId}`)
314314
.set('Authorization', `Bearer ${token}`)
315315
.set('Content-Type', 'application/json')
316+
if (_.get(response.body, 'legacy.pureV5')) {
317+
// pure V5 challenges don't have a legacy ID
318+
return null
319+
}
316320
const legacyId = parseInt(response.body.legacyId, 10)
317321
logger.debug(`Legacy challenge id is ${legacyId} for v5 challenge id ${challengeId}`)
318322
return legacyId
@@ -375,12 +379,15 @@ function * getSubmissionPhaseId (challengeId) {
375379
const checkPoint = _.filter(phases, { name: 'Checkpoint Submission', isOpen: true })
376380
const submissionPh = _.filter(phases, { name: 'Submission', isOpen: true })
377381
const finalFixPh = _.filter(phases, { name: 'Final Fix', isOpen: true })
382+
const approvalPh = _.filter(phases, { name: 'Approval', isOpen: true })
378383
if (checkPoint.length !== 0) {
379384
phaseId = checkPoint[0].phaseId
380385
} else if (submissionPh.length !== 0) {
381386
phaseId = submissionPh[0].phaseId
382387
} else if (finalFixPh.length !== 0) {
383388
phaseId = finalFixPh[0].phaseId
389+
} else if (approvalPh.length !== 0) {
390+
phaseId = approvalPh[0].phaseId
384391
}
385392
}
386393
return phaseId
@@ -445,7 +452,18 @@ function * checkCreateAccess (authUser, subEntity) {
445452

446453
// Get phases and winner detail from challengeDetails
447454
const phases = challengeDetails.body.phases
448-
const winner = challengeDetails.body.winners
455+
456+
// Check if the User is assigned as the reviewer for the contest
457+
const reviewers = _.filter(currUserRoles, { role: 'Reviewer' })
458+
if (reviewers.length !== 0) {
459+
throw new errors.HttpStatusError(400, `You cannot create a submission for a challenge while you are a reviewer`)
460+
}
461+
462+
// Check if the User is assigned as the iterative reviewer for the contest
463+
const iterativeReviewers = _.filter(currUserRoles, { role: 'Iterative Reviewer' })
464+
if (iterativeReviewers.length !== 0) {
465+
throw new errors.HttpStatusError(400, `You cannot create a submission for a challenge while you are an iterative reviewer`)
466+
}
449467

450468
// Check if the User is assigned as the reviewer for the contest
451469
const reviewers = _.filter(currUserRoles, { role: 'Reviewer' })
@@ -468,14 +486,22 @@ function * checkCreateAccess (authUser, subEntity) {
468486
const submissionPhaseId = yield getSubmissionPhaseId(subEntity.challengeId)
469487

470488
if (submissionPhaseId == null) {
471-
throw new errors.HttpStatusError(403, 'You are not allowed to submit when submission phase is not open')
489+
throw new errors.HttpStatusError(403, 'You cannot create a submission in the current phase')
472490
}
473491

474492
const currPhase = _.filter(phases, { phaseId: submissionPhaseId })
475493

476-
if (currPhase[0].name === 'Final Fix') {
477-
if (!authUser.handle.equals(winner[0].handle)) {
478-
throw new errors.HttpStatusError(403, 'Only winner is allowed to submit during Final Fix phase')
494+
if (currPhase[0].name === 'Final Fix' || currPhase[0].name === 'Approval') {
495+
// Check if the user created a submission in the Submission phase - only such users
496+
// will be allowed to submit during final phase
497+
const userSubmission = yield fetchFromES({
498+
challengeId,
499+
memberId: authUser.userId
500+
}, camelize('Submission'))
501+
502+
// User requesting submission haven't made any submission - prevent them for creating one
503+
if (userSubmission.total === 0) {
504+
throw new errors.HttpStatusError(403, 'You are not expected to create a submission in the current phase')
479505
}
480506
}
481507
} else {
@@ -579,7 +605,7 @@ function * checkGetAccess (authUser, submission) {
579605
const appealsResponseStatus = getPhaseStatus('Appeals Response', challengeDetails.body)
580606

581607
// Appeals Response is not closed yet
582-
if (appealsResponseStatus !== 'Closed') {
608+
if (appealsResponseStatus !== 'Closed' && appealsResponseStatus !== 'Invalid') {
583609
throw new errors.HttpStatusError(403, 'You cannot access other submissions before the end of Appeals Response phase')
584610
} else {
585611
const userSubmission = yield fetchFromES({
@@ -615,10 +641,21 @@ function * checkGetAccess (authUser, submission) {
615641
* @returns {Promise}
616642
*/
617643
function * checkReviewGetAccess (authUser, submission) {
644+
let resources
618645
let challengeDetails
619646
const token = yield getM2Mtoken()
620647
const challengeId = yield getV5ChallengeId(submission.challengeId)
621648

649+
try {
650+
resources = yield request.get(`${config.RESOURCEAPI_V5_BASE_URL}/resources?challengeId=${challengeId}`)
651+
.set('Authorization', `Bearer ${token}`)
652+
.set('Content-Type', 'application/json')
653+
} catch (ex) {
654+
logger.error(`Error while accessing ${config.RESOURCEAPI_V5_BASE_URL}/resources?challengeId=${challengeId}`)
655+
logger.error(ex)
656+
throw new errors.HttpStatusError(503, `Could not determine the user's role in the challenge with id ${challengeId}`)
657+
}
658+
622659
try {
623660
challengeDetails = yield request.get(`${config.CHALLENGEAPI_V5_URL}/${challengeId}`)
624661
.set('Authorization', `Bearer ${token}`)
@@ -629,9 +666,32 @@ function * checkReviewGetAccess (authUser, submission) {
629666
return false
630667
}
631668

632-
if (challengeDetails) {
669+
// Get map of role id to role name
670+
const resourceRolesMap = yield getRoleIdToRoleNameMap()
671+
672+
// Check if role id to role name mapping is available. If not user's role cannot be determined.
673+
if (resourceRolesMap == null || _.size(resourceRolesMap) === 0) {
674+
throw new errors.HttpStatusError(503, `Could not determine the user's role in the challenge with id ${challengeId}`)
675+
}
676+
677+
if (resources && challengeDetails) {
678+
// Fetch all roles of the User pertaining to the current challenge
679+
const currUserRoles = _.filter(resources.body, { memberHandle: authUser.handle })
680+
681+
// Populate the role names for the current user role ids
682+
_.forEach(currUserRoles, currentUserRole => {
683+
currentUserRole.role = resourceRolesMap[currentUserRole.roleId]
684+
})
685+
633686
const subTrack = challengeDetails.body.legacy.subTrack
634687

688+
// Check if the User is a Copilot, Manager or Observer for that contest
689+
const validRoles = ['Copilot', 'Manager', 'Observer']
690+
const passedRoles = currUserRoles.filter(a => validRoles.includes(a.role))
691+
if (passedRoles.length !== 0) {
692+
return true
693+
}
694+
635695
// For Marathon Match, everyone can access review result
636696
if (subTrack === 'DEVELOP_MARATHON_MATCH') {
637697
logger.info('No access check for Marathon match')
@@ -646,6 +706,10 @@ function * checkReviewGetAccess (authUser, submission) {
646706

647707
return true
648708
}
709+
} else {
710+
// We don't have enough details to validate the access
711+
logger.debug('No enough details to validate the Permissions')
712+
throw new errors.HttpStatusError(503, `Not all information could be fetched about challenge with id ${submission.challengeId}`)
649713
}
650714
}
651715

@@ -690,6 +754,7 @@ function * postToBusApi (payload) {
690754
function cleanseReviews (reviews, authUser) {
691755
// Not a machine user
692756
if (!authUser.scopes) {
757+
logger.info('Not a machine user. Filtering reviews...')
693758
const admin = _.filter(authUser.roles, role => role.toLowerCase() === 'Administrator'.toLowerCase())
694759
const copilot = _.filter(authUser.roles, role => role.toLowerCase() === 'Copilot'.toLowerCase())
695760

@@ -815,6 +880,21 @@ function * getLatestChallenges (page) {
815880
}
816881
}
817882

883+
/**
884+
* Get legacy scorecard id if the scorecard id is uuid form
885+
* @param {String} scoreCardId Scorecard ID
886+
* @returns {String} Legacy scorecard ID of the given challengeId
887+
*/
888+
function getLegacyScoreCardId (scoreCardId) {
889+
if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(scoreCardId)) {
890+
logger.debug(`${scoreCardId} detected as uuid. Converting to legacy scorecard id`)
891+
892+
return config.get('V5TOLEGACYSCORECARDMAPPING')[scoreCardId]
893+
}
894+
895+
return scoreCardId
896+
}
897+
818898
module.exports = {
819899
wrapExpress,
820900
autoWrapExpress,
@@ -833,5 +913,6 @@ module.exports = {
833913
getRoleIdToRoleNameMap,
834914
getV5ChallengeId,
835915
adjustSubmissionChallengeId,
836-
getLatestChallenges
916+
getLatestChallenges,
917+
getLegacyScoreCardId
837918
}

0 commit comments

Comments
 (0)