203 lines
15 KiB
TypeScript
203 lines
15 KiB
TypeScript
import assert from "node:assert/strict"
|
|
import { randomUUID } from "node:crypto"
|
|
import { test } from "node:test"
|
|
import { mcpConnectionPolicySchema } from "@mmapp/contracts/mcp"
|
|
import type { AuthUser } from "../lib/permissions.js"
|
|
import type { ConnectionRow, McpPrincipal } from "../modules/mcp/store.js"
|
|
import { McpError } from "../modules/mcp/security.js"
|
|
import type { ProbeStorageConnection } from "./server-probe-journal-maintenance.js"
|
|
import { createMcpServerPollAuthorityForTests, createRestServerPollAuthorityForTests } from "./server-poll-authority.js"
|
|
|
|
const actor = (): AuthUser => ({ id: "operator", email: "", name: "", apps: ["mm"], permissions: ["mm:servers:write"], issuedAt: Date.now() - 1000, expiresAt: Date.now() + 60000 })
|
|
const context = () => ({ acceptedAt: Date.now(), requestId: randomUUID(), authRequired: true, deadlineAt: Date.now() + 2000 })
|
|
const request = () => ({ serverId: 1, receiptId: randomUUID() })
|
|
const deferred = () => { let resolve!: () => void; const promise = new Promise<void>(done => { resolve = done }); return { promise, resolve } }
|
|
const safe = (error: unknown) => error instanceof McpError && !error.message.includes("PRIVATE_PROVIDER")
|
|
test("REST original request expiry during metadata refuses before any later source query", async () => {
|
|
const f = fixture(); let active = true
|
|
const authorize = createRestServerPollAuthorityForTests(actor(), { ...context(), assertRequestActive: () => { if (!active) throw new McpError("READ_CANCELLED", "expired", 409) } }, f.dependencies)
|
|
f.onProtections(() => { active = false })
|
|
await assert.rejects(authorize(request(), [1]), { code: "READ_CANCELLED" })
|
|
assert.equal(f.portal(), 1)
|
|
assert.equal(f.leases[0].commands.at(-1), "SELECT object_ref FROM mcp_protections ORDER BY id LIMIT 1001")
|
|
assert.deepEqual(f.leases[0].releases, [true])
|
|
})
|
|
test("REST original request expiry during checkout destroys the lease without SQL", async () => {
|
|
const f = fixture(); let active = true
|
|
const dependencies = { ...f.dependencies, connect: async (...args: Parameters<typeof f.dependencies.connect>) => {
|
|
const lease = await f.dependencies.connect(...args); active = false; return lease
|
|
} }
|
|
const authorize = createRestServerPollAuthorityForTests(actor(), { ...context(), assertRequestActive: () => { if (!active) throw new McpError("READ_CANCELLED", "expired", 409) } }, dependencies)
|
|
await assert.rejects(authorize(request(), [1]), { code: "READ_CANCELLED" })
|
|
assert.deepEqual(f.leases[0].commands, []); assert.deepEqual(f.leases[0].releases, [true])
|
|
})
|
|
function fixture() {
|
|
const principal: McpPrincipal = { connectionId: randomUUID(), credentialId: randomUUID(), credentialExpiresAt: new Date(Date.now() + 60000).toISOString() }
|
|
let connection = { id: principal.connectionId, name: "owned", owner_id: "operator", owner_permissions: ["mm:servers:write"], generation: "a".repeat(64),
|
|
policy: mcpConnectionPolicySchema.parse({ id: principal.connectionId, status: "active", version: 1, mode: "approval", permissions: ["mm:servers:write"], expiresAt: principal.credentialExpiresAt,
|
|
scope: { serverIds: [1, 2], operationIds: ["servers.poll"], allowGlobal: true } }), owner_valid_until: new Date(principal.credentialExpiresAt), owner_claims_issued_at: new Date(Date.now() - 1000) } as ConnectionRow
|
|
let servers: Record<string, unknown>[] = [{ id: 1 }, { id: 2 }], protections: Record<string, unknown>[] = [], grants: Record<string, unknown>[] = [], generation = connection.generation
|
|
const leases: { commands: string[]; releases: boolean[]; timeouts: number[] }[] = []
|
|
let reads = 0, portal = 0, changed: (() => void) | undefined
|
|
const dependencies: Parameters<typeof createRestServerPollAuthorityForTests>[2] = {
|
|
connect: async () => {
|
|
const value = { commands: [] as string[], releases: [] as boolean[], timeouts: [] as number[] }; leases.push(value)
|
|
const sql: ProbeStorageConnection = { query: async (text, _values, options) => {
|
|
value.commands.push(text); value.timeouts.push(options!.timeoutMs)
|
|
if (text.startsWith("SELECT id FROM servers")) return { rows: structuredClone(servers) }
|
|
if (text.startsWith("SELECT object_ref")) { changed?.(); return { rows: structuredClone(protections) } }
|
|
if (text.startsWith("SELECT payload")) return { rows: structuredClone(grants) }
|
|
return { rows: [] }
|
|
}, release: unsafe => { value.releases.push(unsafe ?? false) } }
|
|
return sql
|
|
},
|
|
connectionSource: async (_principal: McpPrincipal) => { reads++; return { connection: structuredClone(connection), grantRows: structuredClone(grants) } },
|
|
portal: async () => { portal++ }, generation: async () => generation,
|
|
}
|
|
return { principal, dependencies, leases, reads: () => reads, portal: () => portal,
|
|
setConnection: (work: (row: ConnectionRow) => void) => work(connection), changeConnection: () => { connection = { ...connection, owner_id: "changed" } },
|
|
setServers: (rows: Record<string, unknown>[]) => { servers = rows }, setProtections: (rows: Record<string, unknown>[]) => { protections = rows },
|
|
setGrants: (rows: Record<string, unknown>[]) => { grants = rows }, setGeneration: (value: string) => { generation = value }, onProtections: (callback: () => void) => { changed = callback } }
|
|
}
|
|
|
|
test("MCP current grant is checked before IDs, full fleet required, proof stable and immutable", async () => {
|
|
const f = fixture(), authorize = createMcpServerPollAuthorityForTests(f.principal, {}, f.dependencies)
|
|
const first = await authorize(request(), [1]), second = await authorize(request(), [1, 2])
|
|
assert.equal(first.global, true); assert.deepEqual(first.serverIds, [1, 2]); assert.equal(first.authorityProof, second.authorityProof)
|
|
assert(Object.isFrozen(first)); assert(Object.isFrozen(first.serverIds)); assert.equal(f.reads(), 4)
|
|
assert(f.leases.every(lease => lease.commands[0] === "BEGIN ISOLATION LEVEL READ COMMITTED READ ONLY" && lease.commands.at(-1) === "ROLLBACK" && lease.releases.join() === "false"))
|
|
assert(f.leases.every(lease => lease.timeouts.every(timeout => timeout > 0 && timeout <= 10000)))
|
|
assert(!f.leases.flatMap(lease => lease.commands).some(sql => /SELECT \*|password|username|INSERT|UPDATE|DELETE/.test(sql)))
|
|
f.setConnection(row => { row.policy.scope.serverIds = [1] })
|
|
await assert.rejects(authorize(request(), [1]), safe)
|
|
})
|
|
|
|
test("MCP permission/global/mode/generation/operation denial precedes metadata and credentials", async () => {
|
|
const mutations: ((row: ConnectionRow) => void)[] = [row => { row.owner_permissions = [] }, row => { row.policy.permissions = [] }, row => { row.policy.mode = "observer" },
|
|
row => { row.policy.scope.allowGlobal = false }, row => { row.policy.scope.operationIds = [] }, row => { row.generation = "b".repeat(64) }, row => { row.owner_valid_until = new Date(0) }]
|
|
for (const mutate of mutations) {
|
|
const f = fixture(); f.setConnection(mutate)
|
|
await assert.rejects(createMcpServerPollAuthorityForTests(f.principal, {}, f.dependencies)(request(), [1]), safe)
|
|
assert(!f.leases[0].commands.some(sql => sql.startsWith("SELECT id FROM servers") || sql.startsWith("SELECT object_ref")))
|
|
}
|
|
})
|
|
|
|
test("MCP original principal frozen, current temporary grant and source drift change/refuse proof", async () => {
|
|
const f = fixture(), originalId = f.principal.credentialId
|
|
let seen = ""
|
|
const source = f.dependencies.connectionSource
|
|
f.dependencies.connectionSource = async (principal, client) => { seen = principal.credentialId; return { ...(await source(principal, client)), connection: fixtureConnection } }
|
|
const fixtureConnection = (await fixture().dependencies.connectionSource(f.principal)).connection
|
|
fixtureConnection.id = f.principal.connectionId; fixtureConnection.policy.id = f.principal.connectionId
|
|
const authorize = createMcpServerPollAuthorityForTests(f.principal, {}, f.dependencies)
|
|
f.principal.credentialId = randomUUID()
|
|
const first = await authorize(request(), [1]); assert.equal(seen, originalId)
|
|
f.setGrants([{ payload: { id: "grant-a", expiresAt: "2099-01-01T00:00:00Z" } }])
|
|
const second = await authorize(request(), [1]); assert.notEqual(first.authorityProof, second.authorityProof)
|
|
f.onProtections(() => { fixtureConnection.policy.version++ })
|
|
// The captured before row must not share a mutable provider object.
|
|
await assert.rejects(authorize(request(), [1]), safe)
|
|
})
|
|
|
|
test("global protections: deny blocks MCP and REST; human-required permits only MCP planning lane", async () => {
|
|
const f = fixture(); f.setProtections([{ object_ref: { kind: "server", id: "2", serverId: 2, protection: "human-approval-required" } }])
|
|
await createMcpServerPollAuthorityForTests(f.principal, {}, f.dependencies)(request(), [1])
|
|
await assert.rejects(createRestServerPollAuthorityForTests(actor(), context(), f.dependencies)(request(), [1]), { code: "APPROVAL_REQUIRED" })
|
|
f.setProtections([{ object_ref: { kind: "server", id: "2", serverId: 2, protection: "deny" } }])
|
|
await assert.rejects(createMcpServerPollAuthorityForTests(f.principal, {}, f.dependencies)(request(), [1]), { code: "PROTECTED_OBJECT" })
|
|
})
|
|
|
|
test("REST rejects missing/expired JWT or app/permission before any I/O", () => {
|
|
const f = fixture()
|
|
for (const value of [undefined, { ...actor(), permissions: [], isAdmin: true }, { ...actor(), issuedAt: undefined }, { ...actor(), expiresAt: undefined },
|
|
{ ...actor(), apps: [] }, { ...actor(), expiresAt: Date.now() - 1 }, { ...actor(), id: "x".repeat(201) }]) {
|
|
assert.throws(() => createRestServerPollAuthorityForTests(value, context(), f.dependencies), safe)
|
|
}
|
|
assert.equal(f.leases.length, 0); assert.equal(f.portal(), 0)
|
|
})
|
|
|
|
test("REST current portal fences cloned original JWT and authorized IDs before safe 404", async () => {
|
|
const f = fixture(), original = actor(), seen: AuthUser[] = []
|
|
f.dependencies.portal = async value => { seen.push(value as AuthUser); assert(Object.isFrozen(value)) }
|
|
const authorize = createRestServerPollAuthorityForTests(original, context(), f.dependencies)
|
|
original.id = "changed"; original.permissions.length = 0; original.expiresAt = 0
|
|
const grant = await authorize(request(), [1]); assert.deepEqual(grant.serverIds, [1, 2]); assert.equal(seen.length, 2); assert.equal(seen[0].id, "operator")
|
|
f.setServers([{ id: 2 }]); await assert.rejects(authorize(request(), [1]), { code: "NOT_FOUND" })
|
|
assert.equal(seen.length, 3)
|
|
})
|
|
|
|
test("local mode requires trusted authRequired=false and never ignores supplied stale actors", async () => {
|
|
const f = fixture(), value = { ...context(), authRequired: false }
|
|
assert.equal((await createRestServerPollAuthorityForTests(undefined, value, f.dependencies)(request(), [1])).global, true)
|
|
assert.equal(f.portal(), 0)
|
|
assert.throws(() => createRestServerPollAuthorityForTests({ ...actor(), expiresAt: 0 }, value, f.dependencies), safe)
|
|
})
|
|
|
|
test("portal revocation before/after source metadata and generation drift refuse safe grant", async () => {
|
|
for (const at of [1, 2]) {
|
|
const f = fixture(); let count = 0
|
|
f.dependencies.portal = async () => { if (++count === at) throw new McpError("OWNER_CLAIMS_REVOKED", "PRIVATE_PROVIDER", 401) }
|
|
await assert.rejects(createRestServerPollAuthorityForTests(actor(), context(), f.dependencies)(request(), [1]), safe)
|
|
if (at === 1) assert(!f.leases[0].commands.some(sql => sql.startsWith("SELECT id FROM servers")))
|
|
}
|
|
const f = fixture(); f.onProtections(() => f.setGeneration("changed"))
|
|
await assert.rejects(createRestServerPollAuthorityForTests(actor(), context(), f.dependencies)(request(), [1]), { code: "STATE_CONFLICT" })
|
|
})
|
|
|
|
test("catalog overflow/duplicates/order/extra fields and malformed protections fail closed", async () => {
|
|
for (const rows of [[{ id: 1 }, { id: 1 }], [{ id: 2 }, { id: 1 }], [{ id: 1, password: "PRIVATE_PROVIDER" }], Array.from({ length: 1001 }, (_, index) => ({ id: index + 1 }))]) {
|
|
const f = fixture(); f.setServers(rows)
|
|
await assert.rejects(createRestServerPollAuthorityForTests(actor(), context(), f.dependencies)(request(), [1]), safe)
|
|
}
|
|
const f = fixture(); f.setProtections([{ object_ref: { protection: "unknown" } }])
|
|
await assert.rejects(createRestServerPollAuthorityForTests(actor(), context(), f.dependencies)(request(), [1]), safe)
|
|
})
|
|
|
|
test("whole callback bounds crypto gap, destroys once and rejects late facade SQL", async () => {
|
|
const f = fixture(), started = deferred(), resume = deferred(), late = deferred()
|
|
f.dependencies.portal = async (_actor, client) => {
|
|
assert(client); started.resolve(); await resume.promise
|
|
await assert.rejects(client.query("SELECT late_private_metadata"), safe); late.resolve()
|
|
}
|
|
const authorize = createRestServerPollAuthorityForTests(actor(), { ...context(), deadlineAt: Date.now() + 40 }, f.dependencies)
|
|
const pending = authorize(request(), [1]); await started.promise; await assert.rejects(pending, safe)
|
|
assert.deepEqual(f.leases[0].releases, [true]); assert(!f.leases[0].commands.includes("ROLLBACK"))
|
|
resume.resolve(); await late.promise; assert(!f.leases[0].commands.includes("SELECT late_private_metadata")); assert.deepEqual(f.leases[0].releases, [true])
|
|
})
|
|
|
|
test("pre-aborted/original expiry refuses without checkout and provider details sanitized", async () => {
|
|
const f = fixture(), controller = new AbortController(); controller.abort()
|
|
assert.throws(() => createMcpServerPollAuthorityForTests({ ...f.principal, credentialExpiresAt: new Date(0).toISOString() }, {}, f.dependencies), safe)
|
|
assert.throws(() => createRestServerPollAuthorityForTests(actor(), { ...context(), signal: controller.signal }, f.dependencies), safe)
|
|
assert.equal(f.leases.length, 0)
|
|
f.dependencies.connectionSource = async () => { throw new Error("PRIVATE_PROVIDER") }
|
|
await assert.rejects(createMcpServerPollAuthorityForTests(f.principal, {}, f.dependencies)(request(), [1]), safe)
|
|
assert.deepEqual(f.leases[0].releases, [false])
|
|
})
|
|
|
|
test("MCP source is mandatory, read before/after on each invocation, with no second grant lookup", async () => {
|
|
const f = fixture(), authorize = createMcpServerPollAuthorityForTests(f.principal, {}, f.dependencies)
|
|
await authorize(request(), [1]); await authorize(request(), [1])
|
|
assert.equal(f.reads(), 4)
|
|
assert.equal(f.leases.flatMap(lease => lease.commands).filter(sql => sql.startsWith("SELECT payload")).length, 0,
|
|
"The coherent provider already supplies exact raw grant rows; authority must not requery them")
|
|
assert(f.leases.every(lease => lease.commands.length === 5))
|
|
f.onProtections(() => f.setGrants([{ payload: { credentialId: f.principal.credentialId, opaque: "changed-source" } }]))
|
|
await assert.rejects(authorize(request(), [1]), { code: "STATE_CONFLICT" })
|
|
})
|
|
|
|
test("MCP duplicate coherent grant source refuses rather than using a fallback read", async () => {
|
|
const f = fixture(); f.setGrants([{ payload: {} }, { payload: {} }])
|
|
await assert.rejects(createMcpServerPollAuthorityForTests(f.principal, {}, f.dependencies)(request(), [1]), safe)
|
|
assert.equal(f.reads(), 1)
|
|
assert(!f.leases[0].commands.some(sql => sql.startsWith("SELECT payload")))
|
|
})
|
|
|
|
test("MCP missing coherent source has no legacy connection fallback", async () => {
|
|
const f = fixture(); let fallbackCalls = 0
|
|
const dependencies = { ...f.dependencies, connectionSource: undefined, connection: async () => { fallbackCalls++; return {} } } as unknown as typeof f.dependencies
|
|
await assert.rejects(createMcpServerPollAuthorityForTests(f.principal, {}, dependencies)(request(), [1]), { code: "STATE_UNAVAILABLE" })
|
|
assert.equal(fallbackCalls, 0)
|
|
assert(!f.leases[0].commands.some(sql => sql.startsWith("SELECT payload")))
|
|
})
|