diff --git a/api/bin/spark.js b/api/bin/spark.js index cd9ea082..3d30c99d 100644 --- a/api/bin/spark.js +++ b/api/bin/spark.js @@ -15,6 +15,7 @@ const { DOMAIN = 'localhost', DATABASE_URL, DEAL_INGESTER_TOKEN, + CHECKER_TOKEN, REQUEST_LOGGING = 'true' } = process.env @@ -24,6 +25,9 @@ const { // The same token is configured in Fly.io secrets for the deal-observer service too. assert(DEAL_INGESTER_TOKEN, 'DEAL_INGESTER_TOKEN is required') +// This token is used by permissioned Spark nodes to authenticate measurement creation requests +assert(CHECKER_TOKEN, 'CHECKER_TOKEN is required') + const client = new pg.Pool({ connectionString: DATABASE_URL, // allow the pool to close all connections and become empty @@ -77,6 +81,7 @@ const handler = await createHandler({ client, logger, dealIngestionAccessToken: DEAL_INGESTER_TOKEN, + checkerToken: CHECKER_TOKEN, domain: DOMAIN }) diff --git a/api/index.js b/api/index.js index 8bc2e057..7cd578bc 100644 --- a/api/index.js +++ b/api/index.js @@ -18,8 +18,9 @@ import { ethAddressFromDelegated } from '@glif/filecoin-address' * @param {pg.Client} client * @param {string} domain * @param {string} dealIngestionAccessToken + * @param {string} checkerToken */ -const handler = async (req, res, client, domain, dealIngestionAccessToken) => { +const handler = async (req, res, client, domain, dealIngestionAccessToken, checkerToken) => { if (req.headers.host.split(':')[0] !== domain) { return redirect(req, res, `https://${domain}${req.url}`, 301) } @@ -31,7 +32,7 @@ const handler = async (req, res, client, domain, dealIngestionAccessToken) => { } else if (segs[0] === 'retrievals' && req.method === 'GET') { assert.fail(410, 'This API endpoint is no longer supported.') } else if (segs[0] === 'measurements' && req.method === 'POST') { - await createMeasurement(req, res, client) + await createMeasurement(req, res, client, checkerToken) } else if (segs[0] === 'measurements' && req.method === 'GET') { await getMeasurement(req, res, client, Number(segs[1])) } else if (segs[0] === 'rounds' && segs[1] === 'meridian' && req.method === 'GET') { @@ -53,7 +54,9 @@ const handler = async (req, res, client, domain, dealIngestionAccessToken) => { } } -const createMeasurement = async (req, res, client) => { +const createMeasurement = async (req, res, client, checkerToken) => { + assert.strictEqual(req.headers.authorization, `Bearer ${checkerToken}`, 403) + const body = await getRawBody(req, { limit: '100kb' }) const measurement = JSON.parse(body.toString()) validate(measurement, 'sparkVersion', { type: 'string', required: false }) @@ -460,13 +463,14 @@ export const ingestEligibleDeals = async (req, res, client, dealIngestionAccessT export const createHandler = async ({ client, logger, + domain, dealIngestionAccessToken, - domain + checkerToken }) => { return (req, res) => { const start = new Date() logger.request(`${req.method} ${req.url} ...`) - handler(req, res, client, domain, dealIngestionAccessToken) + handler(req, res, client, domain, dealIngestionAccessToken, checkerToken) .catch(err => errorHandler(res, err, logger)) .then(() => { logger.request(`${req.method} ${req.url} ${res.statusCode} (${new Date().getTime() - start.getTime()}ms)`) diff --git a/api/test/test.js b/api/test/test.js index d730e94c..ebf29137 100644 --- a/api/test/test.js +++ b/api/test/test.js @@ -18,6 +18,7 @@ const sparkVersion = '1.17.0' // This must be in sync with the minimum supported const currentSparkRoundNumber = 42n const VALID_DEAL_INGESTION_TOKEN = 'authorized-token' +const VALID_CHECKER_TOKEN = 'authorized-checker-token' const VALID_MEASUREMENT = { cid: 'bafytest', @@ -74,6 +75,7 @@ describe('Routes', () => { request () {} }, dealIngestionAccessToken: VALID_DEAL_INGESTION_TOKEN, + checkerToken: VALID_CHECKER_TOKEN, domain: '127.0.0.1' }) server = http.createServer(handler) @@ -150,7 +152,7 @@ describe('Routes', () => { const createRequest = await fetch(`${spark}/measurements`, { method: 'POST', - headers: { 'Content-Type': 'application/json' }, + headers: { 'Content-Type': 'application/json', authorization: `Bearer ${VALID_CHECKER_TOKEN}` }, body: JSON.stringify(measurement) }) await assertResponseStatus(createRequest, 200) @@ -207,7 +209,7 @@ describe('Routes', () => { const createRequest = await fetch(`${spark}/measurements`, { method: 'POST', - headers: { 'Content-Type': 'application/json' }, + headers: { 'Content-Type': 'application/json', authorization: `Bearer ${VALID_CHECKER_TOKEN}` }, body: JSON.stringify(measurement) }) await assertResponseStatus(createRequest, 200) @@ -234,7 +236,7 @@ describe('Routes', () => { const createRequest = await fetch(`${spark}/measurements`, { method: 'POST', - headers: { 'Content-Type': 'application/json' }, + headers: { 'Content-Type': 'application/json', authorization: `Bearer ${VALID_CHECKER_TOKEN}` }, body: JSON.stringify(measurement) }) await assertResponseStatus(createRequest, 200) @@ -261,7 +263,7 @@ describe('Routes', () => { const createRequest = await fetch(`${spark}/measurements`, { method: 'POST', - headers: { 'Content-Type': 'application/json' }, + headers: { 'Content-Type': 'application/json', authorization: `Bearer ${VALID_CHECKER_TOKEN}` }, body: JSON.stringify(measurement) }) await assertResponseStatus(createRequest, 400) @@ -280,7 +282,7 @@ describe('Routes', () => { const createRequest = await fetch(`${spark}/measurements`, { method: 'POST', - headers: { 'Content-Type': 'application/json' }, + headers: { 'Content-Type': 'application/json', authorization: `Bearer ${VALID_CHECKER_TOKEN}` }, body: JSON.stringify(measurement) }) await assertResponseStatus(createRequest, 200) @@ -324,7 +326,7 @@ describe('Routes', () => { const res = await fetch(`${spark}/measurements`, { method: 'POST', - headers: { 'Content-Type': 'application/json' }, + headers: { 'Content-Type': 'application/json', authorization: `Bearer ${VALID_CHECKER_TOKEN}` }, body: JSON.stringify(measurement) }) await assertResponseStatus(res, 410) @@ -347,7 +349,7 @@ describe('Routes', () => { const createRequest = await fetch(`${spark}/measurements`, { method: 'POST', - headers: { 'Content-Type': 'application/json' }, + headers: { 'Content-Type': 'application/json', authorization: `Bearer ${VALID_CHECKER_TOKEN}` }, body: JSON.stringify(measurement) }) await assertResponseStatus(createRequest, 200) @@ -377,7 +379,7 @@ describe('Routes', () => { const createRequest = await fetch(`${spark}/measurements`, { method: 'POST', - headers: { 'Content-Type': 'application/json' }, + headers: { 'Content-Type': 'application/json', authorization: `Bearer ${VALID_CHECKER_TOKEN}` }, body: JSON.stringify(measurement) }) await assertResponseStatus(createRequest, 200) @@ -412,7 +414,7 @@ describe('Routes', () => { for (const measurement of measurements) { const res = await fetch(`${spark}/measurements`, { method: 'POST', - headers: { 'Content-Type': 'application/json' }, + headers: { 'Content-Type': 'application/json', authorization: `Bearer ${VALID_CHECKER_TOKEN}` }, body: JSON.stringify(measurement) }) await assertResponseStatus(res, 400) @@ -423,6 +425,38 @@ describe('Routes', () => { const { rows } = await client.query('SELECT id FROM measurements') assert.deepStrictEqual(rows, []) }) + + it('rejects unauthorized measurement', async () => { + await client.query('DELETE FROM measurements') + + const res = await fetch(`${spark}/measurements`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(VALID_MEASUREMENT) + }) + await assertResponseStatus(res, 403) + const body = await res.text() + assert.strictEqual(body, 'Forbidden') + + const { rows } = await client.query('SELECT id FROM measurements') + assert.deepStrictEqual(rows, []) + }) + + it('rejects measurement with wrong authorizaiton token', async () => { + await client.query('DELETE FROM measurements') + + const res = await fetch(`${spark}/measurements`, { + method: 'POST', + headers: { 'Content-Type': 'application/json', authorization: 'Bearer invalid' }, + body: JSON.stringify(VALID_MEASUREMENT) + }) + await assertResponseStatus(res, 403) + const body = await res.text() + assert.strictEqual(body, 'Forbidden') + + const { rows } = await client.query('SELECT id FROM measurements') + assert.deepStrictEqual(rows, []) + }) }) describe('GET /measurements/:id', () => { @@ -430,7 +464,7 @@ describe('Routes', () => { const measurement = { ...VALID_MEASUREMENT } const createRequest = await fetch(`${spark}/measurements`, { method: 'POST', - headers: { 'Content-Type': 'application/json' }, + headers: { 'Content-Type': 'application/json', authorization: `Bearer ${VALID_CHECKER_TOKEN}` }, body: JSON.stringify(measurement) }) await assertResponseStatus(createRequest, 200) @@ -640,7 +674,11 @@ describe('Routes', () => { `) const res = await fetch(`${spark}/measurements`, { method: 'POST', - body: JSON.stringify(VALID_MEASUREMENT) + body: JSON.stringify(VALID_MEASUREMENT), + headers: { + 'Content-Type': 'application/json', + authorization: `Bearer ${VALID_CHECKER_TOKEN}` + } }) await assertResponseStatus(res, 200) const body = await res.json() @@ -661,6 +699,7 @@ describe('Routes', () => { request () {} }, dealIngestionAccessToken: VALID_DEAL_INGESTION_TOKEN, + checkerToken: VALID_CHECKER_TOKEN, domain: 'foobar' }) server = http.createServer(handler)