Skip to content

Commit

Permalink
enforce authz checks on websocket and DEST endpoints
Browse files Browse the repository at this point in the history
  • Loading branch information
finn-block committed Nov 8, 2023
1 parent b62809f commit 96626b8
Show file tree
Hide file tree
Showing 4 changed files with 50 additions and 27 deletions.
14 changes: 11 additions & 3 deletions src/dwn-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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}`);
});
Expand All @@ -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...`));
}
}
Expand Down
31 changes: 14 additions & 17 deletions src/http-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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 },
});
Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -206,8 +203,8 @@ export class HttpApi {
}
});

if (this.#pow) {
this.#pow.setupRoutes(this.#api);
if (this.pow) {
this.pow.setupRoutes(this.#api);
}
}

Expand All @@ -216,8 +213,8 @@ export class HttpApi {
}

async start(port: number, callback?: () => void): Promise<http.Server> {
if (this.#pow) {
await this.#pow.initialize();
if (this.pow) {
await this.pow.initialize();
}
this.#listen(port, callback);
return this.#server;
Expand Down
17 changes: 16 additions & 1 deletion src/ws-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
}

Expand All @@ -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;

Expand Down Expand Up @@ -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
Expand Down
15 changes: 9 additions & 6 deletions tests/http-api.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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 () {
Expand Down Expand Up @@ -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 () {
Expand Down

0 comments on commit 96626b8

Please sign in to comment.