From 464241783b1fe850b18aba4fd5151c4929376ee1 Mon Sep 17 00:00:00 2001 From: Samuel <131569500+SamOkampo@users.noreply.github.com> Date: Tue, 11 Aug 2026 15:56:18 -0500 Subject: [PATCH 1/2] fix: lock down metrics table access --- metrics-store.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/metrics-store.js b/metrics-store.js index 983d61b..ff5e6df 100644 --- a/metrics-store.js +++ b/metrics-store.js @@ -32,7 +32,7 @@ class MetricsStore { try { await this.pool.query(` - CREATE TABLE IF NOT EXISTS airdows_daily_metrics ( + CREATE TABLE IF NOT EXISTS public.airdows_daily_metrics ( metric_date DATE PRIMARY KEY, samples BIGINT NOT NULL DEFAULT 0, completed BIGINT NOT NULL DEFAULT 0, @@ -48,6 +48,15 @@ class MetricsStore { updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ) `); + await this.pool.query( + 'ALTER TABLE public.airdows_daily_metrics ENABLE ROW LEVEL SECURITY' + ); + await this.pool.query( + 'REVOKE ALL ON TABLE public.airdows_daily_metrics FROM anon' + ); + await this.pool.query( + 'REVOKE ALL ON TABLE public.airdows_daily_metrics FROM authenticated' + ); console.info('[AirDows] Persistent metrics connected.'); return true; } catch (error) { From 2dbaf674dc2f500687ac64902fef47c98652004d Mon Sep 17 00:00:00 2001 From: Samuel <131569500+SamOkampo@users.noreply.github.com> Date: Tue, 11 Aug 2026 15:56:49 -0500 Subject: [PATCH 2/2] test: cover metrics table RLS hardening --- test/metrics-store-security.test.js | 151 ++++++++++++++++++++++++++++ 1 file changed, 151 insertions(+) create mode 100644 test/metrics-store-security.test.js diff --git a/test/metrics-store-security.test.js b/test/metrics-store-security.test.js new file mode 100644 index 0000000..6dfc388 --- /dev/null +++ b/test/metrics-store-security.test.js @@ -0,0 +1,151 @@ +'use strict'; + +const test = require('node:test'); +const assert = require('node:assert/strict'); + +const pgPath = require.resolve('pg'); +const metricsStorePath = require.resolve('../metrics-store'); + +function normalizeSql(sql) { + return String(sql).replace(/\s+/g, ' ').trim(); +} + +function loadMetricsStoreWithPool(FakePool) { + const originalPgExports = require(pgPath); + const pgCacheEntry = require.cache[pgPath]; + pgCacheEntry.exports = { ...originalPgExports, Pool: FakePool }; + delete require.cache[metricsStorePath]; + + try { + return require(metricsStorePath).MetricsStore; + } finally { + pgCacheEntry.exports = originalPgExports; + delete require.cache[metricsStorePath]; + } +} + +function createFakePoolClass() { + return class FakePool { + static instances = []; + + constructor(options) { + this.options = options; + this.queries = []; + this.ended = false; + this.constructor.instances.push(this); + } + + async query(sql, values) { + const text = normalizeSql(sql); + this.queries.push({ text, values }); + + if (text.startsWith('SELECT')) { + return { + rows: [{ + samples: '7', + completed: '6', + failed: '1', + cancelled: '0', + host: '4', + srflx: '2', + relay: '1', + unknown_route: '0', + relay_chunks: '8', + relay_estimated_bytes: '4096', + pro_required_events: '0', + started_at: '2026-08-11T00:00:00.000Z' + }] + }; + } + + return { rows: [] }; + } + + async end() { + this.ended = true; + } + }; +} + +test('initialize creates and locks down the public metrics table in order', async () => { + const FakePool = createFakePoolClass(); + const MetricsStore = loadMetricsStoreWithPool(FakePool); + const store = new MetricsStore('postgres://backend:secret@example.invalid/db'); + + assert.equal(await store.initialize(), true); + + const pool = FakePool.instances[0]; + assert.ok(pool); + const queries = pool.queries.map(({ text }) => text); + + assert.match( + queries[0], + /^CREATE TABLE IF NOT EXISTS public\.airdows_daily_metrics \(/ + ); + assert.equal( + queries[1], + 'ALTER TABLE public.airdows_daily_metrics ENABLE ROW LEVEL SECURITY' + ); + assert.equal( + queries[2], + 'REVOKE ALL ON TABLE public.airdows_daily_metrics FROM anon' + ); + assert.equal( + queries[3], + 'REVOKE ALL ON TABLE public.airdows_daily_metrics FROM authenticated' + ); + + const initializationSql = queries.slice(0, 4).join('\n'); + assert.doesNotMatch(initializationSql, /CREATE\s+POLICY/i); + assert.doesNotMatch(initializationSql, /GRANT\s+/i); + assert.doesNotMatch(initializationSql, /REVOKE[^\n]*(?:service_role|postgres)/i); + + await store.close(); + assert.equal(pool.ended, true); +}); + +test('backend metric writes and reads still use the same store after hardening', async () => { + const FakePool = createFakePoolClass(); + const MetricsStore = loadMetricsStoreWithPool(FakePool); + const store = new MetricsStore('postgres://backend:secret@example.invalid/db'); + + assert.equal(await store.initialize(), true); + + await store.writeDailyMetrics({ + samples: 1, + completed: 1, + failed: 0, + cancelled: 0, + host: 1, + srflx: 0, + relay: 0, + unknownRoute: 0, + relayChunks: 0, + relayEstimatedBytes: 0, + proRequiredEvents: 0 + }); + + const totals = await store.getTotals(); + const pool = FakePool.instances[0]; + const queries = pool.queries.map(({ text }) => text); + + assert.ok(queries.some((query) => query.startsWith('INSERT INTO airdows_daily_metrics'))); + assert.ok(queries.some((query) => /FROM airdows_daily_metrics$/.test(query))); + assert.deepEqual(totals, { + samples: 7, + completed: 6, + failed: 1, + cancelled: 0, + host: 4, + srflx: 2, + relay: 1, + unknownRoute: 0, + relayChunks: 8, + relayEstimatedBytes: 4096, + proRequiredEvents: 0, + startedAt: '2026-08-11T00:00:00.000Z' + }); + + await store.close(); + assert.equal(pool.ended, true); +});