Files
Jyotisha/frontend/tests/admin-auth.test.ts
T
2026-08-08 11:01:56 +08:00

198 lines
6.4 KiB
TypeScript

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<TestConfig>> = {},
): AdminAuthorizationDependencies<TestConfig> {
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<AdminAuthorizationDependencies<TestConfig>>,
): Promise<AdminAuthorizationError> {
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<Partial<AdminAuthorizationDependencies<TestConfig>>> = [
{
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: "后台服务暂时不可用" });
});