Права доступа API-ключей: скоупы read/write/full и выборочный доступ к формам и справочникам
- organization_api_keys.scopes (jsonb, NULL = полный доступ для legacy-ключей) - MCP: фильтрация инструментов по режиму и проверка formIds в handler'ах - PATCH /api/mcp-keys/:id, валидация скоупов в server/utils/api-key.ts - UI Settings: диалог создания/редактирования ключа с режимом и мультивыбором форм/справочников
This commit is contained in:
@@ -2,6 +2,7 @@ import { storage } from "../storage";
|
||||
import { authenticateToken, requirePermission, type AuthenticatedRequest } from "../middleware/auth.middleware";
|
||||
import { tenantIsolation } from "../middleware/tenant.middleware";
|
||||
import { type ReminderRecipient } from "@shared/schema";
|
||||
import { normalizeApiKeyScopes, parseApiKeyScopesInput } from "../utils/api-key";
|
||||
import { handleMcpRequest, handleMcpSse, handleMcpMessages } from "../mcp";
|
||||
import { setupSwagger } from "../swagger";
|
||||
import express, { type Request, type Response, type NextFunction } from 'express';
|
||||
@@ -45,6 +46,8 @@ async function requireMcpApiKey(req: Request, res: Response, next: NextFunction)
|
||||
const apiKey = await storage.getApiKeyByHash(trimmed);
|
||||
if (apiKey && apiKey.isActive) {
|
||||
storage.touchApiKey(apiKey.id).catch(() => {});
|
||||
// Прикрепляем нормализованные скоупы ключа к запросу для downstream-обработчиков
|
||||
(req as any).apiKeyScopes = normalizeApiKeyScopes(apiKey.scopes);
|
||||
next();
|
||||
return;
|
||||
}
|
||||
@@ -82,7 +85,11 @@ export function registerMcpRagRoutes(app: import("express").Express): void {
|
||||
app.get('/api/mcp-keys', authenticateToken, tenantIsolation, requirePermission('settings.manage'), async (req: AuthenticatedRequest, res) => {
|
||||
try {
|
||||
const keys = await storage.listApiKeys(req.organizationId!);
|
||||
const safeKeys = keys.map(({ keyHash: _h, ...rest }) => rest);
|
||||
// Возвращаем scopes каждого ключа в нормализованном виде (null в БД = полный доступ)
|
||||
const safeKeys = keys.map(({ keyHash: _h, scopes, ...rest }) => ({
|
||||
...rest,
|
||||
scopes: normalizeApiKeyScopes(scopes),
|
||||
}));
|
||||
res.json({ success: true, keys: safeKeys });
|
||||
} catch (error) {
|
||||
console.error('List MCP keys error:', error);
|
||||
@@ -93,7 +100,14 @@ export function registerMcpRagRoutes(app: import("express").Express): void {
|
||||
app.post('/api/mcp-keys', authenticateToken, tenantIsolation, requirePermission('settings.manage'), async (req: AuthenticatedRequest, res) => {
|
||||
try {
|
||||
const label = (req.body?.label as string | undefined)?.trim() || 'Default';
|
||||
const { key, record } = await storage.createApiKey(req.organizationId!, req.user!.id, label);
|
||||
// Скоупы необязательны: без них ключ создаётся с полным доступом (scopes = NULL)
|
||||
let scopes: import('@shared/schema').ApiKeyScopes | null = null;
|
||||
if (req.body?.scopes !== undefined && req.body?.scopes !== null) {
|
||||
const parsed = parseApiKeyScopesInput(req.body.scopes);
|
||||
if (!parsed.ok) return res.status(400).json({ error: parsed.error });
|
||||
scopes = parsed.scopes;
|
||||
}
|
||||
const { key, record } = await storage.createApiKey(req.organizationId!, req.user!.id, label, scopes);
|
||||
const { keyHash: _h, ...safeRecord } = record;
|
||||
res.json({ success: true, key, record: safeRecord });
|
||||
} catch (error) {
|
||||
@@ -102,6 +116,46 @@ export function registerMcpRagRoutes(app: import("express").Express): void {
|
||||
}
|
||||
});
|
||||
|
||||
// Обновление label и/или scopes существующего ключа.
|
||||
// scopes: null в body — сброс к полному доступу (NULL в БД).
|
||||
app.patch('/api/mcp-keys/:id', authenticateToken, tenantIsolation, requirePermission('settings.manage'), async (req: AuthenticatedRequest, res) => {
|
||||
try {
|
||||
const id = parseInt(req.params.id);
|
||||
if (isNaN(id)) return res.status(400).json({ error: 'Неверный ID' });
|
||||
|
||||
const updates: { label?: string; scopes?: import('@shared/schema').ApiKeyScopes | null } = {};
|
||||
|
||||
if (req.body?.label !== undefined) {
|
||||
if (typeof req.body.label !== 'string' || !req.body.label.trim()) {
|
||||
return res.status(400).json({ error: 'Поле label должно быть непустой строкой' });
|
||||
}
|
||||
updates.label = req.body.label.trim();
|
||||
}
|
||||
|
||||
if (req.body?.scopes !== undefined) {
|
||||
if (req.body.scopes === null) {
|
||||
updates.scopes = null; // сброс к полному доступу
|
||||
} else {
|
||||
const parsed = parseApiKeyScopesInput(req.body.scopes);
|
||||
if (!parsed.ok) return res.status(400).json({ error: parsed.error });
|
||||
updates.scopes = parsed.scopes;
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(updates).length === 0) {
|
||||
return res.status(400).json({ error: 'Нечего обновлять: передайте label и/или scopes' });
|
||||
}
|
||||
|
||||
const updated = await storage.updateApiKey(id, req.organizationId!, updates);
|
||||
if (!updated) return res.status(404).json({ error: 'Ключ не найден' });
|
||||
const { keyHash: _h, ...safeRecord } = updated;
|
||||
res.json({ success: true, record: safeRecord });
|
||||
} catch (error) {
|
||||
console.error('Update MCP key error:', error);
|
||||
res.status(500).json({ error: 'Ошибка обновления ключа' });
|
||||
}
|
||||
});
|
||||
|
||||
app.delete('/api/mcp-keys/:id', authenticateToken, tenantIsolation, requirePermission('settings.manage'), async (req: AuthenticatedRequest, res) => {
|
||||
try {
|
||||
const id = parseInt(req.params.id);
|
||||
|
||||
Reference in New Issue
Block a user