From 939870346d1800ed5182ebfb8fef3ad5529b1586 Mon Sep 17 00:00:00 2001 From: panxiao81 Date: Fri, 25 Sep 2026 21:08:25 +0000 Subject: [PATCH] =?UTF-8?q?=E8=A1=A5=E5=85=85=20AD=20HTTPS=20=E8=A1=A8?= =?UTF-8?q?=E5=8D=95=E7=9A=84=E6=B5=8F=E8=A7=88=E5=99=A8=E5=9B=9E=E5=BD=92?= =?UTF-8?q?=E9=AA=8C=E8=AF=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/ad-login.md | 3 ++- frontend/tests/ad-login.spec.ts | 31 +++++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) create mode 100644 frontend/tests/ad-login.spec.ts diff --git a/docs/ad-login.md b/docs/ad-login.md index e197ad8..e8ae5c2 100644 --- a/docs/ad-login.md +++ b/docs/ad-login.md @@ -96,7 +96,8 @@ LDAP endpoint identification 或用信任所有证书的 socket factory。连接 2026-09-25:JVM 测试共 12 项通过(原有 6 项、新增 AD 6 项),`bootJar` 构建通过。 实际 JVM 使用受信 CA 完成 Samba AD RootDSE 查询,HTTPS 页面返回 200。 浏览器已检查登录表单渲染、真实 CSRF 原生 POST 和失败后清空密码;只使用在访问 AD 前 -即拒绝的合成外域用户名,不尝试猜测人类密码。 +即拒绝的合成外域用户名,不尝试猜测人类密码。浏览器回归共 1 项通过,包含移动端布局。 +复现:`IAM_AD_URL=https://验收域名:端口 npm --prefix frontend run test:browser -- ad-login.spec.ts`。 实际人类密码与组结果仍需维护者 在 HTTPS 页面输入凭据验收,不在聊天、命令行或日志中传递人类密码。 新增 AD 路径尚未进行 Native 测试,不能复用旧 UI 原型的 Native 结论。 diff --git a/frontend/tests/ad-login.spec.ts b/frontend/tests/ad-login.spec.ts new file mode 100644 index 0000000..8946864 --- /dev/null +++ b/frontend/tests/ad-login.spec.ts @@ -0,0 +1,31 @@ +import { test, expect } from "@playwright/test"; + +// Explicit opt-in: never submit synthetic credentials to an arbitrary configured directory. +// The external-domain username below must be rejected before any LDAP bind. +test("AD HTTPS page uses native POST and keeps failed credentials out of the response", async ({ page, context }) => { + test.skip(!process.env.IAM_AD_URL, "Set IAM_AD_URL to the HTTPS first-factor PoC"); + const url = new URL("/signin", process.env.IAM_AD_URL!); + expect(url.protocol).toBe("https:"); + const requests: { method: string; type: string; path: string }[] = []; + page.on("request", request => requests.push({ + method: request.method(), type: request.resourceType(), path: new URL(request.url()).pathname, + })); + await page.goto(url.toString()); + await expect(page.getByRole("heading", { name: "登录你的账号" })).toBeVisible(); + await expect(page.locator("form")).toHaveAttribute("method", "post"); + await expect(page.locator("input[name=_csrf]")).toHaveCount(1); + const session = (await context.cookies()).find(cookie => cookie.name === "JSESSIONID"); + expect(session).toMatchObject({ secure: true, httpOnly: true, sameSite: "Lax" }); + await page.getByLabel("用户名", { exact: true }).fill("ui-check@invalid.example"); + await page.getByLabel("密码", { exact: true }).fill("synthetic-ui-check"); + await page.getByRole("button", { name: "继续", exact: true }).click(); + await expect(page.getByRole("alert")).toContainText("无法验证账号"); + await expect(page.getByLabel("密码", { exact: true })).toHaveValue(""); + expect(await page.content()).not.toContain("synthetic-ui-check"); + expect(requests.filter(request => request.method === "POST")).toEqual([ + { method: "POST", type: "document", path: "/signin/password" }, + ]); + expect(requests.filter(request => ["fetch", "xhr"].includes(request.type))).toHaveLength(0); + await page.setViewportSize({ width: 390, height: 844 }); + expect(await page.evaluate(() => document.documentElement.scrollWidth > innerWidth)).toBe(false); +});