feat: Token 支持多项目关联且全部为 MCP 令牌;配置操作全量审计,支持按项目/键查询
build-and-deploy / build-and-deploy (push) Successful in 40s

This commit is contained in:
jif2.zhang
2026-09-21 09:40:33 +08:00
parent be2e4d9cdf
commit 6bc738d359
13 changed files with 833 additions and 249 deletions
+42 -4
View File
@@ -9,7 +9,7 @@ const fs = require("node:fs");
const os = require("node:os");
const path = require("node:path");
const { createMcpServer } = require("../src/mcp/tools");
const { init, loadSystem, saveSystem } = require("../src/persistence/duckdbAdapter");
const { init, loadSystem, saveSystem, loadAuditLogs } = require("../src/persistence/duckdbAdapter");
// 使用临时数据目录,避免污染真实 data/
const tmpDataDir = fs.mkdtempSync(path.join(os.tmpdir(), "registry-mcp-test-"));
@@ -35,10 +35,12 @@ test.after(async () => {
* 直接调用已注册工具的处理函数
* @param {string} name - 工具名
* @param {Object} args - 工具参数
* @param {Object} [srv] - MCP 服务器实例(默认使用全局 server)
* @returns {Promise<Object>} 工具结果
*/
async function callTool(name, args) {
const tool = server._registeredTools[name];
async function callTool(name, args, srv) {
const target = srv || server;
const tool = target._registeredTools[name];
assert.ok(tool, `工具不存在: ${name}`);
try {
const result = await tool.handler(args, {});
@@ -153,13 +155,49 @@ test("edit 模式下可以修改配置说明", async () => {
assert.strictEqual(updatedObj.value, "localhost", "仅改说明,值不应变化");
// 清理测试项目
const delProject = await callTool("delete_project", { projectId: project.id });
const delProject = await callTool("delete_project", { projectId: project.id });
const confirmProject = await callTool("confirm_delete_project", {
confirmation_token: delProject.structuredContent.confirmation_token,
});
assert.strictEqual(confirmProject.structuredContent.status, "deleted");
});
test("Token 项目范围限制 MCP 配置访问,越权操作被拒绝且不入记录", async () => {
// 先创建真实项目,再构造仅允许该项目访问的 Token 会话
const projText = (await callTool("create_project", { name: "MCP-Scope-Proj" })).text;
const project = JSON.parse(projText.match(/\{[\s\S]*\}/)[0]);
const scoped = createMcpServer({ token: { name: "scope-token", projectIds: [project.id] }, ip: "10.1.2.3" });
// 无权访问的项目:读取/写入被拒绝
const deniedRead = await callTool("list_configs", { projectId: "other-proj" }, scoped);
assert.ok(deniedRead.isError);
assert.match(deniedRead.text, /无权访问/);
const deniedWrite = await callTool("create_config", { projectId: "other-proj", key: "x", description: "x" }, scoped);
assert.ok(deniedWrite.isError);
assert.match(deniedWrite.text, /无权访问/);
// 有权限:可读可写,并留下审计记录
const createdReal = await callTool("create_config", { projectId: project.id, key: "sql/dsn", description: "d", value: "v1" }, scoped);
assert.ok(!createdReal.isError);
const logsReal = await loadAuditLogs({ projectId: project.id, key: "sql/dsn" });
const createLog = logsReal.find((l) => l.action === "create");
assert.ok(createLog, "应在 audit_logs 中存在 create 记录");
assert.strictEqual(createLog.source, "mcp");
assert.strictEqual(createLog.actor, "scope-token");
assert.strictEqual(createLog.ip, "10.1.2.3");
assert.strictEqual(createLog.value, "v1");
// 越权操作不产生记录(other-proj 无任何记录)
const deniedLogs = await loadAuditLogs({ projectId: "other-proj" });
assert.strictEqual(deniedLogs.length, 0);
// 清理测试项目
const delProject = await callTool("delete_project", { projectId: project.id });
await callTool("confirm_delete_project", { confirmation_token: delProject.structuredContent.confirmation_token });
});
test("配置删除同样为两步式确认", async () => {
const { text: createText } = await callTool("create_project", { name: "MCP-Unittest-Cfg" });
const project = JSON.parse(createText.match(/\{[\s\S]*\}/)[0]);