Skip to content

Commit

Permalink
Add task token and answer token checks
Browse files Browse the repository at this point in the history
  • Loading branch information
SebastienTainon committed Apr 10, 2024
1 parent 50ee536 commit 66f5e36
Show file tree
Hide file tree
Showing 9 changed files with 305 additions and 172 deletions.
3 changes: 2 additions & 1 deletion schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -599,7 +599,8 @@ DELIMITER ;
CREATE TABLE `tm_platforms` (
`ID` bigint NOT NULL,
`name` varchar(255) NOT NULL,
`public_key` varchar(500) NOT NULL
`public_key` varchar(500) NOT NULL,
`api_url` varchar(200) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3;

-- --------------------------------------------------------
Expand Down
15 changes: 13 additions & 2 deletions src/crypto/jwes_decoder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,23 @@ export class JwesDecoder {
public algorithm = 'ES256';

// Recipient public key to check signature for JWS, own private key to decrypt for JWE
public async setKeys(jwsKey: string|undefined, jweKey: string|undefined): Promise<void> {
if (!jwsKey || !jweKey) {
public async setKeys(jwsKey: string, jweKey: string|undefined = undefined): Promise<void> {
await this.setJwsKey(jwsKey);

if (jweKey) {
await this.setJweKey(jweKey);
}
}

public async setJwsKey(jwsKey: string): Promise<void> {
if (!jwsKey) {
throw new Error('A valid JWS key and a valid JWE key must be fulfilled');
}

this.jwsKey = await jose.importSPKI(jwsKey, this.algorithm);
}

public async setJweKey(jweKey: string): Promise<void> {
this.jweKey = await jose.importPKCS8(jweKey, this.algorithm);
}

Expand Down
1 change: 1 addition & 0 deletions src/db_models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ export interface Platform {
ID: string,
name: string,
public_key: string,
api_url: string,
}

export interface Submission {
Expand Down
3 changes: 3 additions & 0 deletions src/error_handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ export class NotFoundError extends Error {
export class InvalidInputError extends Error {
}

export class PlatformInteractionError extends Error {
}

export function isResponseBoom(response: any): response is Boom {
return (response as Boom).isBoom && (response as Boom).isServer;
}
Expand Down
63 changes: 23 additions & 40 deletions src/grader_interface.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {PlatformTokenParameters, TokenGenerator} from './tokenization';
import {findSourceCodeById, getPlatformTokenParams, SubmissionParameters} from './submissions';
import {TokenGenerator} from './tokenization';
import {findSourceCodeById, SubmissionParameters} from './submissions';
import * as Db from './db';
import {findTaskById} from './tasks';
import {Submission, TaskLimit, TaskLimitModel, TaskTest} from './db_models';
Expand All @@ -8,6 +8,10 @@ import got from 'got';
import log from 'loglevel';
import {getRandomId} from './util';
import appConfig from './config';
import {
PlatformAnswerTokenData,
PlatformTaskTokenData,
} from './platform_interface';

function baseLangToJSONLang(baseLang: string): string {
baseLang = baseLang.toLocaleLowerCase();
Expand Down Expand Up @@ -85,8 +89,8 @@ export function setQueueRequestSender(queueRequestSender: (queueRequest: QueueRe
sendQueueRequestToGraderQueue = queueRequestSender;
}

export async function sendSubmissionToTaskGrader(submissionId: string, submissionData: SubmissionParameters): Promise<void> {
const queueRequest = await generateQueueRequest(submissionId, submissionData);
export async function sendSubmissionToTaskGrader(submissionId: string, submissionData: SubmissionParameters, taskTokenData: PlatformTaskTokenData, answerTokenData: PlatformAnswerTokenData|null): Promise<void> {
const queueRequest = await generateQueueRequest(submissionId, submissionData, taskTokenData, answerTokenData);

let queueAnswer: string;

Expand Down Expand Up @@ -117,40 +121,19 @@ export interface QueueRequest {
sPlatform?: string,
}

export async function generateQueueRequest(submissionId: string, submissionData: SubmissionParameters): Promise<QueueRequest> {
const params = await getPlatformTokenParams(submissionData.taskId, submissionData.token, submissionData.platform);
export async function generateQueueRequest(submissionId: string, submissionData: SubmissionParameters, taskTokenData: PlatformTaskTokenData, answerTokenData: PlatformAnswerTokenData|null): Promise<QueueRequest> {
let idUserAnswer = null;
let answerTokenParams: PlatformTokenParameters|null = null;
if (submissionData.answerToken) {
answerTokenParams = await getPlatformTokenParams(submissionData.taskId, submissionData.answerToken, submissionData.platform);
if (answerTokenParams.idUserAnswer) {
idUserAnswer = answerTokenParams.idUserAnswer;
}
}

if (answerTokenParams && !appConfig.testMode.enabled) {
if (answerTokenParams.idUser !== params.idUser || answerTokenParams.itemUrl !== params.itemUrl) {
throw new InvalidInputError(`Mismatching tokens idUser or itemUrl, token = ${JSON.stringify(params)}, answerToken = ${JSON.stringify(answerTokenParams)}`);
}
if (!answerTokenParams.sAnswer) {
throw new InvalidInputError('Missing answer in answerToken');
}
const decodedAnswer = JSON.parse(answerTokenParams.sAnswer) as {idSubmission?: string};
if (!('idSubmission' in decodedAnswer) || decodedAnswer['idSubmission'] !== submissionId) {
throw new InvalidInputError('Impossible to read submission associated with answer token or submission ID mismatching');
}
if (false === params.bSubmissionPossible || false === params.bAllowGrading) {
throw new InvalidInputError('Token indicates read-only task');
}
if (answerTokenData?.payload.idUserAnswer) {
idUserAnswer = answerTokenData.payload.idUserAnswer;
}

const returnUrl = submissionData?.taskParams?.returnUrl ?? params.returnUrl;
const returnUrl = submissionData?.taskParams?.returnUrl;

if (returnUrl || idUserAnswer) {
await Db.execute('update tm_submissions set sReturnUrl = :returnUrl, idUserAnswer = :idUserAnswer WHERE tm_submissions.`ID` = :idSubmission and tm_submissions.idUser = :idUser and tm_submissions.idPlatform = :idPlatform and tm_submissions.idTask = :idTask;', {
idUser: params.idUser,
idTask: params.idTaskLocal,
idPlatform: params.idPlatform,
idUser: taskTokenData.payload.idUser,
idTask: taskTokenData.idTaskLocal,
idPlatform: taskTokenData.platform.ID,
idSubmission: submissionId,
returnUrl,
idUserAnswer,
Expand All @@ -165,18 +148,18 @@ WHERE tm_submissions.ID = :idSubmission
and tm_submissions.idUser = :idUser
and tm_submissions.idPlatform = :idPlatform
and tm_submissions.idTask = :idTask;`, {
idUser: params.idUser,
idTask: params.idTaskLocal,
idPlatform: params.idPlatform,
idUser: taskTokenData.payload.idUser,
idTask: taskTokenData.idTaskLocal,
idPlatform: taskTokenData.platform.ID,
idSubmission: submissionId,
});
if (null === submission) {
throw new InvalidInputError('Cannot find submission ' + submissionId);
}

const task = await findTaskById(params.idTaskLocal);
const task = await findTaskById(taskTokenData.idTaskLocal);
if (null === task) {
throw new InvalidInputError(`Cannot find task with id ${params.idTaskLocal}`);
throw new InvalidInputError(`Cannot find task with id ${taskTokenData.idTaskLocal}`);
}
const sourceCode = await findSourceCodeById(submission.idSourceCode);
if (null === sourceCode) {
Expand All @@ -190,9 +173,9 @@ WHERE tm_submissions.ID = :idSubmission
let tests: TaskTest[] = [];
if ('UserTest' === submission.sMode) {
tests = await Db.execute<TaskTest[]>('SELECT tm_tasks_tests.* FROM tm_tasks_tests WHERE idUser = :idUser and idPlatform = :idPlatform and idTask = :idTask and idSubmission = :idSubmission ORDER BY iRank ASC', {
idUser: params.idUser,
idTask: params.idTaskLocal,
idPlatform: params.idPlatform,
idUser: taskTokenData.payload.idUser,
idTask: taskTokenData.idTaskLocal,
idPlatform: taskTokenData.platform.ID,
idSubmission: submissionId,
});
}
Expand Down
143 changes: 143 additions & 0 deletions src/platform_interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import got from 'got';
import {
decodePlatformTaskToken, PlatformAnswerTokenPayload,
PlatformGenericTokenPayload,
PlatformTaskTokenPayload,
} from './tokenization';
import appConfig from './config';
import * as Db from './db';
import {Platform} from './db_models';
import {InvalidInputError, PlatformInteractionError} from './error_handler';

export interface PlatformTaskTokenData {
payload: PlatformTaskTokenPayload,
idTaskLocal: string,
platform: Platform,
}

export interface PlatformAnswerTokenData {
payload: PlatformAnswerTokenPayload,
idTaskLocal: string,
platform: Platform,
}

export async function extractPlatformTaskTokenData(token: string|null|undefined, platformName: string|null|undefined, taskId: string|null = null): Promise<PlatformTaskTokenData> {
const platformEntity = await getPlatformByName(platformName);
let payload: PlatformTaskTokenPayload;
try {
payload = await decodePlatformTaskToken<PlatformTaskTokenPayload>(token, platformEntity);
} catch (e) {
if (appConfig.testMode.enabled && null !== taskId) {
payload = getTestTokenParameters(taskId);
} else {
throw e;
}
}

if (!payload.idUser || (!payload.idItem && !payload.itemUrl)) {
throw new InvalidInputError('Missing idUser or idItem in token');
}

return {
payload: payload,
idTaskLocal: await getLocalIdTask(payload),
platform: platformEntity,
};
}

export async function extractPlatformAnswerTaskTokenData(token: string|null|undefined, platformName: string|null|undefined, taskId: string|null = null): Promise<PlatformAnswerTokenData> {
const platformEntity = await getPlatformByName(platformName);
let payload: PlatformAnswerTokenPayload;
try {
payload = await decodePlatformTaskToken<PlatformAnswerTokenPayload>(token, platformEntity);
} catch (e) {
if (appConfig.testMode.enabled && null !== taskId) {
payload = getTestTokenParameters(taskId);
} else {
throw e;
}
}

if (!payload.idUser || (!payload.idItem && !payload.itemUrl)) {
throw new InvalidInputError('Missing idUser or idItem in token');
}

return {
payload: payload,
idTaskLocal: await getLocalIdTask(payload),
platform: platformEntity,
};
}

function getIdFromUrl(itemUrl: string): string|null {
const urlSearchParams = (new URL(itemUrl)).searchParams;
const params = Object.fromEntries(urlSearchParams.entries());

return params['taskId'] ? params['taskId'] : null;
}

async function getLocalIdTask(params: PlatformGenericTokenPayload): Promise<string> {
const idItem = params.idItem || null;
const itemUrl = params.itemUrl || null;
if (itemUrl) {
const id = getIdFromUrl(itemUrl);
if (!id) {
throw new InvalidInputError('Cannot find ID in url ' + itemUrl);
}

return id;
}

const id = await Db.querySingleScalarResult<string>('SELECT ID FROM tm_tasks WHERE sTextId = ?', [idItem]);
if (!id) {
throw new InvalidInputError(`Cannot find task ${idItem || ''}`);
}

return id;
}

function getTestTokenParameters(taskId: string): PlatformTaskTokenPayload {
return {
idUser: appConfig.testMode.userId ? appConfig.testMode.userId : null,
bSubmissionPossible: true,
itemUrl: appConfig.baseUrl ? appConfig.baseUrl + '?taskId=' + taskId : null,
bAccessSolutions: appConfig.testMode.accessSolutions,
nbHintsGiven: appConfig.testMode.nbHintsGiven,
};
}

export async function askPlatformAnswerToken(taskToken: string, answer: string, platform: Platform): Promise<string> {
const platformUrl = `${platform.api_url}/answers`;

const askAnswerTokenRequest = {
answer,
task_token: taskToken,
};

const gotResponse = await got.post(platformUrl, {
headers: {
'content-type': 'application/json',
Cookie: 'access_token=3!6iebvzan0hf3g5w8h34ang33c09pzrkm!beta.opentezos.com!/api/',
},
json: askAnswerTokenRequest,
}).json<{success: boolean, data?: {answer_token: string}}>();

if (!gotResponse.success) {
throw new PlatformInteractionError('Impossible to fetch answer token from platform');
}

return gotResponse.data?.answer_token!;
}

export async function getPlatformByName(platformName?: string|null): Promise<Platform> {
if (!platformName && appConfig.testMode.enabled && appConfig.testMode.platformName) {
platformName = appConfig.testMode.platformName;
}

const platforms = await Db.execute<Platform[]>('SELECT * FROM tm_platforms WHERE name = ?', [platformName]);
if (!platforms.length) {
throw new InvalidInputError(`Cannot find platform ${platformName || ''}`);
}

return platforms[0];
}
17 changes: 16 additions & 1 deletion src/server.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import Hapi, {Lifecycle} from '@hapi/hapi';
import {Server} from '@hapi/hapi';
import {getTask} from './tasks';
import {createSubmission, getSubmission} from './submissions';
import {createOfflineSubmission, createSubmission, getSubmission} from './submissions';
import ReturnValue = Lifecycle.ReturnValue;
import {ErrorHandler, isResponseBoom, NotFoundError} from './error_handler';
import {receiveSubmissionResultsFromTaskGrader} from './grader_webhook';
Expand Down Expand Up @@ -52,6 +52,21 @@ export async function init(): Promise<Server> {
}
});

server.route({
method: 'POST',
path: '/submissions-offline',
options: {
handler: async (request, h) => {
const submissionId = await createOfflineSubmission(request.payload);

return h.response({
success: true,
submissionId,
});
}
}
});

server.route({
method: 'POST',
path: '/task-grader-webhook',
Expand Down
Loading

0 comments on commit 66f5e36

Please sign in to comment.