feat: implement TASK-WP1-04 admin security
This commit is contained in:
@@ -0,0 +1,111 @@
|
||||
import { createHmac, randomUUID } from "node:crypto";
|
||||
import { mkdtempSync, rmSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
|
||||
import { createApp } from "../../apps/api/src/app.js";
|
||||
import { 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-28T12:00:00.000Z");
|
||||
const adminPepper = Buffer.alloc(32, 0xb1);
|
||||
const headers = { host: "127.0.0.1:43121", origin: "http://127.0.0.1:43121" };
|
||||
|
||||
function createHarness() {
|
||||
const root = mkdtempSync(join(tmpdir(), "dada-wp1-04-api-"));
|
||||
roots.push(root);
|
||||
const resend = new MockResendAdapter();
|
||||
const registration = new RegistrationService({
|
||||
adminAllowlistPepper: adminPepper,
|
||||
challengePepper: Buffer.alloc(32, 0xb2),
|
||||
clock: () => now,
|
||||
codeGenerator: () => "418205",
|
||||
currentPrivacyNoticeVersion: "p0a-registration-notice-v1",
|
||||
databasePath: join(root, "dada.sqlite3"),
|
||||
invitePepper: Buffer.alloc(32, 0xb3),
|
||||
resend,
|
||||
sessionPepper: Buffer.alloc(32, 0xb4),
|
||||
});
|
||||
services.push(registration);
|
||||
return { registration, resend };
|
||||
}
|
||||
|
||||
function cookieValue(setCookie: string | string[] | undefined, name: string) {
|
||||
const entries = Array.isArray(setCookie) ? setCookie : [setCookie ?? ""];
|
||||
const match = entries.find((entry) => entry.startsWith(`${name}=`));
|
||||
if (!match) throw new Error(`Cookie ${name} was not returned.`);
|
||||
return match.split(";", 1)[0];
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
for (const service of services.splice(0)) service.close();
|
||||
for (const root of roots.splice(0)) rmSync(root, { force: true, recursive: true });
|
||||
});
|
||||
|
||||
describe("TASK-WP1-04 admin auth API", () => {
|
||||
it("rejects before Resend, creates an admin session, and isolates both audiences", async () => {
|
||||
const { registration, resend } = createHarness();
|
||||
const email = "api-admin@example.invalid";
|
||||
registration.applySecureConfig({
|
||||
adminAllowlistHashes: [createHmac("sha256", adminPepper).update(email).digest("hex").toUpperCase()],
|
||||
adminRecoveryHashes: [],
|
||||
secureConfigRevision: 1,
|
||||
});
|
||||
const app = await createApp({ browserGate: false, networkBoundary: { allowTestPort: true }, registration });
|
||||
|
||||
const blocked = await app.inject({
|
||||
headers,
|
||||
method: "POST",
|
||||
payload: { email: "blocked@example.invalid" },
|
||||
url: "/api/v1/admin-auth/login/send",
|
||||
});
|
||||
expect(blocked.statusCode).toBe(409);
|
||||
expect(blocked.json()).toMatchObject({ error: { details: { field_errors: [{ message_key: "admin.auth.not_allowed" }] } } });
|
||||
expect(resend.calls).toHaveLength(0);
|
||||
|
||||
const sent = await app.inject({
|
||||
headers,
|
||||
method: "POST",
|
||||
payload: { email },
|
||||
url: "/api/v1/admin-auth/login/send",
|
||||
});
|
||||
expect(sent.statusCode).toBe(200);
|
||||
const flowCookie = cookieValue(sent.headers["set-cookie"], "dada_admin_auth_flow");
|
||||
const code = resend.readLatestCode(email);
|
||||
const completed = await app.inject({
|
||||
headers: { ...headers, cookie: flowCookie, "idempotency-key": "wp1-04-api-admin-complete-000000000001" },
|
||||
method: "POST",
|
||||
payload: { registration_id: sent.json().registration_id, verification_code: code },
|
||||
url: "/api/v1/admin-auth/login/complete",
|
||||
});
|
||||
expect(completed.statusCode).toBe(200);
|
||||
expect(completed.json()).toMatchObject({ audience: "admin", status: "authenticated", admin: { role: "super_admin" } });
|
||||
const adminCookie = cookieValue(completed.headers["set-cookie"], "dada_admin_session");
|
||||
|
||||
const session = await app.inject({ headers: { ...headers, cookie: adminCookie }, method: "GET", url: "/api/v1/admin-auth/session" });
|
||||
expect(session.statusCode).toBe(200);
|
||||
expect(session.json()).toMatchObject({ audience: "admin", authenticated: true, notice_acknowledged: false });
|
||||
|
||||
const ordinaryUserId = randomUUID();
|
||||
registration.database.prepare(`
|
||||
INSERT INTO users (
|
||||
user_id, normalized_email, role, status, counts_toward_stage_limit,
|
||||
registration_id, created_at
|
||||
) VALUES (?, 'api-user@example.invalid', 'user', 'active', 1, ?, ?)
|
||||
`).run(ordinaryUserId, randomUUID(), now);
|
||||
const ordinary = registration.issueAuthenticatedSession(ordinaryUserId, "user");
|
||||
const userAtAdmin = await app.inject({
|
||||
headers: { ...headers, cookie: `dada_admin_session=${ordinary.sessionToken}` },
|
||||
method: "GET",
|
||||
url: "/api/v1/admin-auth/session",
|
||||
});
|
||||
expect(userAtAdmin.statusCode).toBe(401);
|
||||
const adminAtUser = await app.inject({ headers: { ...headers, cookie: adminCookie }, method: "GET", url: "/api/v1/auth/session" });
|
||||
expect(adminAtUser.statusCode).toBe(401);
|
||||
await app.close();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,74 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
import { createServer, type ViteDevServer } from "vite";
|
||||
import { resolve } from "node:path";
|
||||
|
||||
let vite: ViteDevServer;
|
||||
let webUrl: string;
|
||||
|
||||
test.beforeAll(async () => {
|
||||
vite = await createServer({
|
||||
configFile: resolve("apps/web/vite.config.ts"),
|
||||
root: resolve("apps/web"),
|
||||
server: { host: "127.0.0.1", port: 0 },
|
||||
});
|
||||
await vite.listen();
|
||||
const address = vite.httpServer?.address();
|
||||
if (!address || typeof address === "string") throw new Error("Vite did not expose a test port.");
|
||||
webUrl = `http://127.0.0.1:${address.port}`;
|
||||
});
|
||||
|
||||
test.afterAll(async () => vite.close());
|
||||
|
||||
test("TDD-WP1-ADM-001 renders the isolated admin login and hands success to /admin", async ({ page }) => {
|
||||
await page.route("**/api/v1/admin-auth/login/send", (route) => route.fulfill({
|
||||
contentType: "application/json",
|
||||
status: 200,
|
||||
body: JSON.stringify({
|
||||
challenge_expires_at: "2026-07-28T12:10:00.000Z",
|
||||
registration_id: "00000000-0000-4000-8000-000000000007",
|
||||
resend_available_at: "2026-07-28T12:01:00.000Z",
|
||||
status: "verification_sent",
|
||||
}),
|
||||
}));
|
||||
await page.route("**/api/v1/admin-auth/login/complete", (route) => route.fulfill({
|
||||
contentType: "application/json",
|
||||
status: 200,
|
||||
body: JSON.stringify({ audience: "admin", status: "authenticated" }),
|
||||
}));
|
||||
|
||||
await page.goto(`${webUrl}/admin/login`);
|
||||
await expect(page.getByRole("heading", { name: "管理员邮箱验证码登录" })).toBeVisible();
|
||||
await expect(page.locator(".auth-art")).toHaveCount(0);
|
||||
await expect(page.getByRole("link", { name: "返回普通用户登录" })).toHaveAttribute("href", "/");
|
||||
await page.getByRole("textbox", { name: "管理员邮箱" }).fill("admin-ui@example.invalid");
|
||||
const sendButton = page.getByRole("button", { name: "获取验证码" });
|
||||
const widthBefore = (await sendButton.boundingBox())?.width;
|
||||
await sendButton.click();
|
||||
const codeInput = page.getByRole("textbox", { name: "验证码" });
|
||||
await expect(codeInput).toBeVisible();
|
||||
expect((await sendButton.boundingBox())?.width).toBe(widthBefore);
|
||||
await codeInput.fill("418205");
|
||||
await page.getByRole("button", { name: "登录后台" }).click();
|
||||
await expect(page).toHaveURL(`${webUrl}/admin`);
|
||||
});
|
||||
|
||||
test("TDD-WP1-ADM-001 shows a generic allowlist rejection without exposing identity state", async ({ page }) => {
|
||||
await page.route("**/api/v1/admin-auth/login/send", (route) => route.fulfill({
|
||||
contentType: "application/json",
|
||||
status: 409,
|
||||
body: JSON.stringify({
|
||||
error: {
|
||||
code: "AUTH_ENTRY_REJECTED",
|
||||
correlation_id: "00000000-0000-4000-8000-000000000008",
|
||||
details: { field_errors: [{ field: "email", message_key: "admin.auth.not_allowed" }] },
|
||||
message_key: "auth.entry_rejected",
|
||||
},
|
||||
}),
|
||||
}));
|
||||
await page.goto(`${webUrl}/admin/login`);
|
||||
await page.getByRole("textbox", { name: "管理员邮箱" }).fill("not-admin@example.invalid");
|
||||
await page.getByRole("button", { name: "获取验证码" }).click();
|
||||
await expect(page.getByRole("alert")).toContainText("无法使用管理员入口");
|
||||
await expect(page.getByRole("alert")).not.toContainText("普通用户");
|
||||
await expect(page.getByRole("alert")).not.toContainText("白名单");
|
||||
});
|
||||
@@ -35,6 +35,6 @@ test("TDD-WP1-AUTH-003 keeps registration and login as separate keyboard entries
|
||||
await page.keyboard.press("ArrowLeft");
|
||||
await expect(loginTab).toBeFocused();
|
||||
await expect(page.getByRole("textbox", { name: "邀请码" })).toHaveCount(0);
|
||||
await expect(page.getByRole("link", { name: "管理员登录" })).toHaveAttribute("href", "/admin");
|
||||
await expect(page.getByRole("link", { name: "管理员登录" })).toHaveAttribute("href", "/admin/login");
|
||||
await expect(page.getByText("测试数据仅保存在本机,不自动备份,也不会迁移到正式系统。")).toBeVisible();
|
||||
});
|
||||
|
||||
@@ -42,7 +42,7 @@ test("TDD-WP1-AUTH-003 renders Z0pf8 login states without silently switching ent
|
||||
await page.goto(webUrl);
|
||||
await expect(page.getByRole("heading", { name: "邮箱验证码登录" })).toBeVisible();
|
||||
await expect(page.getByText("测试数据仅保存在本机,不自动备份,也不会迁移到正式系统。")).toBeVisible();
|
||||
await expect(page.getByRole("link", { name: "管理员登录" })).toHaveAttribute("href", "/admin");
|
||||
await expect(page.getByRole("link", { name: "管理员登录" })).toHaveAttribute("href", "/admin/login");
|
||||
await expect(page.getByLabel("邀请码")).toHaveCount(0);
|
||||
await expect(page.getByLabel("创作署名")).toHaveCount(0);
|
||||
await expect(page.getByLabel("社交 ID")).toHaveCount(0);
|
||||
|
||||
@@ -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",
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,161 @@
|
||||
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 { RegistrationService } from "../../apps/api/src/registration.js";
|
||||
import { MockResendAdapter } from "../../apps/api/src/resend-adapter.js";
|
||||
import { readSecureConfigCandidate } from "../../apps/api/src/secure-config.js";
|
||||
|
||||
const roots: string[] = [];
|
||||
const services: RegistrationService[] = [];
|
||||
const now = Date.parse("2026-07-28T11:00:00.000Z");
|
||||
const adminPepper = Buffer.alloc(32, 0xa1);
|
||||
|
||||
function hash(email: string) {
|
||||
return createHmac("sha256", adminPepper).update(email.trim().toLowerCase(), "utf8").digest("hex").toUpperCase();
|
||||
}
|
||||
|
||||
function createService(withPepper = true) {
|
||||
const root = mkdtempSync(join(tmpdir(), "dada-wp1-04-config-"));
|
||||
roots.push(root);
|
||||
const service = new RegistrationService({
|
||||
...(withPepper ? { adminAllowlistPepper: adminPepper } : {}),
|
||||
challengePepper: Buffer.alloc(32, 0xa2),
|
||||
clock: () => now,
|
||||
codeGenerator: () => "418205",
|
||||
currentPrivacyNoticeVersion: "p0a-registration-notice-v1",
|
||||
databasePath: join(root, "dada.sqlite3"),
|
||||
invitePepper: Buffer.alloc(32, 0xa3),
|
||||
resend: new MockResendAdapter(),
|
||||
sessionPepper: Buffer.alloc(32, 0xa4),
|
||||
});
|
||||
services.push(service);
|
||||
return service;
|
||||
}
|
||||
|
||||
function writeEvidence(file: string, value: unknown) {
|
||||
const directory = process.env.DADA_EVIDENCE_DIR_CONFIG;
|
||||
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-CFG-001-secure-revision", () => {
|
||||
it("applies one complete revision atomically and rolls invalid candidates back without sensitive persistence", async () => {
|
||||
const service = createService();
|
||||
const firstEmail = "first-admin@example.invalid";
|
||||
const replacementEmail = "replacement-admin@example.invalid";
|
||||
const firstHash = hash(firstEmail);
|
||||
const replacementHash = hash(replacementEmail);
|
||||
const configPath = join(roots[0]!, "instance.json");
|
||||
writeFileSync(configPath, `${JSON.stringify({
|
||||
admin_allowlist_hashes: [firstHash],
|
||||
admin_recovery_hashes: [],
|
||||
schema_version: 1,
|
||||
secure_config_revision: 1,
|
||||
})}\n`);
|
||||
expect(readSecureConfigCandidate(configPath)).toEqual({
|
||||
adminAllowlistHashes: [firstHash],
|
||||
adminRecoveryHashes: [],
|
||||
secureConfigRevision: 1,
|
||||
});
|
||||
|
||||
expect(service.applySecureConfig({
|
||||
adminAllowlistHashes: [firstHash],
|
||||
adminRecoveryHashes: [],
|
||||
secureConfigRevision: 1,
|
||||
})).toMatchObject({ appliedRevision: 1, status: "applied" });
|
||||
const resend = service.options.resend as MockResendAdapter;
|
||||
const sent = await service.sendAdminLoginCode({ clientKey: "first-admin-client", email: firstEmail });
|
||||
const loggedIn = service.completeAdminLogin({
|
||||
clientKey: "first-admin-client",
|
||||
code: resend.readLatestCode(firstEmail),
|
||||
idempotencyKey: "wp1-04-config-first-login-00000000001",
|
||||
registrationId: sent.registrationId,
|
||||
});
|
||||
|
||||
expect(service.applySecureConfig({
|
||||
adminAllowlistHashes: [replacementHash],
|
||||
adminRecoveryHashes: [],
|
||||
secureConfigRevision: 2,
|
||||
})).toMatchObject({ appliedRevision: 2, status: "applied" });
|
||||
expect(service.readAdminSession(loggedIn.sessionToken)).toBeUndefined();
|
||||
|
||||
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-conflict@example.invalid', 'user', 'active', 1, ?, ?)
|
||||
`).run(ordinaryUserId, randomUUID(), now);
|
||||
const conflictHash = hash("ordinary-conflict@example.invalid");
|
||||
expect(() => service.applySecureConfig({
|
||||
adminAllowlistHashes: [replacementHash, conflictHash],
|
||||
adminRecoveryHashes: [],
|
||||
secureConfigRevision: 3,
|
||||
})).toThrow(/identity_conflict/);
|
||||
expect(() => service.applySecureConfig({
|
||||
adminAllowlistHashes: ["not-a-valid-hmac"],
|
||||
adminRecoveryHashes: [],
|
||||
secureConfigRevision: 3,
|
||||
})).toThrow(/hmac_invalid/);
|
||||
|
||||
const withoutPepper = createService(false);
|
||||
expect(() => withoutPepper.applySecureConfig({
|
||||
adminAllowlistHashes: [replacementHash],
|
||||
adminRecoveryHashes: [],
|
||||
secureConfigRevision: 1,
|
||||
})).toThrow(/admin_pepper_not_configured/);
|
||||
|
||||
const state = service.database.prepare("SELECT * FROM secure_config_apply_state WHERE singleton = 1").get() as any;
|
||||
expect(state).toMatchObject({ allowlist_count: 1, applied_revision: 2 });
|
||||
expect(Object.keys(state)).not.toContain("admin_allowlist_hashes");
|
||||
const audits = service.database.prepare(`
|
||||
SELECT actor_type, actor_ref, operation_type, result, before_summary, after_summary
|
||||
FROM admin_operation_logs ORDER BY occurred_at
|
||||
`).all();
|
||||
expect(audits).toEqual(expect.arrayContaining([
|
||||
expect.objectContaining({ actor_ref: "backend_secure_config", actor_type: "system", result: "succeeded" }),
|
||||
expect.objectContaining({ actor_ref: "backend_secure_config", actor_type: "system", result: "failed" }),
|
||||
]));
|
||||
const retention = service.database.prepare(`
|
||||
SELECT occurred_at, expires_at FROM admin_operation_logs ORDER BY occurred_at LIMIT 1
|
||||
`).get() as { expires_at: number; occurred_at: number };
|
||||
expect(retention.expires_at - retention.occurred_at).toBe(180 * 24 * 60 * 60 * 1_000);
|
||||
expect(() => service.database.prepare(`
|
||||
UPDATE admin_operation_logs SET result = 'failed' WHERE log_id = (SELECT log_id FROM admin_operation_logs LIMIT 1)
|
||||
`).run()).toThrow(/admin_operation_logs_immutable/);
|
||||
expect(() => service.database.prepare(`
|
||||
DELETE FROM admin_operation_logs WHERE log_id = (SELECT log_id FROM admin_operation_logs LIMIT 1)
|
||||
`).run()).toThrow(/admin_operation_logs_immutable/);
|
||||
const redactionProbe = JSON.stringify({ audits, state });
|
||||
for (const forbidden of [firstEmail, replacementEmail, firstHash, replacementHash, adminPepper.toString("hex")]) {
|
||||
expect(redactionProbe).not.toContain(forbidden);
|
||||
}
|
||||
|
||||
writeEvidence("config-result.json", {
|
||||
applied_revision: state.applied_revision,
|
||||
failed_candidates: ["identity_conflict", "hmac_invalid", "admin_pepper_not_configured"],
|
||||
status: "passed",
|
||||
});
|
||||
writeEvidence("db-diff.json", {
|
||||
after: { allowlist_count: state.allowlist_count, applied_revision: state.applied_revision },
|
||||
removed_admin_sessions_revoked: true,
|
||||
rejected_revision_advanced: false,
|
||||
status: "passed",
|
||||
});
|
||||
writeEvidence("redaction.json", {
|
||||
forbidden_values_absent: true,
|
||||
stored_secure_state_fields: Object.keys(state).sort(),
|
||||
status: "passed",
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user