From 38ac9cecdcf2ae3be22f6acca674085b92880944 Mon Sep 17 00:00:00 2001 From: Jordan Diaz Date: Mon, 6 Apr 2026 21:52:13 +0000 Subject: [PATCH] MCP: bloquear escritura de records por accessList del usuario Co-Authored-By: Claude Opus 4.6 (1M context) --- mcp-server/tools/helpers/accessControl.js | 38 +++++++++++++++++++ mcp-server/tools/records/addModuleToRecord.js | 7 ++++ mcp-server/tools/records/createUpdate.js | 7 ++++ mcp-server/tools/records/delete.js | 7 ++++ 4 files changed, 59 insertions(+) create mode 100644 mcp-server/tools/helpers/accessControl.js diff --git a/mcp-server/tools/helpers/accessControl.js b/mcp-server/tools/helpers/accessControl.js new file mode 100644 index 0000000..8d3fc0d --- /dev/null +++ b/mcp-server/tools/helpers/accessControl.js @@ -0,0 +1,38 @@ +import fs from 'fs'; +import path from 'path'; + +/** + * Check if the current user has write access to a table. + * Reads .acai file from ACAI_PROJECT_DIR. + * Returns { allowed: true } or { allowed: false, error: "..." } + */ +export function canAccessTable(tableName) { + const projectDir = process.env.ACAI_PROJECT_DIR || ""; + if (!projectDir) return { allowed: true }; // no project dir, don't block + + const acaiFile = path.join(projectDir, ".acai"); + try { + if (!fs.existsSync(acaiFile)) return { allowed: true }; + const data = JSON.parse(fs.readFileSync(acaiFile, "utf-8")); + const user = data.user || {}; + + // Admin has full access + if (user.isAdmin === "1" || user.isAdmin === 1) return { allowed: true }; + + const accessList = user.accessList || {}; + if (!accessList || Object.keys(accessList).length === 0) return { allowed: true }; + + // all.accessLevel >= 9 means full access + const allAccess = parseInt(accessList.all?.accessLevel || "0"); + if (allAccess >= 9) return { allowed: true }; + + // Check specific table (without cms_ prefix) + const bare = tableName.replace(/^cms_/, ""); + const entry = accessList[bare]; + if (entry && parseInt(entry.accessLevel || "0") > 0) return { allowed: true }; + + return { allowed: false, error: `No tienes acceso a la tabla '${bare}'` }; + } catch (e) { + return { allowed: true }; // On error, don't block + } +} diff --git a/mcp-server/tools/records/addModuleToRecord.js b/mcp-server/tools/records/addModuleToRecord.js index 0aacd0e..c070bcc 100644 --- a/mcp-server/tools/records/addModuleToRecord.js +++ b/mcp-server/tools/records/addModuleToRecord.js @@ -3,6 +3,7 @@ import { withAuth, getSessionCredentials } from "../../auth/index.js"; import { handleApiResponse, handleToolError, validateRequired } from "../helpers/errorHandler.js"; import { AcaiHttpClient } from "../helpers/acaiHttpClient.js"; import { withAuthParams } from "../helpers/authSchema.js"; +import { canAccessTable } from "../helpers/accessControl.js"; export function registerAddModuleToRecordTool(server) { server.tool( @@ -29,6 +30,12 @@ Response includes: sectionId, moduleId, position, totalModules`, const validationError = validateRequired({ tableName, recordNum, moduleId }, ['tableName', 'recordNum', 'moduleId'], 'add_module_to_record'); if (validationError) return validationError; + // Check table access + const accessCheck = canAccessTable(tableName); + if (!accessCheck.allowed) { + return { content: [{ type: "text", text: JSON.stringify({ success: false, error: accessCheck.error }) }], isError: true }; + } + const sessionId = extra.sessionId; const credentials = await getSessionCredentials(sessionId); const payload = { diff --git a/mcp-server/tools/records/createUpdate.js b/mcp-server/tools/records/createUpdate.js index 2d522c0..9784a73 100644 --- a/mcp-server/tools/records/createUpdate.js +++ b/mcp-server/tools/records/createUpdate.js @@ -4,6 +4,7 @@ import { handleToolError, validateRequired, handleApiResponse } from "../helpers import { AcaiHttpClient } from "../helpers/acaiHttpClient.js"; import { table } from "console"; import { withAuthParams } from "../helpers/authSchema.js"; +import { canAccessTable } from "../helpers/accessControl.js"; export function registerCreateOrUpdateRecordTool(server) { server.tool( @@ -26,6 +27,12 @@ export function registerCreateOrUpdateRecordTool(server) { const validationError = validateRequired({ tableName, fields }, ['tableName', 'fields'], 'create_or_update_record'); if (validationError) return validationError; + // Check table access + const accessCheck = canAccessTable(tableName); + if (!accessCheck.allowed) { + return { content: [{ type: "text", text: JSON.stringify({ success: false, error: accessCheck.error }) }], isError: true }; + } + // if fields is string, try to parse as JSON if (typeof fields === 'string') { try { diff --git a/mcp-server/tools/records/delete.js b/mcp-server/tools/records/delete.js index a9daad3..9dfac33 100644 --- a/mcp-server/tools/records/delete.js +++ b/mcp-server/tools/records/delete.js @@ -3,6 +3,7 @@ import { withAuth, getSessionCredentials } from "../../auth/index.js"; import { handleToolError, validateRequired, handleApiResponse } from "../helpers/errorHandler.js"; import { AcaiHttpClient } from "../helpers/acaiHttpClient.js"; import { withAuthParams } from "../helpers/authSchema.js"; +import { canAccessTable } from "../helpers/accessControl.js"; export function registerDeleteTableRecordsTool(server) { server.tool( @@ -20,6 +21,12 @@ export function registerDeleteTableRecordsTool(server) { const validationError = validateRequired({ tableName }, ['tableName'], 'delete_table_records'); if (validationError) return validationError; + // Check table access + const accessCheck = canAccessTable(tableName); + if (!accessCheck.allowed) { + return { content: [{ type: "text", text: JSON.stringify({ success: false, error: accessCheck.error }) }], isError: true }; + } + if (!recordIds && !deleteAll) { return { content: [{ type: "text", text: "Error: You must provide either 'recordIds' or set 'deleteAll' to true." }],