Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: bypass maintenance mode for allowed tokens #2345

Merged
merged 2 commits into from
Jan 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/api/src/auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
4 changes: 4 additions & 0 deletions packages/api/src/env.js
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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]
*/

Expand Down Expand Up @@ -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') {
Expand Down
17 changes: 16 additions & 1 deletion packages/api/src/maintenance.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { HTTPError, MaintenanceError } from './errors.js'
import { getTokenFromRequest } from './auth.js'

/**
* @typedef {'rw' | 'r-' | '--'} Mode
Expand Down Expand Up @@ -61,8 +62,22 @@ export function withMode (mode) {
})
}

const modeSkip = () => {
if (!request.headers) {
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()
}
}
Expand Down
59 changes: 59 additions & 0 deletions packages/api/test/maintenance.spec.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,23 @@
/* 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', () => {
it('should throw error when in maintenance for a READ_ONLY route', () => {
Expand Down Expand Up @@ -52,6 +64,53 @@ describe('maintenance middleware', () => {
}), /API undergoing maintenance/)
})

it('should bypass maintenance mode with a allowed token', async () => {
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()

// Allowed token
const issuer = 'test-upload'
const token = await getTestJWT(issuer, issuer)

/** @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
})
assert.strictEqual(res.status, 200)
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')

// Not allowed token
const notAllowedIssuer = 'test-upload-not-allowed'
const notAllowedToken = await getTestJWT(notAllowedIssuer, notAllowedIssuer)

const notAllowedRes = await fetch(new URL('car', endpoint), {
method: 'POST',
headers: {
Authorization: `Bearer ${notAllowedToken}`,
'Content-Type': 'application/vnd.ipld.car'
},
body: carBody
})
assert.strictEqual(notAllowedRes.status, 503)

// fallback maintenance mode
bindings.MAINTENANCE_MODE = 'rw'
})

it('should throw for invalid maintenance mode', () => {
const handler = withMode(READ_WRITE)
const block = (request, env) => {
Expand Down
Loading