From 96626b8a19bcdbaf6b11d6eae4554b2b9864a9d3 Mon Sep 17 00:00:00 2001 From: finn Date: Wed, 8 Nov 2023 12:53:40 -0800 Subject: [PATCH] enforce authz checks on websocket and DEST endpoints --- src/dwn-server.ts | 14 +++++++++++--- src/http-api.ts | 31 ++++++++++++++----------------- src/ws-api.ts | 17 ++++++++++++++++- tests/http-api.spec.ts | 15 +++++++++------ 4 files changed, 50 insertions(+), 27 deletions(-) diff --git a/src/dwn-server.ts b/src/dwn-server.ts index 3ab2a82..ea09237 100644 --- a/src/dwn-server.ts +++ b/src/dwn-server.ts @@ -9,8 +9,9 @@ import { HttpServerShutdownHandler } from './lib/http-server-shutdown-handler.js import { type Config, config as defaultConfig } from './config.js'; import { HttpApi } from './http-api.js'; +import { ProofOfWork } from './pow.js'; import { setProcessHandlers } from './process-handlers.js'; -import { getDWNConfig } from './storage.js'; +import { getDWNConfig, getDialectFromURI } from './storage.js'; import { WsApi } from './ws-api.js'; export type DwnServerOptions = { @@ -50,7 +51,14 @@ export class DwnServer { this.dwn = await Dwn.create(getDWNConfig(this.config)); } - this.#httpApi = new HttpApi(this.dwn); + let pow: ProofOfWork = null; + if (this.config.powRegistration) { + pow = new ProofOfWork( + getDialectFromURI(new URL(this.config.tenantRegistrationStore)), + ); + } + + this.#httpApi = new HttpApi(this.dwn, pow); await this.#httpApi.start(this.config.port, () => { log.info(`HttpServer listening on port ${this.config.port}`); }); @@ -60,7 +68,7 @@ export class DwnServer { ); if (this.config.webSocketServerEnabled) { - this.#wsApi = new WsApi(this.#httpApi.server, this.dwn); + this.#wsApi = new WsApi(this.#httpApi.server, this.dwn, pow); this.#wsApi.start(() => log.info(`WebSocketServer ready...`)); } } diff --git a/src/http-api.ts b/src/http-api.ts index 21f3a36..0373115 100644 --- a/src/http-api.ts +++ b/src/http-api.ts @@ -20,28 +20,21 @@ import { JsonRpcErrorCodes, } from './lib/json-rpc.js'; -import { config } from './config.js'; import { jsonRpcApi } from './json-rpc-api.js'; import { requestCounter, responseHistogram } from './metrics.js'; -import { ProofOfWork } from './pow.js'; -import { getDialectFromURI } from './storage.js'; +import type { ProofOfWork } from './pow.js'; export class HttpApi { #api: Express; #server: http.Server; - #pow: ProofOfWork | undefined; + pow?: ProofOfWork; dwn: Dwn; - constructor(dwn: Dwn) { + constructor(dwn: Dwn, pow?: ProofOfWork) { this.#api = express(); this.#server = http.createServer(this.#api); this.dwn = dwn; - - if (config.powRegistration) { - this.#pow = new ProofOfWork( - getDialectFromURI(new URL(config.tenantRegistrationStore)), - ); - } + this.pow = pow; this.#setupMiddleware(); this.#setupRoutes(); @@ -92,6 +85,10 @@ export class HttpApi { }); this.#api.get('/:did/records/:id', async (req, res) => { + if (this.pow && !(await this.pow.isAuthorized(req.params.did))) { + return res.status(403).json('did not authorized on this server'); + } + const record = await RecordsRead.create({ filter: { recordId: req.params.id }, }); @@ -154,8 +151,8 @@ export class HttpApi { } if ( - config.powRegistration && - !(await this.#pow.isAuthorized(dwnRpcRequest.params.target)) + this.pow && + !(await this.pow.isAuthorized(dwnRpcRequest.params.target)) ) { const reply = createJsonRpcErrorResponse( dwnRpcRequest.id || uuidv4(), @@ -206,8 +203,8 @@ export class HttpApi { } }); - if (this.#pow) { - this.#pow.setupRoutes(this.#api); + if (this.pow) { + this.pow.setupRoutes(this.#api); } } @@ -216,8 +213,8 @@ export class HttpApi { } async start(port: number, callback?: () => void): Promise { - if (this.#pow) { - await this.#pow.initialize(); + if (this.pow) { + await this.pow.initialize(); } this.#listen(port, callback); return this.#server; diff --git a/src/ws-api.ts b/src/ws-api.ts index cac1cea..7e1edb9 100644 --- a/src/ws-api.ts +++ b/src/ws-api.ts @@ -14,16 +14,19 @@ import { import { jsonRpcApi } from './json-rpc-api.js'; import { requestCounter } from './metrics.js'; +import type { ProofOfWork } from './pow.js'; const SOCKET_ISALIVE_SYMBOL = Symbol('isAlive'); const HEARTBEAT_INTERVAL = 30_000; export class WsApi { #wsServer: WebSocketServer; + pow?: ProofOfWork; dwn: Dwn; - constructor(server: Server, dwn: Dwn) { + constructor(server: Server, dwn: Dwn, pow?: ProofOfWork) { this.dwn = dwn; + this.pow = pow; this.#wsServer = new WebSocketServer({ server }); } @@ -41,6 +44,7 @@ export class WsApi { */ #handleConnection(socket: WebSocket, _request: IncomingMessage): void { const dwn = this.dwn; + const pow = this.pow; socket[SOCKET_ISALIVE_SYMBOL] = true; @@ -91,6 +95,17 @@ export class WsApi { return socket.send(responseBuffer); } + if (pow && !(await pow.isAuthorized(dwnRequest.params.target))) { + const jsonRpcResponse = createJsonRpcErrorResponse( + dwnRequest.id || uuidv4(), + JsonRpcErrorCodes.Forbidden, + 'tenant not authorized, please register first', + ); + + const responseBuffer = WsApi.jsonRpcResponseToBuffer(jsonRpcResponse); + return socket.send(responseBuffer); + } + // Check whether data was provided in the request const { encodedData } = dwnRequest.params; const requestDataStream = encodedData diff --git a/tests/http-api.spec.ts b/tests/http-api.spec.ts index c04c68b..b14e9ae 100644 --- a/tests/http-api.spec.ts +++ b/tests/http-api.spec.ts @@ -24,6 +24,8 @@ import { createJsonRpcRequest, JsonRpcErrorCodes, } from '../src/lib/json-rpc.js'; +import { ProofOfWork } from '../src/pow.js'; +import { getDialectFromURI } from '../src/storage.js'; import { clear as clearDwn, dwn } from './test-dwn.js'; import type { Profile } from './utils.js'; import { @@ -45,9 +47,9 @@ describe('http api', function () { before(async function () { config.powRegistration = true; - config.tenantRegistrationStore = 'sqlite://'; // use in-memory database that doesn't persist after tests have run + const pow = new ProofOfWork(getDialectFromURI(new URL('sqlite://'))); profile = await createProfile(); - httpApi = new HttpApi(dwn); + httpApi = new HttpApi(dwn, pow); }); beforeEach(async function () { @@ -613,13 +615,14 @@ describe('http api', function () { expect(response.status).to.equal(404); }); - it('returns a 404 for invalid did', async function () { - const { recordsWrite } = await createRecordsWriteMessage(profile); + it('returns a 403 for invalid or unauthorized did', async function () { + const unauthorized = await createProfile(); + const { recordsWrite } = await createRecordsWriteMessage(unauthorized); const response = await fetch( - `http://localhost:3000/1234567892345678/records/${recordsWrite.message.recordId}`, + `http://localhost:3000/${unauthorized.did}/records/${recordsWrite.message.recordId}`, ); - expect(response.status).to.equal(404); + expect(response.status).to.equal(403); }); it('returns a 404 for invalid record id', async function () {