feat: Token 支持多项目关联且全部为 MCP 令牌;配置操作全量审计,支持按项目/键查询
build-and-deploy / build-and-deploy (push) Successful in 40s
build-and-deploy / build-and-deploy (push) Successful in 40s
This commit is contained in:
+42
-4
@@ -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]);
|
||||
|
||||
Reference in New Issue
Block a user