feat: implement TASK-WP1-04 admin security

This commit is contained in:
suyx
2026-07-28 18:32:50 +08:00
parent 66fe3b763a
commit 03f1509de7
29 changed files with 2344 additions and 40 deletions
+129
View File
@@ -0,0 +1,129 @@
import { createHmac, randomUUID } from "node:crypto";
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join, resolve } from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import { RegistrationError, RegistrationService } from "../../apps/api/src/registration.js";
import { MockResendAdapter } from "../../apps/api/src/resend-adapter.js";
const roots: string[] = [];
const services: RegistrationService[] = [];
const now = Date.parse("2026-07-28T10:00:00.000Z");
const adminPepper = Buffer.alloc(32, 0x91);
function allowlistHash(email: string) {
return createHmac("sha256", adminPepper).update(email.trim().toLowerCase(), "utf8").digest("hex").toUpperCase();
}
function createHarness() {
const root = mkdtempSync(join(tmpdir(), "dada-wp1-04-admin-"));
roots.push(root);
const resend = new MockResendAdapter();
const service = new RegistrationService({
adminAllowlistPepper: adminPepper,
challengePepper: Buffer.alloc(32, 0x92),
clock: () => now,
codeGenerator: () => "418205",
currentPrivacyNoticeVersion: "p0a-registration-notice-v1",
databasePath: join(root, "dada.sqlite3"),
invitePepper: Buffer.alloc(32, 0x93),
resend,
sessionPepper: Buffer.alloc(32, 0x94),
});
services.push(service);
return { resend, service };
}
function writeEvidence(file: string, value: unknown) {
const directory = process.env.DADA_EVIDENCE_DIR_ADMIN;
if (!directory) return;
mkdirSync(directory, { recursive: true });
writeFileSync(resolve(directory, file), `${JSON.stringify(value, null, 2)}\n`);
}
afterEach(() => {
for (const service of services.splice(0)) service.close();
for (const root of roots.splice(0)) rmSync(root, { force: true, recursive: true });
});
describe("TDD-WP1-ADM-001-admin-auth-boundary", () => {
it("keeps admin authentication allowlisted, multi-admin, isolated, and recoverable only by secure config", async () => {
const { resend, service } = createHarness();
const adminEmails = ["admin-one@example.invalid", "admin-two@example.invalid"];
const hashes = adminEmails.map(allowlistHash);
service.applySecureConfig({
adminAllowlistHashes: hashes,
adminRecoveryHashes: [],
secureConfigRevision: 1,
});
await expect(service.sendAdminLoginCode({ clientKey: "blocked-client", email: "blocked@example.invalid" }))
.rejects.toMatchObject({ reason: "admin_not_allowed" });
expect(resend.calls).toHaveLength(0);
const admins = [];
for (const [index, email] of adminEmails.entries()) {
const sent = await service.sendAdminLoginCode({ clientKey: `admin-client-${index}`, email });
const completed = service.completeAdminLogin({
clientKey: `admin-client-${index}`,
code: resend.readLatestCode(email),
idempotencyKey: `wp1-04-admin-login-${index}`.padEnd(40, "0"),
registrationId: sent.registrationId,
});
expect(completed).toMatchObject({ audience: "admin", status: "authenticated", admin: { role: "super_admin" } });
admins.push(completed);
}
const ordinaryUserId = randomUUID();
service.database.prepare(`
INSERT INTO users (
user_id, normalized_email, role, status, counts_toward_stage_limit,
registration_id, created_at
) VALUES (?, 'ordinary@example.invalid', 'user', 'active', 1, ?, ?)
`).run(ordinaryUserId, randomUUID(), now);
const ordinarySession = service.issueAuthenticatedSession(ordinaryUserId, "user");
expect(service.readAdminSession(ordinarySession.sessionToken)).toBeUndefined();
expect(service.readUserSession(admins[0].sessionToken)).toBeUndefined();
for (const admin of admins) service.revokeAdminSessions(admin.admin.userId, "disabled");
await expect(service.sendAdminLoginCode({ clientKey: "disabled-client", email: adminEmails[0] }))
.rejects.toMatchObject({ reason: "account_suspended" });
service.applySecureConfig({
adminAllowlistHashes: hashes,
adminRecoveryHashes: hashes,
secureConfigRevision: 2,
});
await expect(service.sendAdminLoginCode({ clientKey: "recovered-client", email: adminEmails[0] }))
.resolves.toMatchObject({ status: "verification_sent" });
const counts = {
adminAccess: service.database.prepare("SELECT COUNT(*) AS count FROM admin_access WHERE allowed = 1").get().count,
adminCredits: service.database.prepare(`
SELECT COUNT(*) AS count FROM credit_accounts c JOIN users u ON u.user_id = c.user_id WHERE u.role = 'super_admin'
`).get().count,
admins: service.database.prepare("SELECT COUNT(*) AS count FROM users WHERE role = 'super_admin'").get().count,
ordinaryUsers: service.database.prepare("SELECT COUNT(*) AS count FROM users WHERE role = 'user'").get().count,
};
expect(counts).toEqual({ adminAccess: 2, adminCredits: 0, admins: 2, ordinaryUsers: 1 });
const audits = service.database.prepare("SELECT actor_type, actor_ref, result FROM admin_operation_logs ORDER BY occurred_at").all();
expect(audits.length).toBeGreaterThanOrEqual(4);
expect(audits.every((entry: any) => ["system", "super_admin"].includes(entry.actor_type))).toBe(true);
expect(JSON.stringify(audits)).not.toContain("@example.invalid");
writeEvidence("response.json", {
admin_count: counts.admins,
audiences_isolated: true,
non_allowlisted_status: "rejected_before_send",
recovery_source: "secure_config_revision_2",
status: "passed",
});
writeEvidence("db-diff.json", { after: counts, admin_sessions_revoked_before_recovery: true, status: "passed" });
writeEvidence("external-calls.json", {
calls: resend.calls.map((call) => ({ purpose: call.purpose, recipient_kind: "synthetic_admin" })),
non_allowlisted_calls: 0,
status: "passed",
});
});
});