Files
MikrotikManager/backend/src/modules/mcp/routing-controller-baseline.test.ts
T

143 lines
9.9 KiB
TypeScript

import assert from "node:assert/strict"
import { randomUUID } from "node:crypto"
import { mkdtemp, readFile, rm } from "node:fs/promises"
import { tmpdir } from "node:os"
import { basename, dirname, join, resolve } from "node:path"
import { test, type TestContext } from "node:test"
import ts from "typescript"
import { z } from "zod"
import { scopedTargetRuleMatches } from "./routing-community-scoped-model.js"
import * as security from "./security.js"
import type * as Baseline from "./routing-controller-baseline.js"
// Run the actual baseline implementation and durable HMAC on a temporary key.
// Only the SQL/RouterOS/authority adapters supply synthetic snapshots.
async function fixture(t: TestContext) {
const root = resolve(tmpdir()), prefix = "mcp-controller-baseline-test-"
const state = await mkdtemp(join(root, prefix)), previous = process.env.MCP_STATE_DIR
process.env.MCP_STATE_DIR = state
t.after(async () => {
if (previous === undefined) delete process.env.MCP_STATE_DIR
else process.env.MCP_STATE_DIR = previous
assert.equal(dirname(resolve(state)), root)
assert(basename(state).startsWith(prefix))
await rm(state, { recursive: true })
})
const { fingerprint, privateSecretFingerprint } = security
const now = Date.now(), profileId = randomUUID(), planId = randomUUID(), operationId = randomUUID(), connectionId = randomUUID()
const grant = new Date(now + 120000).toISOString()
const principal = { connectionId, credentialId: randomUUID(), credentialExpiresAt: grant }
const profile = { serverId: 1, table: "tenant", probeVrf: "tenant", candidates: ["a", "b"].map(id => ({ id, jumpHostId: 1, exitNodeId: 1 })) }
const permissions = ["mm:network:read", "mm:network:write", "mm:filters:write"]
const connection = { id: connectionId, owner_id: "owner", generation: await security.accessGeneration(),
owner_valid_until: new Date(grant), owner_claims_issued_at: new Date(now), owner_permissions: permissions,
policy: { status: "active", mode: "operator", expiresAt: grant, permissions,
scope: { serverIds: [1], operationIds: ["routing.analyze", "routing.community.switch", "routing.path.verify"] } } }
const context = { principal, connection } as Parameters<typeof Baseline.prepareRoutingControllerBaseline>[4]
const originalRule = 'if (bgp-communities includes 65000:10) {\n set gw 192.0.2.1;\n set gw-interface "marker-gre-a";\n accept;\n}'
const desiredRule = originalRule.replace("192.0.2.1", "192.0.2.9").replace("marker-gre-a", "marker-gre-b")
const bareRule = desiredRule.replace('"marker-gre-b"', "marker-gre-b")
const signTarget = await security.privateFingerprinter("baseline-test-native-target")
const stableTarget = { ".id": "*1", chain: "input", comment: "managed" }
let observed = { ...stableTarget, rule: originalRule }, candidate = "a", failAfterRead = false, driftSourceAtFence = false
const targetProof = (rule: string) => signTarget(fingerprint({ ...observed, rule }))
const desiredTargetProof = targetProof(desiredRule), dependencyProof = "d".repeat(64), checkpointProof = "c".repeat(64)
const sourceProofs = [{ serverId: 1, proof: "a".repeat(64) }]
const receiptId = randomUUID(), requestSeal = "request-proof", networkSeal = "network-proof"
const input = { proofRevision: 3, profile, principal, ownerId: connection.owner_id, nativeId: "*1", originalCandidateId: "a",
originalTargetProof: targetProof(originalRule), desiredTargetProof, dependencyProof, contextSources: sourceProofs, candidateId: "b",
persistentProfileAuthority: { packet: { profileId, profileVersion: 1 } }, receiptId }
const step = { kind: "service", service: "routing.community.scoped", input }
const configuration = { checkpointProof, observedAt: new Date(now-100).toISOString(), packet: { revision: 2, operationId, stepIndex: 0,
nativeId: "*1", originalCandidateId: "a", originalTargetProof: input.originalTargetProof, desiredCandidateId: "b", desiredTargetProof,
dependencyProof, contextSources: sourceProofs, desiredRule } }
const nativeVerificationSeal = await privateSecretFingerprint(fingerprint({ request: requestSeal, dependency: dependencyProof, desired: desiredTargetProof,
candidate: "b", configuration: checkpointProof, network: networkSeal }), "route-ai-scoped-verified-v3")
const row = { id: operationId, plan_id: planId, state: "applied", step_state: "verified", operation_id: "routing.community.switch",
operation_version: 7, approved_by: "owner", created_at: new Date(now-200), intended_state: security.redact(step),
evidence: { sealed: await security.encryptSecret(JSON.stringify({ steps: [step], target: { scope: "servers", serverIds: [1] } })) },
result: { nativeVerificationSeal, configurationCheckpointProof: checkpointProof, networkCheckpointProof: networkSeal, receiptId,
proofRevision: 3, nativeWrites: 1, configurationVerified: true, postProbeStatus: "passed", controlProbeVerified: true } }
const providers: Record<string, unknown> = {
"./routing-main-transition.js": {},
"../../db/index.js": { pool: { query: async (_sql: string, ids: string[]) => {
assert.deepEqual(ids, [operationId, planId, connectionId])
return { rows: [failAfterRead ? { ...row, step_state: "unknown" } : row] }
} } },
"./registry.js": { findMcpOperation: () => ({ version: 7 }) },
"./routing-community-scoped-request.js": { assertScopedCommunityRequest: async () => requestSeal },
"./routing-profile-authority.js": { assertPersistentScopedProfileAuthority: async () => {},
routingProfileAuthorityBindingSchema: { parse: (value: unknown) => value } },
"./routing-community-scoped-checkpoint.js": { loadScopedConfigurationPacket: async () => configuration,
loadScopedNetworkCheckpoint: async () => ({ status: "passed", networkSeal }) },
"./routing-original-effects.js": {},
zod: { z }, "@mmapp/contracts/mcp-routing": { routeAiProfileSchema: { parse: (value: unknown) => value } },
"../../lib/permissions.js": { hasPermission: (set: string[], permission: string) => set.includes(permission) },
"./store.js": { effectiveMcpConnection: async () => connection },
"./security.js": security,
"./routing-community-context.js": { readRouteCommunityContext: async (_profile: unknown, options: { authorize(): Promise<void> }) => {
await options.authorize()
const target = { nativeId: observed[".id"], rawRow: observed, desiredTargetProof: targetProof }
return { before: { nativeRuleId: target.nativeId, scopedExecutionAvailable: true, currentCandidateId: candidate,
targetRuleProof: targetProof(observed.rule), dependencyProof, sourceProofs },
ownedTarget: { ...target, matchesRuleProof: (body: string, proof: unknown) => scopedTargetRuleMatches(target, body, proof) },
fence: async () => { await options.authorize(); if (driftSourceAtFence) failAfterRead = true } }
} },
}
const source = await readFile(new URL("./routing-controller-baseline.ts", import.meta.url), "utf8")
const compiled = ts.transpileModule(source, { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 } }).outputText
const baseline = {} as typeof Baseline
new Function("exports", "require", compiled)(baseline, (name: string) => { assert(providers[name], name); return providers[name] })
const original = await baseline.prepareRoutingControllerBaseline(profileId, 1, profile, grant, context)
const links = { planId, operationId, checkpointProof }
return { original, links, desiredTargetProof, targetProof, originalRule, desiredRule, bareRule, baseline,
advance: () => baseline.advanceRoutingControllerBaseline(original, profileId, 1, profile, grant, context, planId, operationId),
assertCurrent: (value: unknown) => baseline.assertRoutingControllerBaseline(value, profileId, 1, profile, grant, context),
driftAfterNativeRead: () => { driftSourceAtFence = true },
observe: (rule: string, metadata: Partial<typeof stableTarget> = {}, selected = "b") => { candidate = selected; observed = { ...stableTarget, ...metadata, rule } } }
}
test("controller signs an exact legacy-shaped next baseline when native target proof matches", async t => {
const f = await fixture(t)
await f.assertCurrent(f.original)
f.observe(f.desiredRule)
const next = await f.advance()
assert.equal(next.packet.targetProof, f.desiredTargetProof)
assert.deepEqual(next.packet.transition, f.links)
assert.equal(next.packet.previousProof, f.original.proof)
assert.deepEqual(await f.assertCurrent(next), next)
})
test("controller authenticates the source but enrolls the exact bare RouterOS readback", async t => {
const f = await fixture(t)
f.observe(f.bareRule)
const next = await f.advance()
assert.equal(next.packet.targetProof, f.targetProof(f.bareRule))
assert.notEqual(next.packet.targetProof, f.desiredTargetProof)
assert.deepEqual(next.packet.transition, { ...f.links, sourceDesiredTargetProof: f.desiredTargetProof })
assert.deepEqual(await f.assertCurrent(next), next)
const tampered = structuredClone(next)
tampered.packet.transition!.sourceDesiredTargetProof = "e".repeat(64)
await assert.rejects(f.baseline.routingControllerBaselinePrincipal(tampered), { code: "STATE_CONFLICT" })
f.observe(f.desiredRule)
await assert.rejects(f.assertCurrent(next), { code: "STATE_CONFLICT" })
f.observe(f.originalRule, {}, "a")
await assert.rejects(f.assertCurrent(next), { code: "STATE_CONFLICT" })
await f.assertCurrent(f.original)
})
test("controller refuses changed selected rule and drift in other native row fields", async t => {
const f = await fixture(t)
f.observe(f.bareRule.replace("marker-gre-b", "other"))
await assert.rejects(f.advance(), { code: "STATE_CONFLICT" })
f.observe(f.bareRule, { comment: "changed" })
await assert.rejects(f.advance(), { code: "STATE_CONFLICT" })
})
test("controller refuses baseline publication if the source operation changes after readback", async t => {
const f = await fixture(t)
f.observe(f.bareRule)
f.driftAfterNativeRead()
await assert.rejects(f.advance(), { code: "STATE_CONFLICT" })
})