From 0aeeff7298ecf694b2fe8646a9d77ebbc87584fc Mon Sep 17 00:00:00 2001 From: Vasco Santos Date: Thu, 4 Jan 2024 15:42:58 +0100 Subject: [PATCH] feat: bypass maintenance mode for allowed tokens --- packages/api/src/auth.js | 2 +- packages/api/src/env.js | 4 +++ packages/api/src/maintenance.js | 16 +++++++++- packages/api/test/maintenance.spec.js | 44 ++++++++++++++++++++++++++- 4 files changed, 63 insertions(+), 3 deletions(-) diff --git a/packages/api/src/auth.js b/packages/api/src/auth.js index b1e33e9b06..a3e54f90d1 100644 --- a/packages/api/src/auth.js +++ b/packages/api/src/auth.js @@ -266,7 +266,7 @@ function verifyAuthToken (token, decoded, env) { return env.db.getKey(decoded.sub, token) } -function getTokenFromRequest (request, { magic }) { +export function getTokenFromRequest (request, { magic }) { const authHeader = request.headers.get('Authorization') || '' if (!authHeader) { throw new NoTokenError() diff --git a/packages/api/src/env.js b/packages/api/src/env.js index 0b123300aa..035f2ee236 100644 --- a/packages/api/src/env.js +++ b/packages/api/src/env.js @@ -41,6 +41,7 @@ import { Factory as ClaimFactory } from './utils/content-claims.js' * @property {string} [SENTRY_RELEASE] * @property {string} [LOGTAIL_TOKEN] * @property {string} MAINTENANCE_MODE + * @property {string} [MODE_SKIP_LIST] * @property {string} [DANGEROUSLY_BYPASS_MAGIC_AUTH] * @property {string} [ELASTIC_IPFS_PEER_ID] * @property {string} [ENABLE_ADD_TO_CLUSTER] @@ -71,6 +72,7 @@ import { Factory as ClaimFactory } from './utils/content-claims.js' * @property {import('./utils/billing-types').CustomersService} customers * @property {string} stripeSecretKey * @property {string[]} gatewayUrls + * @property {string[]} modeSkipList * @property {import('./utils/content-claims').Factory} [claimFactory] */ @@ -150,6 +152,8 @@ export async function envAll (req, env, ctx) { // @ts-ignore env.MODE = env.MAINTENANCE_MODE || DEFAULT_MODE + env.modeSkipList = env.MODE_SKIP_LIST ? JSON.parse(env.MODE_SKIP_LIST) : [] + env.ELASTIC_IPFS_PEER_ID = env.ELASTIC_IPFS_PEER_ID ?? 'bafzbeibhqavlasjc7dvbiopygwncnrtvjd2xmryk5laib7zyjor6kf3avm' if (!env.LINKDEX_URL && env.ENV !== 'dev') { diff --git a/packages/api/src/maintenance.js b/packages/api/src/maintenance.js index f1b88c83e2..bd2e970d8e 100644 --- a/packages/api/src/maintenance.js +++ b/packages/api/src/maintenance.js @@ -1,4 +1,5 @@ import { HTTPError, MaintenanceError } from './errors.js' +import { getTokenFromRequest } from './auth.js' /** * @typedef {'rw' | 'r-' | '--'} Mode @@ -61,8 +62,21 @@ export function withMode (mode) { }) } + const modeSkip = () => { + if (!request || !env) { + return false + } + const list = env.modeSkipList + const token = getTokenFromRequest(request, env) + + if (list.includes(token)) { + return true + } + return false + } + // Not enabled, use maintenance handler. - if (!enabled()) { + if (!enabled() && !modeSkip()) { return maintenanceHandler() } } diff --git a/packages/api/test/maintenance.spec.js b/packages/api/test/maintenance.spec.js index b1703b0a27..15d534ea21 100644 --- a/packages/api/test/maintenance.spec.js +++ b/packages/api/test/maintenance.spec.js @@ -1,13 +1,25 @@ /* eslint-env mocha */ import assert from 'assert' +import { CID } from 'multiformats/cid' +import { sha256 } from 'multiformats/hashes/sha2' +import fetch from '@web-std/fetch' + import { withMode, READ_WRITE, READ_ONLY, NO_READ_OR_WRITE } from '../src/maintenance.js' +import { CAR_CODE } from '../src/constants.js' + +import { endpoint } from './scripts/constants.js' +import { createCar } from './scripts/car.js' +import { getTestJWT } from './scripts/helpers.js' + +// client needs global fetch +Object.assign(global, { fetch }) -describe('maintenance middleware', () => { +describe.only('maintenance middleware', () => { it('should throw error when in maintenance for a READ_ONLY route', () => { const handler = withMode(READ_ONLY) const block = (request, env) => { @@ -52,6 +64,36 @@ describe('maintenance middleware', () => { }), /API undergoing maintenance/) }) + it('should bypass maintenance mode with a allowed token', async () => { + const issuer = 'test-upload' + const token = await getTestJWT(issuer, issuer) + const { root, car: carBody } = await createCar('dude where\'s my CAR') + const carBytes = new Uint8Array(await carBody.arrayBuffer()) + const expectedCid = root.toString() + const expectedCarCid = CID.createV1(CAR_CODE, await sha256.digest(carBytes)).toString() + + /** @type {import('miniflare').Miniflare} */ + const mf = globalThis.miniflare + const bindings = await mf.getBindings() + bindings['MAINTENANCE_MODE'] = 'r-' + bindings['MODE_SKIP_LIST'] = JSON.stringify([token]) + + const res = await fetch(new URL('car', endpoint), { + method: 'POST', + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/vnd.ipld.car' + }, + body: carBody + }) + const { cid, carCid } = await res.json() + assert.strictEqual(cid, expectedCid, 'Server responded with expected CID') + assert.strictEqual(carCid, expectedCarCid, 'Server responded with expected CAR CID') + + // fallback maintenance mode + bindings['MAINTENANCE_MODE'] = 'rw' + }) + it('should throw for invalid maintenance mode', () => { const handler = withMode(READ_WRITE) const block = (request, env) => {