Атрибуция от бота в MCP/REST + REST API по ключу (этап 2)

- MCP: getActor (владелец ключа + бот), аудит changedByName=бот/label ключа, botId, metadata.source='mcp'; аудит для update_task/update_task_status
- sendTaskMessage: botId (messageType 'bot'), починен путь isBotToken без req.user
- authenticateTokenOrApiKey: JWT или X-Api-Key с белым списком endpoint'ов (/api/upload, messages, field-values, create task), проверки скоупов, аудит source='api'
- Лимиты файлов через env: UPLOAD_IMAGE_MAX_MB=25, UPLOAD_DOC_MAX_MB=100, UPLOAD_MAX_MB=100
- MCP upload-инструменты: параметр fileUrl для привязки уже загруженного файла; новый инструмент get_api_guide
This commit is contained in:
2026-07-22 15:18:04 +03:00
parent 68153caef2
commit 8cfd49fd9f
13 changed files with 720 additions and 246 deletions

View File

@@ -1,12 +1,13 @@
import { Router } from "express";
import { z } from 'zod';
import { storage } from "../storage";
import { authenticateToken, tryBotServiceToken, type AuthenticatedRequest } from "../middleware/auth.middleware";
import { authenticateToken, authenticateTokenOrApiKey, tryBotServiceToken, type AuthenticatedRequest } from "../middleware/auth.middleware";
import { tenantIsolation } from "../middleware/tenant.middleware";
import { validateRequest } from "../middleware/validation.middleware";
import { createTaskMessageSchema } from "@shared/schema";
import { eventBus } from "./shared";
import { sendTaskMessage, SendTaskMessageError } from "../services/task-message.service";
import { isFormAllowedByScopes } from "../utils/api-key";
export function registerChatMessageRoutes(router: Router): void {
// Get task messages
@@ -40,10 +41,10 @@ export function registerChatMessageRoutes(router: Router): void {
}
);
// Create task message
// Create task message (JWT пользователя или API-ключ организации)
router.post('/api/tasks/:id/messages',
tryBotServiceToken,
authenticateToken,
authenticateTokenOrApiKey,
tenantIsolation,
validateRequest(createTaskMessageSchema.omit({ taskId: true })),
async (req: AuthenticatedRequest, res) => {
@@ -65,9 +66,9 @@ export function registerChatMessageRoutes(router: Router): void {
{
const taskRolesForGuard = await storage.getTaskRoles(taskId, req.organizationId!);
if (taskRolesForGuard.length > 0) {
if (req.user && taskRolesForGuard.length > 0) {
const hasAccess = await storage.canUserAccessTask(
taskId, req.user!.id, req.organizationId!, req.user!.appRole
taskId, req.user.id, req.organizationId!, req.user.appRole
);
if (!hasAccess) {
return res.status(403).json({ success: false, error: 'Нет доступа к этой задаче' });
@@ -75,12 +76,38 @@ export function registerChatMessageRoutes(router: Router): void {
}
}
// Проверки для авторизации по API-ключу (JWT-путь не меняется)
if (req.apiKey) {
if (req.apiKey.scopes.mode === 'read') {
return res.status(403).json({ success: false, error: 'API-ключ в режиме read не может отправлять сообщения' });
}
if (!isFormAllowedByScopes(req.apiKey.scopes, task.formId)) {
return res.status(403).json({ success: false, error: 'Доступ к форме этой задачи запрещён правами API-ключа' });
}
}
// Автор сообщения: пользователь (JWT); по ключу бота — сам бот;
// по ключу без бота — владелец ключа
let messageUser = req.user ?? null;
let messageBotId: number | null = null;
if (!messageUser && req.apiKey) {
if (req.apiKey.botId) {
messageBotId = req.apiKey.botId;
} else {
messageUser = await storage.getUser(req.apiKey.createdBy);
}
}
if (!messageUser && !messageBotId) {
return res.status(401).json({ success: false, error: 'Не удалось определить автора сообщения' });
}
// Основная логика (сообщение + side-эффекты) вынесена в сервис —
// переиспользуется также MCP-сервером (инструмент send_task_message).
try {
const createdMessage = await sendTaskMessage({
task,
user: req.user!,
user: messageUser ?? undefined,
botId: messageBotId,
organizationId: req.organizationId!,
message: req.body.message || '',
messageType: req.body.messageType,