fix(files,security): S-1 per-source Move authorization, S-2 session-only annotation, S-5 restore guards
S-1 (/files/move, /files/organize action=move): - organize-guard: authorizeMoveEntities checks every source's media entities (declared entity plus entities of registered assets under the source); one-time approval still covers the whole request; any source that cannot be attributed to an entity only passes with an approval; denial lists every unauthorized item; optional non-consuming pre-check - transfer: entities per source (directory = registered assets beneath it) - organize: all move groups pre-checked before any group executes; any denial -> whole request 403 approval_required S-2: - api.ts: GET /files/download and POST /files/trash/purge marked auth:"session"; files routes attach requireAdminSession from the registry annotation; handler-side purge check removed, download keeps a same-rule defense check; permanent delete uses isAdminSession - DEV key rule written down: equals an admin-scope key for session-only endpoints, still admin for Move/delete approval bypass (as authorizeMove) - api-registry.test: session list updated; new reverse assertion that no non-session endpoint answers 403 to an admin-scope bearer S-5 (trash restore): - restore-to requires a writable managed library root (isWritableTarget) - in-place restore rebuilds missing parents level by level without following symlinks (mkdirWithin), realpath-checked per level - cross-volume restore capped by FILES_TRASH_CROSS_MAX_BYTES; cross-volume copies (trash in / restore out) go through fsproxy Tests: multi-entity move 403 + listing, unattributed source, organize foreign asset 403, download/purge 403 for DEV and admin-scope keys, restore target not writable, symlinked parent, rebuild parents, cross-volume cap. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
9887d8b311
commit
17cf3cbf97
16 files changed
+587
-107
No files matched your search
@@ -2,11 +2,11 @@ import { createRoute } from "@hono/zod-openapi";
|
||||
import { Api, apiResponse, type EndpointDef } from "@umt/contracts";
|
||||
import type { Context } from "hono";
|
||||
import type { z } from "zod";
|
||||
import { type AuthEnv, requireScope } from "../../http/auth.ts";
|
||||
import { type AuthEnv, requireAdminSession, requireScope } from "../../http/auth.ts";
|
||||
import { errorBody } from "../../http/errors.ts";
|
||||
import { PathGuardError } from "../acquisition-organize/services/organize-guard.ts";
|
||||
import type { ModuleApp } from "../types.ts";
|
||||
import { FilesError, type FmDeps, fsErrorToFiles, isAdmin } from "./common.ts";
|
||||
import { FilesError, type FmDeps, fsErrorToFiles } from "./common.ts";
|
||||
import { downloadFile } from "./download.ts";
|
||||
import { identify } from "./identify.ts";
|
||||
import { idempotent } from "./ledger.ts";
|
||||
@@ -31,7 +31,8 @@ function mount(app: ModuleApp, def: EndpointDef, handler: Handler, raw = false)
|
||||
path: def.path,
|
||||
tags: ["files"],
|
||||
summary: def.summary,
|
||||
middleware: [requireScope(def.scope)] as const,
|
||||
// auth:"session" 的端点(download / trash purge)由中间件统一只放行管理员控制台会话,与注册表标注同源
|
||||
middleware: [requireScope(def.scope), ...(def.auth === "session" ? [requireAdminSession()] : [])] as const,
|
||||
request: {
|
||||
...(def.query ? { query: def.query as z.ZodObject } : {}),
|
||||
...(def.body ? { body: { content: { "application/json": { schema: def.body } }, required: true } } : {}),
|
||||
@@ -97,11 +98,10 @@ export function registerFileRoutes(app: ModuleApp, d: FmDeps) {
|
||||
mount(app, F.trashRestore, async (c, { body }) =>
|
||||
idem(d, c, "trash_restore", body, () => restoreTrash(d, c.get("principal"), body)),
|
||||
);
|
||||
mount(app, F.trashPurge, async (c, { body }) => {
|
||||
// 授权矩阵:清空回收站仅 admin 会话,API Key(含 admin scope)一律拒绝
|
||||
if (!isAdmin(c.get("principal"))) throw new FilesError("forbidden", "清空回收站只允许控制台管理员会话");
|
||||
return idem(d, c, "trash_purge", body, () => purgeTrash(d, c.get("principal"), body));
|
||||
});
|
||||
// 授权矩阵:清空回收站仅 admin 会话(注册表 auth:"session",中间件执行),API Key 与 DEV key 一律拒绝
|
||||
mount(app, F.trashPurge, async (c, { body }) =>
|
||||
idem(d, c, "trash_purge", body, () => purgeTrash(d, c.get("principal"), body)),
|
||||
);
|
||||
mount(app, F.identify, async (c, { body }) => identify(d, c.get("principal"), body));
|
||||
mount(app, F.scrape, async (c, { body }) => idem(d, c, "scrape", body, () => scrape(d, c.get("principal"), body)));
|
||||
mount(app, F.probe, async (_c, { body }) => probeFiles(d, body));
|
||||
|
||||
Reference in new issue
Block a user