import assert from "node:assert/strict"; import test from "node:test"; import { adminErrorResponse } from "../src/lib/admin/admin-error-response.ts"; import { AdminAuthorizationError, authorizeAdminRequest, type AdminAuthorizationDependencies, type AdminSession, } from "../src/lib/admin/auth-boundary.ts"; import { authorizeAdminAccess } from "../src/lib/admin/auth-policy.ts"; import type { IdentityUser } from "../src/modules/identity/contracts.ts"; import { IdentityAuthorizationError, type IdentityServerSession, } from "../src/modules/identity/session.ts"; function user(): IdentityUser { return { id: "11111111-1111-4111-8111-111111111111", email: "admin@example.com", emailVerified: true, twoFactorEnabled: false, name: "Admin", image: null, role: ["user"], }; } function identitySession(): IdentityServerSession { return { user: user(), sessionId: "session-id", sessionToken: "session-token", expiresAt: new Date("2026-08-08T00:00:00.000Z"), }; } function adminSession(): AdminSession { const identity = identitySession(); return { user: identity.user, roles: ["owner"], permissions: ["admin.access"], requiresMfa: false, identitySession: { id: identity.sessionId, token: identity.sessionToken, expiresAt: identity.expiresAt, }, }; } type TestConfig = { adminHost: string }; function dependencies( overrides: Partial> = {}, ): AdminAuthorizationDependencies { return { readAuthProvider: () => "self-hosted", readRequestHeaders: () => new Headers({ host: "admin.example.com" }), readIdentityConfig: () => ({ adminHost: "admin.example.com" }), resolveIdentitySurface: (host, config) => host === config.adminHost ? "admin" : null, requireIdentitySession: async () => identitySession(), loadAdminSession: async () => adminSession(), ...overrides, }; } async function rejectedAuthorization( overrides: Partial>, ): Promise { try { await authorizeAdminRequest("admin.access", undefined, dependencies(overrides)); } catch (error) { assert.ok(error instanceof AdminAuthorizationError); return error; } assert.fail("authorization unexpectedly succeeded"); } test("anonymous admin access is 401", () => { assert.deepEqual(authorizeAdminAccess(null, [], "admin.access"), { allowed: false, status: 401, }); }); test("missing permission is 403", () => { assert.deepEqual(authorizeAdminAccess(user(), [], "admin.access"), { allowed: false, status: 403, }); assert.deepEqual(authorizeAdminAccess(user(), ["admin.access"], "models.publish"), { allowed: false, status: 403, }); }); test("database permission keys authorize only the requested operation", () => { assert.deepEqual(authorizeAdminAccess(user(), ["admin.access"], "admin.access"), { allowed: true, }); assert.deepEqual(authorizeAdminAccess(user(), ["models.read", "models.publish"], "models.publish"), { allowed: true, }); }); test("admin authorization checks the configured admin Host before session lookup", async () => { let sessionRead = false; const error = await rejectedAuthorization({ readRequestHeaders: () => new Headers({ host: "user.example.com" }), requireIdentitySession: async () => { sessionRead = true; return identitySession(); }, }); assert.equal(error.status, 403); assert.equal(error.message, "无权访问后台"); assert.equal(sessionRead, false); }); test("known admin and identity authorization errors preserve 401/403 semantics", async () => { const original = new AdminAuthorizationError("请求来源不可信", 403); const preserved = await rejectedAuthorization({ readIdentityConfig: () => { throw original; }, }); assert.equal(preserved, original); for (const [status, message] of [[401, "请先登录"], [403, "无权访问后台"]] as const) { const mapped = await rejectedAuthorization({ requireIdentitySession: async () => { throw new IdentityAuthorizationError("provider detail", status); }, }); assert.equal(mapped.status, status); assert.equal(mapped.message, message); } }); test("configuration, identity reader, and RBAC query failures become sanitized 503", async () => { const failures: Array>> = [ { readIdentityConfig: () => { throw new Error("BETTER_AUTH_USER_SECRET leaked detail"); }, }, { requireIdentitySession: async () => { throw new Error("better-auth or identity database leaked detail"); }, }, { loadAdminSession: async () => { throw new Error("postgres admin_permission_keys leaked detail"); }, }, ]; for (const failure of failures) { const error = await rejectedAuthorization(failure); assert.equal(error.status, 503); assert.equal(error.message, "后台服务暂时不可用"); } }); test("adminErrorResponse maps model provider configuration failures without exposing details", async () => { for (const [error, status, message] of [ [new Error("模型供应商地址不在服务器允许列表中"), 400, "模型供应商地址不在服务器允许列表中"], [new Error("网关地址不允许解析到本机、内网或保留地址"), 400, "网关地址不允许解析到本机、内网或保留地址"], [new Error("model_provider_url_unsafe"), 400, "模型供应商地址不安全或解析到内网"], [new Error("model_provider_key_required"), 409, "模型供应商密钥未配置"], ] as const) { const response = adminErrorResponse(error); assert.equal(response.status, status); assert.deepEqual(await response.json(), { error: message }); } for (const error of [new Error("model_provider_runtime_immutable"), new Error("model_provider_code_immutable")]) { const response = adminErrorResponse(error); assert.equal(response.status, 500); assert.deepEqual(await response.json(), { error: "后台服务暂时不可用" }); } }); test("adminErrorResponse preserves the sanitized authorization 503", async () => { const response = adminErrorResponse( new AdminAuthorizationError("后台服务暂时不可用", 503), ); assert.equal(response.status, 503); assert.deepEqual(await response.json(), { error: "后台服务暂时不可用" }); });