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
@@ -1,7 +1,7 @@
|
||||
import { basename, extname } from "node:path";
|
||||
import type { FilesDownloadQuery } from "@umt/contracts";
|
||||
import type { Principal } from "../../http/auth.ts";
|
||||
import { FilesError, type FmDeps, isAdmin, recheck, resolveIn } from "./common.ts";
|
||||
import { FilesError, type FmDeps, isAdminSession, recheck, resolveIn } from "./common.ts";
|
||||
import { audit } from "./ledger.ts";
|
||||
import { kindOfName } from "./list.ts";
|
||||
|
||||
@@ -41,7 +41,9 @@ export async function downloadFile(
|
||||
principal: Principal | undefined,
|
||||
q: FilesDownloadQuery,
|
||||
): Promise<Response> {
|
||||
if (!isAdmin(principal)) throw new FilesError("forbidden", "下载只允许控制台管理员会话(API Key 不可用)");
|
||||
// 路由中间件已按 auth:"session" 拦截;这里同口径复核(纵深防御)
|
||||
if (!isAdminSession(principal))
|
||||
throw new FilesError("forbidden", "下载只允许控制台管理员会话(API Key / DEV key 不可用)");
|
||||
const r = await resolveIn(d, q.root, q.path);
|
||||
if (r.rel === "" || r.lst?.isDirectory()) throw new FilesError("bad_request", "只能下载文件,不能下载目录");
|
||||
const name = basename(r.abs);
|
||||
|
||||
Reference in new issue
Block a user