Skip to content

Commit 371cf2c

Browse files
committed
Allow all users, including anonymous, to pull submission details (not including reviews)
1 parent 59b6584 commit 371cf2c

File tree

7 files changed

+230
-39
lines changed

7 files changed

+230
-39
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@
4949
"pg-boss": "^11.0.5",
5050
"reflect-metadata": "^0.2.2",
5151
"rxjs": "^7.8.1",
52-
"tc-core-library-js": "appirio-tech/tc-core-library-js.git#security"
52+
"tc-core-library-js": "topcoder-platform/tc-core-library-js.git#master"
5353
},
5454
"devDependencies": {
5555
"@eslint/eslintrc": "^3.2.0",

pnpm-lock.yaml

Lines changed: 6 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/api/submission/submission.controller.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,8 @@ export class SubmissionController {
197197
this.logger.log(
198198
`Getting submissions with filters - ${JSON.stringify(queryDto)}`,
199199
);
200-
const authUser: JwtUser = req['user'] as JwtUser;
200+
const authUser: JwtUser =
201+
(req['user'] as JwtUser) ?? ({ isMachine: false, roles: [] } as JwtUser);
201202
return this.service.listSubmission(
202203
authUser,
203204
queryDto,

src/api/submission/submission.service.spec.ts

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -510,6 +510,7 @@ describe('SubmissionService', () => {
510510
reviewType: {
511511
findMany: jest.Mock;
512512
};
513+
$queryRaw: jest.Mock;
513514
};
514515
let prismaErrorServiceMock: { handleError: jest.Mock };
515516
let challengePrismaMock: {
@@ -537,9 +538,14 @@ describe('SubmissionService', () => {
537538
reviewType: {
538539
findMany: jest.fn().mockResolvedValue([]),
539540
},
541+
$queryRaw: jest.fn().mockResolvedValue([]),
540542
};
541543
prismaErrorServiceMock = {
542-
handleError: jest.fn(),
544+
handleError: jest.fn().mockReturnValue({
545+
message: 'Unexpected error',
546+
code: 'INTERNAL_ERROR',
547+
details: {},
548+
}),
543549
};
544550
challengePrismaMock = {
545551
$queryRaw: jest.fn().mockResolvedValue([]),
@@ -624,6 +630,7 @@ describe('SubmissionService', () => {
624630
prismaMock.submission.findFirst.mockResolvedValue({
625631
id: 'submission-new',
626632
});
633+
prismaMock.$queryRaw.mockResolvedValue([{ id: 'submission-new' }]);
627634

628635
const result = await listService.listSubmission(
629636
{ isMachine: false } as any,
@@ -1241,5 +1248,56 @@ describe('SubmissionService', () => {
12411248
expect(screeningReview?.reviewerHandle).toBe('screeningHandle');
12421249
expect(screeningReview?.reviewerMaxRating).toBe(2000);
12431250
});
1251+
1252+
it('exposes submitter identity but strips reviews for anonymous challenge queries', async () => {
1253+
const now = new Date('2025-02-01T12:00:00Z');
1254+
const submissions = [
1255+
{
1256+
id: 'submission-anon',
1257+
challengeId: 'challenge-1',
1258+
memberId: '101',
1259+
submittedDate: now,
1260+
createdAt: now,
1261+
updatedAt: now,
1262+
type: SubmissionType.CONTEST_SUBMISSION,
1263+
status: SubmissionStatus.ACTIVE,
1264+
review: [{ id: 'review-public', score: 100 }],
1265+
reviewSummation: [{ id: 'summation-public' }],
1266+
url: 'https://example.com/submission.zip',
1267+
legacyChallengeId: null,
1268+
prizeId: null,
1269+
},
1270+
];
1271+
1272+
prismaMock.submission.findMany.mockResolvedValue(
1273+
submissions.map((entry) => ({ ...entry })),
1274+
);
1275+
prismaMock.submission.count.mockResolvedValue(submissions.length);
1276+
prismaMock.submission.findFirst.mockResolvedValue({
1277+
id: 'submission-anon',
1278+
});
1279+
1280+
memberPrismaMock.member.findMany.mockResolvedValue([
1281+
{
1282+
userId: BigInt(101),
1283+
handle: 'anonUser',
1284+
maxRating: { rating: 1500 },
1285+
},
1286+
]);
1287+
1288+
const result = await listService.listSubmission(
1289+
{ isMachine: false, roles: [] } as any,
1290+
{ challengeId: 'challenge-1' } as any,
1291+
{ page: 1, perPage: 10 } as any,
1292+
);
1293+
1294+
const submissionResult = result.data[0];
1295+
expect(submissionResult.memberId).toBe('101');
1296+
expect(submissionResult.submitterHandle).toBe('anonUser');
1297+
expect(submissionResult.submitterMaxRating).toBe(1500);
1298+
expect(submissionResult).not.toHaveProperty('review');
1299+
expect(submissionResult).not.toHaveProperty('reviewSummation');
1300+
expect(submissionResult.url).toBeNull();
1301+
});
12441302
});
12451303
});

src/api/submission/submission.service.ts

Lines changed: 95 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1808,9 +1808,11 @@ export class SubmissionService {
18081808
: undefined;
18091809

18101810
if (requestedMemberId) {
1811-
const userId = authUser.userId ? String(authUser.userId) : undefined;
1811+
const userId = authUser?.userId
1812+
? String(authUser.userId)
1813+
: undefined;
18121814
const isRequestingMember = userId === requestedMemberId;
1813-
const hasCopilotRole = (authUser.roles ?? []).includes(
1815+
const hasCopilotRole = (authUser?.roles ?? []).includes(
18141816
UserRole.Copilot,
18151817
);
18161818
const hasElevatedAccess = isAdmin(authUser) || hasCopilotRole;
@@ -1858,7 +1860,11 @@ export class SubmissionService {
18581860
: '';
18591861

18601862
let restrictedChallengeIds = new Set<string>();
1861-
if (!isPrivilegedRequester && requesterUserId) {
1863+
if (
1864+
!isPrivilegedRequester &&
1865+
requesterUserId &&
1866+
!queryDto.challengeId
1867+
) {
18621868
try {
18631869
restrictedChallengeIds =
18641870
await this.getActiveSubmitterRestrictedChallengeIds(
@@ -1881,7 +1887,8 @@ export class SubmissionService {
18811887
if (
18821888
!isPrivilegedRequester &&
18831889
requesterUserId &&
1884-
restrictedChallengeIds.size
1890+
restrictedChallengeIds.size &&
1891+
!queryDto.challengeId
18851892
) {
18861893
const restrictedList = Array.from(restrictedChallengeIds);
18871894
const restrictionCriteria: Prisma.submissionWhereInput = {
@@ -1924,12 +1931,13 @@ export class SubmissionService {
19241931
orderBy,
19251932
});
19261933

1927-
// Enrich with submitter handle and max rating for authorized callers
1928-
const canViewSubmitter = await this.canViewSubmitterIdentity(
1929-
authUser,
1930-
queryDto.challengeId,
1931-
);
1932-
if (canViewSubmitter && submissions.length) {
1934+
// Enrich with submitter handle and max rating (always for challenge listings)
1935+
const shouldEnrichSubmitter =
1936+
submissions.length > 0 &&
1937+
(queryDto.challengeId
1938+
? true
1939+
: await this.canViewSubmitterIdentity(authUser, queryDto.challengeId));
1940+
if (shouldEnrichSubmitter) {
19331941
try {
19341942
const memberIds = Array.from(
19351943
new Set(
@@ -1939,11 +1947,24 @@ export class SubmissionService {
19391947
),
19401948
);
19411949
if (memberIds.length) {
1942-
const idsAsBigInt = memberIds.map((id) => BigInt(id));
1943-
const members = await this.memberPrisma.member.findMany({
1944-
where: { userId: { in: idsAsBigInt } },
1945-
include: { maxRating: true },
1946-
});
1950+
const idsAsBigInt: bigint[] = [];
1951+
for (const id of memberIds) {
1952+
try {
1953+
idsAsBigInt.push(BigInt(id));
1954+
} catch (error) {
1955+
this.logger.debug(
1956+
`[listSubmission] Skipping submitter ${id}: unable to convert to BigInt. ${error}`,
1957+
);
1958+
}
1959+
}
1960+
1961+
const members =
1962+
idsAsBigInt.length > 0
1963+
? await this.memberPrisma.member.findMany({
1964+
where: { userId: { in: idsAsBigInt } },
1965+
include: { maxRating: true },
1966+
})
1967+
: [];
19471968
const map = new Map<
19481969
string,
19491970
{ handle: string; maxRating: number | null }
@@ -1997,11 +2018,6 @@ export class SubmissionService {
19972018
submissions,
19982019
reviewVisibilityContext,
19992020
);
2000-
this.stripSubmitterMemberIds(
2001-
authUser,
2002-
submissions,
2003-
reviewVisibilityContext,
2004-
);
20052021
await this.stripIsLatestForUnlimitedChallenges(submissions);
20062022

20072023
this.logger.log(
@@ -2404,25 +2420,43 @@ export class SubmissionService {
24042420
return emptyContext;
24052421
}
24062422

2423+
const requesterUserId =
2424+
authUser?.userId !== undefined && authUser?.userId !== null
2425+
? String(authUser.userId).trim()
2426+
: '';
2427+
24072428
const isPrivilegedRequester = authUser?.isMachine || isAdmin(authUser);
2429+
if (!isPrivilegedRequester && !requesterUserId) {
2430+
for (const submission of submissions) {
2431+
if (Object.prototype.hasOwnProperty.call(submission, 'review')) {
2432+
delete (submission as any).review;
2433+
}
2434+
if (
2435+
Object.prototype.hasOwnProperty.call(submission, 'reviewSummation')
2436+
) {
2437+
delete (submission as any).reviewSummation;
2438+
}
2439+
}
2440+
return {
2441+
...emptyContext,
2442+
requesterUserId,
2443+
};
2444+
}
2445+
24082446
if (isPrivilegedRequester) {
2409-
const requesterUserId =
2410-
authUser?.userId !== undefined && authUser?.userId !== null
2411-
? String(authUser.userId).trim()
2412-
: '';
24132447
return {
24142448
...emptyContext,
24152449
requesterUserId,
24162450
};
24172451
}
24182452

2419-
const uid =
2420-
authUser?.userId !== undefined && authUser?.userId !== null
2421-
? String(authUser.userId).trim()
2422-
: '';
2453+
const uid = requesterUserId;
24232454

24242455
if (!uid) {
2425-
return emptyContext;
2456+
return {
2457+
...emptyContext,
2458+
requesterUserId,
2459+
};
24262460
}
24272461

24282462
const challengeIds = Array.from(
@@ -3393,7 +3427,26 @@ export class SubmissionService {
33933427
}
33943428

33953429
const uid = visibilityContext.requesterUserId;
3430+
this.logger.debug(
3431+
`[stripSubmitterSubmissionDetails] requesterUserId=${uid ?? '<undefined>'}`,
3432+
);
33963433
if (!uid) {
3434+
this.logger.debug(
3435+
'[stripSubmitterSubmissionDetails] Anonymized requester; removing review metadata and URLs.',
3436+
);
3437+
for (const submission of submissions) {
3438+
if (Object.prototype.hasOwnProperty.call(submission, 'review')) {
3439+
delete (submission as any).review;
3440+
}
3441+
if (
3442+
Object.prototype.hasOwnProperty.call(submission, 'reviewSummation')
3443+
) {
3444+
delete (submission as any).reviewSummation;
3445+
}
3446+
if (Object.prototype.hasOwnProperty.call(submission, 'url')) {
3447+
(submission as any).url = null;
3448+
}
3449+
}
33973450
return;
33983451
}
33993452

@@ -3468,6 +3521,19 @@ export class SubmissionService {
34683521

34693522
const uid = visibilityContext.requesterUserId;
34703523
if (!uid) {
3524+
for (const submission of submissions) {
3525+
if (Object.prototype.hasOwnProperty.call(submission, 'review')) {
3526+
delete (submission as any).review;
3527+
}
3528+
if (
3529+
Object.prototype.hasOwnProperty.call(submission, 'reviewSummation')
3530+
) {
3531+
delete (submission as any).reviewSummation;
3532+
}
3533+
if (Object.prototype.hasOwnProperty.call(submission, 'url')) {
3534+
(submission as any).url = null;
3535+
}
3536+
}
34713537
return;
34723538
}
34733539

src/shared/guards/tokenRoles.guard.spec.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ describe('TokenRolesGuard', () => {
2020
type TestRequest = Record<string, unknown> & {
2121
method: string;
2222
query: Record<string, unknown>;
23-
user: {
23+
user?: {
2424
userId: string;
2525
isMachine: boolean;
2626
roles?: unknown[];
@@ -132,5 +132,16 @@ describe('TokenRolesGuard', () => {
132132

133133
expect(() => guard.canActivate(context)).toThrow(ForbiddenException);
134134
});
135+
136+
it('allows anonymous access when challengeId is provided', () => {
137+
const request = {
138+
method: 'GET',
139+
query: { challengeId: '12345' },
140+
};
141+
142+
const context = createExecutionContext(request as TestRequest);
143+
144+
expect(guard.canActivate(context)).toBe(true);
145+
});
135146
});
136147
});

0 commit comments

Comments
 (0)