Skip to content
Open
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
29 changes: 28 additions & 1 deletion apps/backend/apps/client/src/submission/submission.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1125,11 +1125,29 @@ export class SubmissionService {
where: {
id,
problemId,
contestId,
// contestId가 null인 경우 해당 필드를 필터에서 제외한다.
// null로 그대로 넘기면 Prisma가 WHERE contest_id IS NULL 조건을 생성하여
// 대회 종료 후 공개된 문제에서 과거 대회 제출 기록을 조회할 수 없게 되는 버그가 발생한다.
// assignmentId는 null 여부 자체가 보안상 의미가 있으므로 필터를 유지한다.
contestId: contestId ?? undefined,
assignmentId
},
select: {
userId: true,
contest: {
select: {
startTime: true,
endTime: true,
isJudgeResultVisible: true
}
},
assignment: {
select: {
startTime: true,
endTime: true,
isJudgeResultVisible: true
}
},
Comment on lines +1144 to +1150

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

assignment 검사하는 부분이 삭제되어서 JOIN 연산도 필요없을 것 같습니다.
select도 없애주세요

user: {
select: {
username: true
Expand All @@ -1153,6 +1171,15 @@ export class SubmissionService {
throw new EntityNotExistException('Submission')
}

// contestId/assignmentId가 요청에 포함되지 않았지만 실제 submission에 contest/assignment가 있는 경우
// (대회 종료 후 공개된 문제에서 과거 대회 제출 기록을 조회하는 케이스)
// submission의 릴레이션 데이터를 직접 사용하여 진행 중인 대회의 타인 제출 열람을 차단한다.
// contestRecord를 별도로 조회하지 않으므로, 대회 비참가자도 보안 검사를 우회할 수 없다.
if (!contest && submission.contest) {
contest = submission.contest
isJudgeResultVisible = submission.contest.isJudgeResultVisible
}

// 본인이나 관리자가 아닐 경우
if (
submission.userId !== userId &&
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -603,6 +603,113 @@ describe('SubmissionService', () => {
})
).to.be.rejectedWith(ForbiddenAccessException)
})

it('should return own past-contest submission when accessed from public problem page (contestId=null)', async () => {
// 대회 종료 후 공개된 문제에서 본인이 대회 당시 제출한 submission을 조회하는 케이스
// submission의 contest 릴레이션을 통해 대회 정보를 직접 가져옴
const endedContest = {
...mockContest,
startTime: new Date(Date.now() - 20000),
endTime: new Date(Date.now() - 10000), // 이미 종료된 대회
isJudgeResultVisible: true
}
const testcaseResult = submissionResults.map((result) => {
return {
...result,
cpuTime:
result.cpuTime || result.cpuTime === BigInt(0)
? result.cpuTime.toString()
: null
}
})

db.problem.findFirst.resolves(problems[0])
db.submission.findFirst.resolves({
...submissions[0],
contest: endedContest, // 대회 당시 제출이므로 contest 릴레이션이 존재
user: { username: 'username' },
submissionResult: submissionResults
})

expect(
await service.getSubmission({
id: submissions[0].id,
problemId: problems[0].id,
userId: submissions[0].userId, // 본인 조회
userRole: Role.User,
contestId: null, // 공개 문제 페이지에서 contestId 없이 호출
assignmentId: null
})
).to.deep.equal({
problemId: problems[0].id,
username: 'username',
code: submissions[0].code.map((snippet) => snippet.text).join('\n'),
language: submissions[0].language,
createTime: submissions[0].createTime,
result: submissions[0].result,
testcaseResult
})
})

it('should block viewing other users submission from ongoing contest via public page', async () => {
// contestId=null로 공개 엔드포인트를 호출하더라도
// submission의 contest가 진행 중인 대회이면 타인 제출 열람을 차단해야 함
// submission의 릴레이션 데이터를 직접 사용하므로 contestRecord 유무와 무관하게 차단됨
const ongoingContest = {
...mockContest,
startTime: new Date(Date.now() - 10000),
endTime: new Date(Date.now() + 10000)
}

db.problem.findFirst.resolves(problems[0])
db.submission.findFirst.resolves({
...submissions[0],
contest: ongoingContest, // 진행 중인 대회의 릴레이션 데이터
userId: 2 // 타인의 submission
})

await expect(
service.getSubmission({
id: submissions[0].id,
problemId: problems[0].id,
userId: 1, // 다른 유저가 조회 시도
userRole: Role.User,
contestId: null, // contestId 없이 공개 엔드포인트 호출
assignmentId: null
})
).to.be.rejectedWith(ForbiddenAccessException)
})

it('should block non-participant from viewing submission of ongoing contest via public page', async () => {
// 대회에 참가하지 않은 유저가 공개 엔드포인트를 통해
// 진행 중인 대회의 타인 제출물을 조회 시도하는 케이스
// contestRecord가 없어도 submission.contest 릴레이션으로 차단되어야 함
const ongoingContest = {
...mockContest,
startTime: new Date(Date.now() - 10000),
endTime: new Date(Date.now() + 10000)
}

db.problem.findFirst.resolves(problems[0])
db.submission.findFirst.resolves({
...submissions[0],
contest: ongoingContest, // 진행 중인 대회의 릴레이션 데이터
userId: 2 // 타인의 submission
})
// 비참가자이므로 contestRecord가 없음
db.contestRecord.findUnique.resolves(null)

await expect(
service.getSubmission({
id: submissions[0].id,
problemId: problems[0].id,
userId: 1, // 대회에 참가하지 않은 유저
userRole: Role.User,
contestId: null,
assignmentId: null
})
).to.be.rejectedWith(ForbiddenAccessException)
})
})

describe('getContestSubmissions', () => {
Expand Down
Loading