fix: 修正 user-billing 自服务代理鉴权边界 (#1187)

This commit is contained in:
Lyon
2026-06-14 10:17:56 +08:00
committed by GitHub
parent b9dd25ef68
commit ee78ed9fdc
2 changed files with 27 additions and 6 deletions
+6 -6
View File
@@ -1262,14 +1262,14 @@ async function maybeHandleUserBillingApiKeysHttp(request, response, url, options
async function handleUserBillingProfileHttp(request, response, options) {
const token = bearerTokenFromRequest(request);
const client = options.userBillingClient ?? options.accessController?.userBilling ?? null;
if (!token || !client?.configured || typeof client.updateProfile !== "function") {
return sendRestError(request, response, 503, "user_billing_not_configured", "HWLAB user-billing profile API is not configured", { operation: "PATCH /v1/users/me/profile", target: { type: "service", id: "hwlab-user-billing" } });
}
const auth = await options.accessController.authenticate(request, { required: true });
if (!auth.ok) return sendJson(response, auth.status, auth);
if (!String(auth.authMethod ?? "").startsWith("user-billing")) {
return sendRestError(request, response, 403, "user_billing_auth_required", "profile update requires a user-billing bearer token", { operation: "PATCH /v1/users/me/profile", target: { type: "route", id: "/v1/users/me/profile" } });
}
if (!token || !client?.configured || typeof client.updateProfile !== "function") {
return sendRestError(request, response, 503, "user_billing_not_configured", "HWLAB user-billing profile API is not configured", { operation: "PATCH /v1/users/me/profile", target: { type: "service", id: "hwlab-user-billing" } });
}
const parsed = await readJsonObject(request, DEFAULT_BODY_LIMIT_BYTES);
if (!parsed.ok) return sendJson(response, 400, parsed.error);
const result = await client.updateProfile(token, parsed.value);
@@ -1279,14 +1279,14 @@ async function handleUserBillingProfileHttp(request, response, options) {
async function handleUserBillingPasswordHttp(request, response, options) {
const token = bearerTokenFromRequest(request);
const client = options.userBillingClient ?? options.accessController?.userBilling ?? null;
if (!token || !client?.configured || typeof client.changePassword !== "function") {
return sendRestError(request, response, 503, "user_billing_not_configured", "HWLAB user-billing password API is not configured", { operation: "POST /v1/users/me/password", target: { type: "service", id: "hwlab-user-billing" } });
}
const auth = await options.accessController.authenticate(request, { required: true });
if (!auth.ok) return sendJson(response, auth.status, auth);
if (auth.authMethod !== "user-billing-session") {
return sendRestError(request, response, 403, "user_billing_session_required", "password update requires a user-billing session token", { operation: "POST /v1/users/me/password", target: { type: "route", id: "/v1/users/me/password" } });
}
if (!token || !client?.configured || typeof client.changePassword !== "function") {
return sendRestError(request, response, 503, "user_billing_not_configured", "HWLAB user-billing password API is not configured", { operation: "POST /v1/users/me/password", target: { type: "service", id: "hwlab-user-billing" } });
}
const parsed = await readJsonObject(request, DEFAULT_BODY_LIMIT_BYTES);
if (!parsed.ok) return sendJson(response, 400, parsed.error);
const result = await client.changePassword(token, parsed.value);
@@ -517,3 +517,24 @@ test("cloud api proxies R1 user-billing API key and profile self-service routes"
await new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve())));
}
});
test("cloud api reports auth_required before user-billing profile/password proxy config errors", async () => {
const userBillingClient = {
configured: true,
async updateProfile() { throw new Error("profile proxy should not run without auth"); },
async changePassword() { throw new Error("password proxy should not run without auth"); }
};
const server = createCloudApiServer({ env: { HWLAB_ACCESS_CONTROL_REQUIRED: "1" }, userBillingClient });
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
try {
const { port } = server.address();
const profile = await fetch(`http://127.0.0.1:${port}/v1/users/me/profile`, { method: "PATCH", headers: { "content-type": "application/json" }, body: JSON.stringify({ displayName: "Missing Auth" }) });
assert.equal(profile.status, 401);
assert.equal((await profile.json()).error.code, "auth_required");
const password = await fetch(`http://127.0.0.1:${port}/v1/users/me/password`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ currentPassword: "old-password", newPassword: "new-password" }) });
assert.equal(password.status, 401);
assert.equal((await password.json()).error.code, "auth_required");
} finally {
await new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve())));
}
});