From ec91ae7ea463a1c3c50b6feaf770a27b6640f1f5 Mon Sep 17 00:00:00 2001 From: Hannah Howard Date: Tue, 2 Jul 2024 01:29:41 -0700 Subject: [PATCH] feat(api): add create aspect to maintainence (#2708) # Goals It seems we need a more selective maintainence mode. # Implementation add create aspect to maintaince to more selectively disable will need a slightly sensitive deploy step --- .env.tpl | 2 +- packages/api/src/config.js | 14 +++++++++ packages/api/src/index.js | 29 +++++++++--------- packages/api/src/middleware/maintenance.js | 22 ++++++++++---- packages/api/test/config.spec.js | 21 +++++++++++-- packages/api/test/maintenance.spec.js | 35 ++++++++++++++++++++-- packages/api/test/scripts/globals.js | 2 +- 7 files changed, 98 insertions(+), 27 deletions(-) diff --git a/.env.tpl b/.env.tpl index 7346843dfb..d91fe74581 100644 --- a/.env.tpl +++ b/.env.tpl @@ -37,7 +37,7 @@ PICKUP_BASIC_AUTH_TOKEN = dGVzdDp0ZXN0 PICKUP_API_URL = http://127.0.0.1:9094 # Maintenance Mode -MAINTENANCE_MODE = rw +MAINTENANCE_MODE = rwc # S3 S3_ENDPOINT = http://127.0.0.1:9000 diff --git a/packages/api/src/config.js b/packages/api/src/config.js index 50603f5259..a23c45fd07 100644 --- a/packages/api/src/config.js +++ b/packages/api/src/config.js @@ -1,6 +1,9 @@ import { modes as MaintenanceModes, DEFAULT_MODE, + NO_READ_OR_WRITE, + READ_ONLY, + READ_WRITE_ONLY, } from './middleware/maintenance.js' /** @@ -193,6 +196,17 @@ function maintenanceModeFromString(s) { return m } } + /** @type {Record} */ + const legacyModeMappings = { + '--': NO_READ_OR_WRITE, + 'r-': READ_ONLY, + rw: READ_WRITE_ONLY, + } + for (const [key, value] of Object.entries(legacyModeMappings)) { + if (s === key) { + return value + } + } throw new Error( `invalid maintenance mode value "${s}". valid choices: ${MaintenanceModes}` ) diff --git a/packages/api/src/index.js b/packages/api/src/index.js index 1ef0931dfa..b94a2ad9ab 100644 --- a/packages/api/src/index.js +++ b/packages/api/src/index.js @@ -31,7 +31,8 @@ import { getServiceConfig } from './config.js' import { withMode, READ_ONLY as RO, - READ_WRITE as RW, + READ_WRITE_ONLY as RW, + READ_WRITE_CREATE as RWC, } from './middleware/maintenance.js' import { getContext } from './utils/context.js' import { withAuth } from './middleware/auth.js' @@ -96,7 +97,7 @@ r.add( r.add( 'post', '/pins', - withAuth(withMode(pinsAdd, RW), { + withAuth(withMode(pinsAdd, RWC), { checkHasPsaAccess, checkHasAccountRestriction, }), @@ -105,7 +106,7 @@ r.add( r.add( 'post', '/pins/:requestid', - withAuth(withMode(pinsReplace, RW), { + withAuth(withMode(pinsReplace, RWC), { checkHasPsaAccess, checkHasAccountRestriction, }), @@ -114,7 +115,7 @@ r.add( r.add( 'delete', '/pins/:requestid', - withAuth(withMode(pinsDelete, RW), { + withAuth(withMode(pinsDelete, RWC), { checkHasDeleteRestriction, checkHasPsaAccess, }), @@ -128,7 +129,7 @@ r.add('get', '/:cid', withAuth(withMode(nftGet, RO)), [postCors]) r.add( 'post', '/upload', - withAuth(withMode(nftUpload, RW), { + withAuth(withMode(nftUpload, RWC), { checkHasAccountRestriction, checkUcan, }), @@ -137,24 +138,24 @@ r.add( r.add( 'patch', '/upload/:cid', - withAuth(withMode(nftUpdateUpload, RW), { checkHasAccountRestriction }), + withAuth(withMode(nftUpdateUpload, RWC), { checkHasAccountRestriction }), [postCors] ) r.add( 'post', '/store', - withAuth(withMode(nftStore, RW), { checkHasAccountRestriction }), + withAuth(withMode(nftStore, RWC), { checkHasAccountRestriction }), [postCors] ) r.add( 'delete', '/:cid', - withAuth(withMode(nftDelete, RW), { checkHasDeleteRestriction }), + withAuth(withMode(nftDelete, RWC), { checkHasDeleteRestriction }), [postCors] ) // Temporary Metaplex upload route, mapped to metaplex user account. -r.add('post', '/metaplex/upload', withMode(metaplexUpload, RW), [postCors]) +r.add('post', '/metaplex/upload', withMode(metaplexUpload, RWC), [postCors]) // User r.add( @@ -206,7 +207,7 @@ r.add( r.add( 'post', '/api/pins', - withAuth(withMode(pinsAdd, RW), { + withAuth(withMode(pinsAdd, RWC), { checkHasPsaAccess, checkHasAccountRestriction, }), @@ -215,7 +216,7 @@ r.add( r.add( 'post', '/api/pins/:requestid', - withAuth(withMode(pinsReplace, RW), { + withAuth(withMode(pinsReplace, RWC), { checkHasPsaAccess, checkHasAccountRestriction, }), @@ -224,7 +225,7 @@ r.add( r.add( 'delete', '/api/pins/:requestid', - withAuth(withMode(pinsDelete, RW), { + withAuth(withMode(pinsDelete, RWC), { checkHasDeleteRestriction, checkHasPsaAccess, }), @@ -238,13 +239,13 @@ r.add('get', '/api/:cid', withAuth(withMode(nftGet, RO)), [postCors]) r.add( 'post', '/api/upload', - withAuth(withMode(nftUpload, RW), { checkUcan, checkHasAccountRestriction }), + withAuth(withMode(nftUpload, RWC), { checkUcan, checkHasAccountRestriction }), [postCors] ) r.add( 'delete', '/api/:cid', - withAuth(withMode(nftDelete, RW), { checkHasDeleteRestriction }), + withAuth(withMode(nftDelete, RWC), { checkHasDeleteRestriction }), [postCors] ) diff --git a/packages/api/src/middleware/maintenance.js b/packages/api/src/middleware/maintenance.js index ab0e4f2daa..d96f252e02 100644 --- a/packages/api/src/middleware/maintenance.js +++ b/packages/api/src/middleware/maintenance.js @@ -2,32 +2,42 @@ import { ErrorMaintenance, HTTPError } from '../errors.js' import { getServiceConfig } from '../config.js' /** - * @typedef {'rw' | 'r-' | '--'} Mode + * @typedef {'rwc' | 'rw-' | 'r--' | '---'} Mode * @typedef {import('../bindings').Handler} Handler */ +/** + * Read and write and create + */ +export const READ_WRITE_CREATE = 'rwc' + /** * Read and write. */ -export const READ_WRITE = 'rw' +export const READ_WRITE_ONLY = 'rw-' /** * Read only mode. */ -export const READ_ONLY = 'r-' +export const READ_ONLY = 'r--' /** * No reading or writing. */ -export const NO_READ_OR_WRITE = '--' +export const NO_READ_OR_WRITE = '---' /** @type {readonly Mode[]} */ -export const modes = Object.freeze([NO_READ_OR_WRITE, READ_ONLY, READ_WRITE]) +export const modes = Object.freeze([ + NO_READ_OR_WRITE, + READ_ONLY, + READ_WRITE_ONLY, + READ_WRITE_CREATE, +]) /** * The default maintenance mode (normal operation). */ -export const DEFAULT_MODE = READ_WRITE +export const DEFAULT_MODE = READ_WRITE_CREATE /** @type {() => Mode} */ let getMaintenanceMode = () => getServiceConfig().MAINTENANCE_MODE diff --git a/packages/api/test/config.spec.js b/packages/api/test/config.spec.js index 7bd7919eca..76f7e11039 100644 --- a/packages/api/test/config.spec.js +++ b/packages/api/test/config.spec.js @@ -26,7 +26,7 @@ const BASE_CONFIG = { DATABASE_TOKEN: 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJzdXBhYmFzZSIsImlhdCI6MTYwMzk2ODgzNCwiZXhwIjoyNTUwNjUzNjM0LCJyb2xlIjoic2VydmljZV9yb2xlIn0.necIJaiP7X2T2QjGeV-FhpkizcNTX8HjDDBAxpgQTEI', DATABASE_CONNECTION: 'postgresql://postgres:postgres@localhost:5432/postgres', - MAINTENANCE_MODE: 'rw', + MAINTENANCE_MODE: 'rwc', S3_REGION: 'us-east-1', S3_ACCESS_KEY_ID: 'minioadmin', S3_SECRET_ACCESS_KEY: 'minioadmin', @@ -145,7 +145,7 @@ test.serial( test.serial( 'serviceConfigFromVariables sets MAINTENANCE_MODE if it contains a valid mode string', (t) => { - const modes = ['--', 'r-', 'rw'] + const modes = ['---', 'r--', 'rw-', 'rwc'] for (const m of modes) { t.is( serviceConfigFromVariables( @@ -159,6 +159,23 @@ test.serial( } ) +test.serial( + 'serviceConfigFromVariables sets MAINTENANCE_MODE if it contains a valid legacy mode string', + (t) => { + const modes = ['--', 'r-', 'rw'] + for (const m of modes) { + t.is( + serviceConfigFromVariables( + override({ + MAINTENANCE_MODE: m, + }) + ).MAINTENANCE_MODE.toString(), + m + '-' + ) + } + } +) + test.serial( 'serviceConfigFromVariables uses unaltered values for string config variables', (t) => { diff --git a/packages/api/test/maintenance.spec.js b/packages/api/test/maintenance.spec.js index 505f31ad6a..8e61a38488 100644 --- a/packages/api/test/maintenance.spec.js +++ b/packages/api/test/maintenance.spec.js @@ -1,8 +1,9 @@ import test from 'ava' import { READ_ONLY, - READ_WRITE, + READ_WRITE_ONLY, NO_READ_OR_WRITE, + READ_WRITE_CREATE, } from '../src/middleware/maintenance.js' import { createClientWithUser } from './scripts/helpers.js' @@ -46,17 +47,25 @@ test('maintenance middleware should throw error when in maintenance mode', async const expectedError = { message: /API undergoing maintenance/ } - await setMode(t, READ_WRITE) + await setMode(t, READ_WRITE_CREATE) await t.notThrowsAsync(tryRead(t, token)) await t.notThrowsAsync(tryWrite(t, token)) + await t.notThrowsAsync(tryCreate(t, token)) + + await setMode(t, READ_WRITE_ONLY) + await t.notThrowsAsync(tryRead(t, token)) + await t.notThrowsAsync(tryWrite(t, token)) + await t.throwsAsync(tryCreate(t, token), expectedError) await setMode(t, READ_ONLY) await t.notThrowsAsync(tryRead(t, token)) await t.throwsAsync(tryWrite(t, token), expectedError) + await t.throwsAsync(tryCreate(t, token), expectedError) await setMode(t, NO_READ_OR_WRITE) await t.throwsAsync(tryRead(t, token), expectedError) await t.throwsAsync(tryWrite(t, token), expectedError) + await t.throwsAsync(tryCreate(t, token), expectedError) }) /** @@ -64,7 +73,7 @@ test('maintenance middleware should throw error when in maintenance mode', async * @param {import('ava').ExecutionContext} t * @param {string} token */ -async function tryWrite(t, token) { +async function tryCreate(t, token) { const mf = getMiniflareContext(t) const res = await mf.dispatchFetch('http://miniflare.test/upload', { headers: { authorization: `Bearer ${token}` }, @@ -77,6 +86,26 @@ async function tryWrite(t, token) { } } +/** + * + * @param {import('ava').ExecutionContext} t + * @param {string} token + */ +async function tryWrite(t, token) { + const mf = getMiniflareContext(t) + const res = await mf.dispatchFetch('http://miniflare.test/internal/tokens', { + headers: { + authorization: `Bearer ${token}`, + }, + method: 'POST', + body: JSON.stringify({ name: `new key ${new Date().toISOString()}` }), + }) + if (!res.ok) { + const { error } = await res.json() + throw new Error(error.message) + } +} + /** * * @param {import('ava').ExecutionContext} t diff --git a/packages/api/test/scripts/globals.js b/packages/api/test/scripts/globals.js index e670fe1099..d2ca6eed6e 100644 --- a/packages/api/test/scripts/globals.js +++ b/packages/api/test/scripts/globals.js @@ -17,7 +17,7 @@ globalThis.PICKUP_API_URL = 'http://127.0.0.1:9094' // will be used with we can active auth in cluster base64 of test:test globalThis.PICKUP_BASIC_AUTH_TOKEN = 'dGVzdDp0ZXN0' -globalThis.MAINTENANCE_MODE = 'rw' +globalThis.MAINTENANCE_MODE = 'rwc' globalThis.S3_ENDPOINT = 'http://127.0.0.1:9000' globalThis.S3_REGION = 'us-east-1'