132 lines
5.8 KiB
TypeScript
132 lines
5.8 KiB
TypeScript
import express, { Router } from 'express';
|
||
import { canManageSpace } from './visibility.js';
|
||
import { viewerOf } from './space-viewer.js';
|
||
import { logger } from '../logger.js';
|
||
import { parseToolPolicy } from '../engine/workspace-tool-policy.js';
|
||
import {
|
||
listToolCategories,
|
||
SENSITIVE_CATEGORIES,
|
||
SENSITIVE_TOOLS,
|
||
} from '../engine/tools/tool-categories.js';
|
||
import type { SpaceApiDeps } from './space-api.js';
|
||
|
||
/** スペースのツールポリシー GET/PUT。createSpaceApi から分離(巨大関数分割)。挙動は等価。 */
|
||
export function registerSpaceToolPolicyRoutes(router: Router, deps: SpaceApiDeps): void {
|
||
const { repo } = deps;
|
||
const jsonParser = express.json({ limit: deps.uploadLimitMb ? `${deps.uploadLimitMb}mb` : '50mb' });
|
||
|
||
// ─── ツールポリシー ─────────────────────────────────────────────────────────
|
||
//
|
||
// GET: スペース可視者(viewer 以上)が読める。
|
||
// PUT: canManageSpace(owner / admin / member.role==='owner')のみ書ける。
|
||
//
|
||
// 返却形状(GET / PUT 共通):
|
||
// { policy: WorkspaceToolPolicy,
|
||
// categories: { name, sensitive, enabled }[],
|
||
// sensitiveTools: { name, enabled }[] }
|
||
//
|
||
// 未知のカテゴリ/ツール名を enabledSensitive / disabledSafe に含めると 400。
|
||
// 既知かどうかの判定: SENSITIVE_CATEGORIES ∪ listToolCategories() の和集合 +
|
||
// SENSITIVE_TOOLS を allowlist として使う。
|
||
|
||
async function buildPolicyResponse(spaceId: string) {
|
||
const json = repo.getSpaceToolPolicy(spaceId);
|
||
const policy = parseToolPolicy(json);
|
||
const allCats = await listToolCategories();
|
||
const disabledSafe = new Set(policy.disabledSafe ?? []);
|
||
const enabledSensitive = new Set(policy.enabledSensitive ?? []);
|
||
|
||
const categories = allCats.map((name) => {
|
||
const sensitive = SENSITIVE_CATEGORIES.has(name);
|
||
const enabled = sensitive ? enabledSensitive.has(name) : !disabledSafe.has(name);
|
||
return { name, sensitive, enabled };
|
||
});
|
||
|
||
const sensitiveTools = [...SENSITIVE_TOOLS].map((name) => ({
|
||
name,
|
||
enabled: enabledSensitive.has(name),
|
||
}));
|
||
|
||
return { policy, categories, sensitiveTools };
|
||
}
|
||
|
||
// 既知のカテゴリ+ツール名の allowlist を組み立てる(validation 用)
|
||
async function buildKnownSensitiveNames(): Promise<Set<string>> {
|
||
const allCats = await listToolCategories();
|
||
const known = new Set<string>(SENSITIVE_CATEGORIES);
|
||
for (const c of allCats) known.add(c); // all cats are valid candidates
|
||
for (const t of SENSITIVE_TOOLS) known.add(t);
|
||
return known;
|
||
}
|
||
|
||
// GET /:id/tool-policy — viewer 可
|
||
router.get('/:id/tool-policy', async (req, res) => {
|
||
const viewer = viewerOf(req, deps.authActive);
|
||
const space = await repo.getSpace(req.params.id, { viewer });
|
||
if (!space) return res.status(404).json({ error: 'not found' });
|
||
try {
|
||
const body = await buildPolicyResponse(space.id);
|
||
res.json(body);
|
||
} catch (err) {
|
||
logger.error(`[space-api] tool-policy GET failed space=${space.id} err=${(err as Error).message}`);
|
||
res.status(500).json({ error: 'internal error' });
|
||
}
|
||
});
|
||
|
||
// PUT /:id/tool-policy — canManageSpace 必須
|
||
router.put('/:id/tool-policy', jsonParser, async (req, res) => {
|
||
const viewer = viewerOf(req, deps.authActive);
|
||
const space = await repo.getSpace(req.params.id, { viewer });
|
||
if (!space) return res.status(404).json({ error: 'not found' });
|
||
const memberRole = repo.getSpaceMemberRole(space.id, viewer.id);
|
||
if (!canManageSpace(viewer, { ownerId: space.ownerId }, memberRole)) {
|
||
return res.status(403).json({ error: 'forbidden' });
|
||
}
|
||
|
||
const { disabledSafe, enabledSensitive } = req.body ?? {};
|
||
|
||
// Validate: both fields must be arrays of strings if present
|
||
if (disabledSafe !== undefined && !Array.isArray(disabledSafe)) {
|
||
return res.status(400).json({ error: 'disabledSafe must be an array' });
|
||
}
|
||
if (enabledSensitive !== undefined && !Array.isArray(enabledSensitive)) {
|
||
return res.status(400).json({ error: 'enabledSensitive must be an array' });
|
||
}
|
||
if (disabledSafe && disabledSafe.some((x: unknown) => typeof x !== 'string')) {
|
||
return res.status(400).json({ error: 'disabledSafe must be an array of strings' });
|
||
}
|
||
if (enabledSensitive && enabledSensitive.some((x: unknown) => typeof x !== 'string')) {
|
||
return res.status(400).json({ error: 'enabledSensitive must be an array of strings' });
|
||
}
|
||
|
||
// Validate: unknown category/tool names → 400
|
||
const known = await buildKnownSensitiveNames();
|
||
if (disabledSafe) {
|
||
const unknown = (disabledSafe as string[]).filter((n) => !known.has(n));
|
||
if (unknown.length > 0) {
|
||
return res.status(400).json({ error: `unknown category names in disabledSafe: ${unknown.join(', ')}` });
|
||
}
|
||
}
|
||
if (enabledSensitive) {
|
||
const unknown = (enabledSensitive as string[]).filter((n) => !known.has(n));
|
||
if (unknown.length > 0) {
|
||
return res.status(400).json({ error: `unknown names in enabledSensitive: ${unknown.join(', ')}` });
|
||
}
|
||
}
|
||
|
||
const cleanPolicy = {
|
||
...(disabledSafe && disabledSafe.length > 0 ? { disabledSafe: disabledSafe as string[] } : {}),
|
||
...(enabledSensitive && enabledSensitive.length > 0 ? { enabledSensitive: enabledSensitive as string[] } : {}),
|
||
};
|
||
|
||
try {
|
||
repo.setSpaceToolPolicy(space.id, JSON.stringify(cleanPolicy));
|
||
const body = await buildPolicyResponse(space.id);
|
||
res.json(body);
|
||
} catch (err) {
|
||
logger.error(`[space-api] tool-policy PUT failed space=${space.id} err=${(err as Error).message}`);
|
||
res.status(500).json({ error: 'internal error' });
|
||
}
|
||
});
|
||
}
|