Files
MikrotikManager/backend/src/services/bandwidth-control-role.test.ts
T

95 lines
8.2 KiB
TypeScript

import assert from "node:assert/strict"
import { test } from "node:test"
import { assertBandwidthControlRuntimeRole, BandwidthControlRuntimeRoleRefused, type BandwidthControlRoleExpected, type BandwidthControlRoleSql } from "./bandwidth-control-role.js"
const expected: BandwidthControlRoleExpected = { databaseName: "owned_database", runtimeRole: "bandwidth_runtime", ownerRole: "control_owner" }
function observation() {
return {
databaseName: expected.databaseName, sessionRole: expected.runtimeRole, currentRole: expected.runtimeRole,
runtimeRoleExists: true, ownerRoleExists: true, runtimeFlagsSafe: true,
ownerMembership: false, dangerousMembership: false,
schemaExists: true, schemaOwner: expected.ownerRole, schemaUsage: true, schemaCreate: false,
publicSchemaUsage: false, publicSchemaCreate: false,
tables: ["bandwidth_attempts", "bandwidth_node_occupancy", "bandwidth_attempt_events", "bandwidth_source_identity"].map(name => ({ name, exists: true, ownerRole: expected.ownerRole, ownedByRuntime: false, privilegesExact: true })),
}
}
const deferred = <T>() => { let resolve!: (value: T) => void; const promise = new Promise<T>(done => { resolve = done }); return { promise, resolve } }
const refused = (error: unknown) => error instanceof BandwidthControlRuntimeRoleRefused && error.code === "BANDWIDTH_CONTROL_RUNTIME_ROLE_REFUSED"
async function check(value: unknown) {
return assertBandwidthControlRuntimeRole({ query: async () => ({ rows: [{ observation: value }] }) }, expected, () => {})
}
test("minimal separate runtime role succeeds with one catalog observation and live fences on both sides", async () => {
const events: string[] = [], calls: Array<{ text: string; values?: unknown[] }> = []
const sql: BandwidthControlRoleSql = { query: async (text, values) => { events.push("SQL"); calls.push({ text, values }); return { rows: [{ observation: observation() }] } } }
assert.equal(await assertBandwidthControlRuntimeRole(sql, expected, () => { events.push("live") }), undefined)
assert.deepEqual(events, ["live", "SQL", "live"]); assert.equal(calls.length, 1)
assert.deepEqual(calls[0].values, [expected.databaseName, expected.runtimeRole, expected.ownerRole])
const text = calls[0].text
for (const catalog of ["pg_roles", "pg_namespace", "pg_class", "has_schema_privilege", "has_table_privilege", "has_any_column_privilege", "aclexplode", "acldefault", "pg_has_role", "current_database"]) assert.ok(text.includes("pg_catalog." + catalog), catalog)
assert.match(text, /pg_has_role\(rt\.oid,r\.oid,'MEMBER'\)/)
for (const privilege of ["TRUNCATE", "REFERENCES", "TRIGGER", " WITH GRANT OPTION"]) assert.ok(text.includes(privilege))
for (const role of ["pg_read_server_files", "pg_write_server_files", "pg_execute_server_program"]) assert.ok(text.includes(role))
assert.ok(text.includes("n.nspname='mmapp_control'")); assert.ok(text.includes("'bandwidth_source_identity',ARRAY['SELECT']"))
assert.doesNotMatch(text, /\b(?:INSERT INTO|DELETE FROM|ALTER TABLE|CREATE SCHEMA|SET ROLE)\b/)
assert.ok(!text.includes(expected.runtimeRole)); assert.ok(!text.includes(expected.ownerRole)); assert.ok(!text.includes(expected.databaseName))
})
test("session/current role, database, role flags and direct or transitive memberships must all match", async () => {
const failures: Record<string, unknown> = { databaseName: "foreign_database", sessionRole: "app_restorer", currentRole: "control_owner", runtimeRoleExists: false, ownerRoleExists: false, runtimeFlagsSafe: false, ownerMembership: true, dangerousMembership: true }
for (const [field, value] of Object.entries(failures)) await assert.rejects(check({ ...observation(), [field]: value }), refused, field)
})
test("missing control schema, incorrect ownership and PUBLIC or runtime CREATE/USAGE escalation refuse", async () => {
const failures: Record<string, unknown> = { schemaExists: false, schemaOwner: "app_restorer", schemaUsage: false, schemaCreate: true, publicSchemaUsage: true, publicSchemaCreate: true }
for (const [field, value] of Object.entries(failures)) await assert.rejects(check({ ...observation(), [field]: value }), refused, field)
})
test("all four relations require the fixed owner and exact least privileges, including read-only source identity", async () => {
for (let index = 0; index < 4; index++) {
for (const change of [{ exists: false }, { ownerRole: expected.runtimeRole }, { ownedByRuntime: true }, { privilegesExact: false }]) {
const value = observation(); Object.assign(value.tables[index], change)
await assert.rejects(check(value), refused)
}
}
const missing = observation(); missing.tables.pop(); await assert.rejects(check(missing), refused)
const duplicate = observation(); duplicate.tables[3] = { ...duplicate.tables[0] }; await assert.rejects(check(duplicate), refused)
const unknown = observation(); unknown.tables[3].name = "public_shadow"; await assert.rejects(check(unknown), refused)
})
test("malformed or truthy metadata never substitutes for exact boolean observations", async () => {
for (const value of [null, {}, { ...observation(), runtimeFlagsSafe: "true" }, { ...observation(), schemaCreate: null }, { ...observation(), extraReady: true }, { ...observation(), tables: null }]) await assert.rejects(check(value), refused)
const tableExtra = observation(); Object.assign(tableExtra.tables[0], { bypass: false }); await assert.rejects(check(tableExtra), refused)
for (const rows of [[], [{ observation: observation() }, { observation: observation() }], [{ observation: observation(), extra: true }]]) await assert.rejects(assertBandwidthControlRuntimeRole({ query: async () => ({ rows }) }, expected, () => {}), refused)
})
test("expected names stay bounded parameters and are captured before async SQL", async () => {
let calls = 0
const invalid = ["", " ", "x".repeat(64), "x\u0000y", "x\ny", "я".repeat(32)]
for (const name of invalid) await assert.rejects(assertBandwidthControlRuntimeRole({ query: async () => { calls++; return { rows: [] } } }, { ...expected, runtimeRole: name }, () => {}), refused)
await assert.rejects(assertBandwidthControlRuntimeRole({ query: async () => { calls++; return { rows: [] } } }, { ...expected, ownerRole: expected.runtimeRole }, () => {}), refused)
assert.equal(calls, 0)
const pending = deferred<{ rows: Record<string, unknown>[] }>(), mutable = { ...expected }
const result = assertBandwidthControlRuntimeRole({ query: async () => pending.promise }, mutable, () => {})
mutable.ownerRole = "changed"; pending.resolve({ rows: [{ observation: observation() }] }); await result
const quoted = { databaseName: "db'quoted", runtimeRole: "runtime';select", ownerRole: 'owner"quoted' }
await assertBandwidthControlRuntimeRole({ query: async (text, values) => { assert.deepEqual(values, [quoted.databaseName, quoted.runtimeRole, quoted.ownerRole]); assert.ok(!text.includes(quoted.runtimeRole)); return { rows: [{ observation: { ...observation(), databaseName: quoted.databaseName, sessionRole: quoted.runtimeRole, currentRole: quoted.runtimeRole, schemaOwner: quoted.ownerRole, tables: observation().tables.map(row => ({ ...row, ownerRole: quoted.ownerRole })) } }] } } }, quoted, () => {})
})
test("expired owners cannot start SQL or accept a late successful observation", async () => {
let calls = 0
const expired = new Error("closed live owner")
await assert.rejects(assertBandwidthControlRuntimeRole({ query: async () => { calls++; return { rows: [] } } }, expected, () => { throw expired }), error => error === expired)
assert.equal(calls, 0)
const pending = deferred<{ rows: Record<string, unknown>[] }>()
let active = true
const result = assertBandwidthControlRuntimeRole({ query: async () => pending.promise }, expected, () => { if (!active) throw expired })
active = false; const rejected = assert.rejects(result, error => error === expired); pending.resolve({ rows: [{ observation: observation() }] }); await rejected
})
test("SQL failure still performs the post-await live fence and returns no provider details", async () => {
let checks = 0
await assert.rejects(assertBandwidthControlRuntimeRole({ query: async () => { throw new Error("SECRET-provider-details") } }, expected, () => { checks++ }), error => refused(error) && !String(error).includes("SECRET"))
assert.equal(checks, 2)
})