feat(oauth): 账号名改从 zone.account 取,去掉 account-settings.read 依赖
实测确认 GET /zones 每个 zone 自带 account {id,name},该接口只需 zone.read,
因此账号 id 与账号名都不必再调 GET /accounts(account-settings.read)。
顺带修掉 account_id 旧字段在当前响应里已为 null 导致的降级路径取不到账号 id。
- RealCFApi.discover 只列 /zones,移除 listAccounts 与 accountSource
- scope 候选集去掉 account-settings.read;设置页移除「含账号名权限」按钮
- 设置页 Scope 说明与 docs/02、README 同步(补 User Details Read 的区别说明)
- 85/85 测试通过(新增 zone 带 account / 不带 account 两种断言)
This commit is contained in:
+37
-14
@@ -74,10 +74,9 @@ describe("Cloudflare OAuth(真实授权,Connect Cloudflare Account)", () => {
|
||||
expect(credRow?.secret_enc).toBeTruthy();
|
||||
expect(credRow!.secret_last4.length).toBe(4);
|
||||
|
||||
// 真实发出的 Cloudflare 调用:换 token -> 账户发现 -> 域名发现 -> 上传 Email Worker 脚本
|
||||
// 真实发出的 Cloudflare 调用:换 token -> 域名发现(账号名随 zone 返回)-> 上传 Email Worker 脚本
|
||||
expect(cf.hit("POST", "/oauth2/token")).toBe(true);
|
||||
expect(cf.hit("GET", "/accounts")).toBe(true);
|
||||
expect(cf.calls.some((c) => c.path.startsWith("/zones?") && c.path.includes("account.id="))).toBe(true);
|
||||
expect(cf.hit("GET", "/zones?")).toBe(true);
|
||||
expect(cf.hit("PUT", "/workers/scripts/")).toBe(true);
|
||||
// token 兑换请求带真实 code 与 client 凭据
|
||||
const tokenBody = cf.bodyOf("POST", "/oauth2/token") ?? "";
|
||||
@@ -121,16 +120,16 @@ describe("Cloudflare OAuth(真实授权,Connect Cloudflare Account)", () => {
|
||||
expect(conn!.encrypted_access_token).not.toContain("cf-oauth-access-token-xyz");
|
||||
});
|
||||
|
||||
test("缺少账号级读权限时:从 zone 的 account_id 反推账户,接入照样完成", async () => {
|
||||
test("账号名从 zone 自带的 account 取:不需要任何账号级读权限", async () => {
|
||||
const t = await makeTestEnv();
|
||||
const cookie = await loginCookie(t);
|
||||
// /accounts 返回 403(没有 account 读取权限),zones 仍可用且带 account_id
|
||||
// 真实 Cloudflare 的 GET /zones 每个 zone 都自带 account {id, name},只要 zone.read
|
||||
const account = { id: "cf-acct-from-zone", name: "Phptits Account" };
|
||||
const cf = installCloudflareMock({
|
||||
zones: [
|
||||
{ id: "zone-a", name: "alpha.com", account_id: "cf-acct-from-zone", status: "active" },
|
||||
{ id: "zone-b", name: "beta.com", account_id: "cf-acct-from-zone", status: "active" },
|
||||
{ id: "zone-a", name: "alpha.com", account_id: "", status: "active", account },
|
||||
{ id: "zone-b", name: "beta.com", account_id: "", status: "active", account },
|
||||
],
|
||||
failAccounts: true,
|
||||
});
|
||||
mock = cf;
|
||||
const { state } = (await (
|
||||
@@ -152,11 +151,36 @@ describe("Cloudflare OAuth(真实授权,Connect Cloudflare Account)", () => {
|
||||
};
|
||||
expect(detail.account.status).toBe("active");
|
||||
expect(detail.account.cloudflare_account_id).toBe("cf-acct-from-zone");
|
||||
// 展示名用第一个域名 + 域名数量
|
||||
expect(detail.account.name).toBe("alpha.com 等 2 个域名");
|
||||
// 展示名就是真实账号名
|
||||
expect(detail.account.name).toBe("Phptits Account");
|
||||
expect(detail.domains.map((d) => d.name)).toEqual(expect.arrayContaining(["alpha.com", "beta.com"]));
|
||||
});
|
||||
|
||||
test("zone 未附带 account 时:回退用 account_id + 第一个域名展示", async () => {
|
||||
const t = await makeTestEnv();
|
||||
const cookie = await loginCookie(t);
|
||||
const cf = installCloudflareMock({
|
||||
zones: [
|
||||
{ id: "zone-a", name: "alpha.com", account_id: "cf-acct-legacy", status: "active" },
|
||||
{ id: "zone-b", name: "beta.com", account_id: "cf-acct-legacy", status: "active" },
|
||||
],
|
||||
});
|
||||
mock = cf;
|
||||
const { state } = (await (
|
||||
await authedFetch(t, cookie, "http://localhost/api/oauth/cloudflare/start")
|
||||
).json()) as { state: string };
|
||||
const resp = await t.app.fetch(
|
||||
new Request(`http://localhost/api/oauth/cloudflare/callback?code=c1&state=${state}`),
|
||||
t.env,
|
||||
);
|
||||
const accountId = new URL("http://x" + resp.headers.get("Location")).searchParams.get("provision")!;
|
||||
const detail = (await (
|
||||
await authedFetch(t, cookie, `http://localhost/api/accounts/${accountId}`)
|
||||
).json()) as { account: { cloudflare_account_id: string | null; name: string } };
|
||||
expect(detail.account.cloudflare_account_id).toBe("cf-acct-legacy");
|
||||
expect(detail.account.name).toBe("alpha.com 等 2 个域名");
|
||||
});
|
||||
|
||||
test("state 一次性:重放授权回调被拒(OAuth CSRF 防护)", async () => {
|
||||
const t = await makeTestEnv();
|
||||
const cookie = await loginCookie(t);
|
||||
@@ -376,14 +400,14 @@ describe("Cloudflare OAuth(真实授权,Connect Cloudflare Account)", () => {
|
||||
).json()) as PreflightBody;
|
||||
expect(res.ok).toBe(true);
|
||||
|
||||
// 主动请求可选的 account-settings.read 而 App 没勾 => 逐轮剔除被点名的,收敛出 allowed/denied
|
||||
// 申请了 App 没勾的 scope => 逐轮剔除被点名的,收敛出 allowed/denied
|
||||
await authedFetch(t, cookie, "http://localhost/api/settings", {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
cf_oauth: {
|
||||
client_id: "preflight-client",
|
||||
scope: "account-settings.read zone.read workers-scripts.write email-routing-rule.write",
|
||||
scope: "zone.read email-routing-address.write",
|
||||
},
|
||||
}),
|
||||
});
|
||||
@@ -393,10 +417,9 @@ describe("Cloudflare OAuth(真实授权,Connect Cloudflare Account)", () => {
|
||||
expect(res.ok).toBe(false);
|
||||
expect(res.error).toBe("invalid_scope");
|
||||
expect(res.scopes.probed).toBe(true);
|
||||
// 候选集里 App 没勾的(所需的 account-settings.read + 4 个备选)都会被点名剔除
|
||||
// 候选集里 App 没勾的 4 个备选都会被点名剔除
|
||||
expect(res.scopes.denied).toEqual(
|
||||
expect.arrayContaining([
|
||||
"account-settings.read",
|
||||
"email-routing-account-rule.read",
|
||||
"email-routing-address.write",
|
||||
"email-routing-suppression.write",
|
||||
|
||||
Reference in New Issue
Block a user