Request validation for cron (#5307)

* chore: add validations for /api/cron.*

* fix: coerce limit to number

* fix: review
This commit is contained in:
Apoorv Mishra
2023-05-07 10:41:20 +05:30
committed by GitHub
parent 3421f24896
commit c8ee501377
5 changed files with 140 additions and 24 deletions

View File

@@ -1,10 +0,0 @@
import { getTestServer } from "@server/test/support";
const server = getTestServer();
describe("#cron.daily", () => {
it("should require authentication", async () => {
const res = await server.post("/api/cron.daily");
expect(res.status).toEqual(401);
});
});

View File

@@ -0,0 +1,111 @@
import { getTestServer } from "@server/test/support";
const server = getTestServer();
describe("POST /api/cron.daily", () => {
it("should require token", async () => {
const res = await server.post("/api/cron.daily");
const body = await res.json();
expect(res.status).toEqual(400);
expect(body.message).toBe("token is required");
});
it("should validate token", async () => {
const res = await server.post("/api/cron.daily", {
body: {
token: "token",
},
});
const body = await res.json();
expect(res.status).toEqual(401);
expect(body.message).toBe("Invalid secret token");
});
it("should validate limit", async () => {
const res = await server.post("/api/cron.daily", {
body: {
limit: -1,
},
});
const body = await res.json();
expect(res.status).toEqual(400);
expect(body.message).toBe("limit: Number must be greater than 0");
});
});
describe("GET /api/cron.daily", () => {
it("should require token", async () => {
const res = await server.get("/api/cron.daily");
const body = await res.json();
expect(res.status).toEqual(400);
expect(body.message).toBe("token is required");
});
it("should validate token", async () => {
const res = await server.get("/api/cron.daily?token=token");
const body = await res.json();
expect(res.status).toEqual(401);
expect(body.message).toBe("Invalid secret token");
});
it("should validate limit", async () => {
const res = await server.get("/api/cron.daily?limit=-1");
const body = await res.json();
expect(res.status).toEqual(400);
expect(body.message).toBe("limit: Number must be greater than 0");
});
});
describe("POST /api/utils.gc", () => {
it("should require token", async () => {
const res = await server.post("/api/utils.gc");
const body = await res.json();
expect(res.status).toEqual(400);
expect(body.message).toBe("token is required");
});
it("should validate token", async () => {
const res = await server.post("/api/utils.gc", {
body: {
token: "token",
},
});
const body = await res.json();
expect(res.status).toEqual(401);
expect(body.message).toBe("Invalid secret token");
});
it("should validate limit", async () => {
const res = await server.post("/api/utils.gc", {
body: {
limit: -1,
},
});
const body = await res.json();
expect(res.status).toEqual(400);
expect(body.message).toBe("limit: Number must be greater than 0");
});
});
describe("GET /api/utils.gc", () => {
it("should require token", async () => {
const res = await server.get("/api/utils.gc");
const body = await res.json();
expect(res.status).toEqual(400);
expect(body.message).toBe("token is required");
});
it("should validate token", async () => {
const res = await server.get("/api/utils.gc?token=token");
const body = await res.json();
expect(res.status).toEqual(401);
expect(body.message).toBe("Invalid secret token");
});
it("should validate limit", async () => {
const res = await server.get("/api/utils.gc?limit=-1");
const body = await res.json();
expect(res.status).toEqual(400);
expect(body.message).toBe("limit: Number must be greater than 0");
});
});

View File

@@ -1,21 +1,17 @@
import crypto from "crypto";
import { Context } from "koa";
import Router from "koa-router";
import env from "@server/env";
import { AuthenticationError } from "@server/errors";
import validate from "@server/middlewares/validate";
import tasks from "@server/queues/tasks";
import { APIContext } from "@server/types";
import * as T from "./schema";
const router = new Router();
const cronHandler = async (ctx: Context) => {
const token =
ctx.method === "POST" ? ctx.request.body?.token : ctx.query.token;
const limit =
(ctx.method === "POST" ? ctx.request.body?.limit : ctx.query.limit) ?? 500;
if (!token || typeof token !== "string") {
throw AuthenticationError("Token is required");
}
const cronHandler = async (ctx: APIContext<T.CronSchemaReq>) => {
const token = (ctx.input.body.token ?? ctx.input.query.token) as string;
const limit = ctx.input.body.limit ?? ctx.input.query.limit;
if (
token.length !== env.UTILS_SECRET.length ||
@@ -39,11 +35,11 @@ const cronHandler = async (ctx: Context) => {
};
};
router.get("cron.:period", cronHandler);
router.post("cron.:period", cronHandler);
router.get("cron.:period", validate(T.CronSchema), cronHandler);
router.post("cron.:period", validate(T.CronSchema), cronHandler);
// For backwards compatibility
router.get("utils.gc", cronHandler);
router.post("utils.gc", cronHandler);
router.get("utils.gc", validate(T.CronSchema), cronHandler);
router.post("utils.gc", validate(T.CronSchema), cronHandler);
export default router;

View File

@@ -0,0 +1 @@
export { default } from "./cron";

View File

@@ -0,0 +1,18 @@
import { isEmpty } from "lodash";
import { z } from "zod";
import BaseSchema from "../BaseSchema";
export const CronSchema = BaseSchema.extend({
body: z.object({
token: z.string().optional(),
limit: z.coerce.number().gt(0).default(500),
}),
query: z.object({
token: z.string().optional(),
limit: z.coerce.number().gt(0).default(500),
}),
}).refine((req) => !(isEmpty(req.body.token) && isEmpty(req.query.token)), {
message: "token is required",
});
export type CronSchemaReq = z.infer<typeof CronSchema>;