// What holds mssql_query to being read-only (novox/hq issue 193): a caller's statement runs as the // reader login and never as the administrator, with no transaction wrapped around it as text, and // without the reader's password the statement is refused. // // The driver is a fake session that records each call's login, database, text and bound // parameters. That the reader cannot write is the server's to enforce and was proven against a real // server; this holds the module to asking for it. Run against the compiled module (npm test builds // first), the way the runtime loads it. import { test } from "node:test"; import assert from "node:assert/strict"; import { MssqlClient, READER, type Connect, type Target } from "../dist/client.js"; interface Call extends Target { text: string; params: Record; } function recording(): { connect: Connect; calls: Call[] } { const calls: Call[] = []; const connect: Connect = async (to) => ({ async run(text, params = {}) { calls.push({ ...to, text, params }); if (/FROM sys.server_principals/.test(text)) return []; // FOR JSON answers its document split across rows of one column. if (/FOR JSON/.test(text)) return [{ JSON_F52E: '[{"name":"al' }, { JSON_F52E: 'pha","n":1}]' }]; return []; }, async close() {}, }); return { connect, calls }; } const conn = { host: "127.0.0.1", port: 1433, user: "sa", password: "admin-secret" }; test("a caller's statement runs as the reader, as it was written, on the database it names", async () => { const { connect, calls } = recording(); const client = new MssqlClient({ ...conn, readerPassword: "reader-secret" }, connect); const result = await client.readOnlyQuery("inventory", "SELECT '$(SQLCMDPASSWORD)' AS p"); const asked = calls.at(-1)!; assert.equal(asked.user, READER, "the statement never runs as the administrator"); assert.equal(asked.password, "reader-secret"); assert.equal(asked.database, "inventory"); assert.ok(asked.text.startsWith("SET NOCOUNT ON; SELECT '$(SQLCMDPASSWORD)' AS p\nFOR JSON PATH"), "the caller's text reaches the server unaltered"); assert.doesNotMatch(asked.text, /BEGIN TRANSACTION|ROLLBACK/, "no transaction wrapped around it as text"); assert.deepEqual(result.rows, [{ name: "alpha", n: 1 }], "a FOR JSON document split across rows is reassembled"); assert.equal(result.command, "SELECT"); }); test("the reader is made as the administrator, kept out of sysadmin, and granted only reading", async () => { const { connect, calls } = recording(); const client = new MssqlClient({ ...conn, readerPassword: "reader-secret" }, connect); await client.readOnlyQuery("inventory", "SELECT 1 AS x"); await client.readOnlyQuery("inventory", "SELECT 2 AS x"); const asAdmin = calls.filter((c) => c.user === "sa").map((c) => c.text); assert.ok(asAdmin.some((s) => s.startsWith(`CREATE LOGIN [${READER}]`))); assert.ok(asAdmin.some((s) => /ALTER SERVER ROLE sysadmin DROP MEMBER/.test(s))); assert.ok(asAdmin.includes(`GRANT CONNECT ANY DATABASE TO [${READER}]`)); assert.ok(asAdmin.includes(`GRANT SELECT ALL USER SECURABLES TO [${READER}]`)); assert.equal(asAdmin.filter((s) => s.startsWith("CREATE LOGIN")).length, 1, "made once, not per call"); assert.equal(calls.filter((c) => c.user === READER).length, 2); }); test("without the reader's password the statement is refused, and nothing runs as the administrator", async () => { const { connect, calls } = recording(); const client = new MssqlClient(conn, connect); await assert.rejects(client.readOnlyQuery("inventory", "SELECT 1"), /refused rather than run as the administrator/); assert.deepEqual(calls, []); }); test("a consumer's password is checked as a bound parameter, never in the text", async () => { const { connect, calls } = recording(); const client = new MssqlClient(conn, connect); await client.holdsLogin("shop", "shop_login", "minted-secret"); const asked = calls[0]; assert.equal(asked.params.meshholdspw, "minted-secret"); assert.doesNotMatch(asked.text, /minted-secret/); assert.match(asked.text, /PWDCOMPARE\(@meshholdspw, password_hash\)/); });