fix(submit): bound the untrusted upload lane — per-user caps + throttles (#75)
A logged-in user could file submissions without bound and stream a 1 GiB context per submission. The only limits were the single-blob size cap and the 5 GiB uploads PVC (platform/workloads.go); nothing counted a user's rows or bytes, so one account could fill the volume and every other user's upload would start failing. - Create: per-user pending_review cap (default 5) — the review queue cannot be parked full of one account's rows. Check-then-insert, documented soft. - UploadContext: per-user stored-context budget (default 2 GiB) charged against the blob store's REAL sizes (new Blobs.Size on local/S3 stores), so the sum cannot drift from the volume; the write is capped at the remaining budget, so the excess is refused before it is persisted, and a re-upload is charged only for its new bytes. - API: per-user create/upload throttles (30s/15s, cmd/felis-wired) on a dedicated cooldown keyspace, reserve→release so a failed attempt never burns the window and a burst collapses to one winner; ErrQuotaExceeded → 403 submission_quota_exceeded (distinct from the 400 an oversize blob gets), 429 submission_cooldown for the throttles. - Panel: zh/en copy for both codes; openapi documents 403/429 on the two user routes; pgint covers the pending-queue count. Unit tests: submit package (cap, budget boundary/exact-fit/replacement, oversize-vs-quota split) and api handlers (quota 403 both paths, throttle 429 + recovery + failure-release). go vet/go test/gofmt clean; panel vitest 118 + typecheck green.
This commit is contained in:
15 files changed
+577
-15
No files matched your search
@@ -42,6 +42,8 @@
|
||||
"build_unavailable": "Image builds aren't available right now.",
|
||||
"build_logs_unavailable": "Build logs aren't available right now.",
|
||||
"already_reviewed": "This submission has already been reviewed.",
|
||||
"submission_quota_exceeded": "Your submission quota is full: too many pending reviews, or your stored uploads are at the limit.",
|
||||
"submission_cooldown": "Too many submission requests — try again shortly.",
|
||||
"submissions_unavailable": "Submissions aren't available right now.",
|
||||
"uploads_unavailable": "Uploads aren't available right now.",
|
||||
"backup_unavailable": "Backups aren't available right now.",
|
||||
|
||||
@@ -42,6 +42,8 @@
|
||||
"build_unavailable": "构建功能当前不可用。",
|
||||
"build_logs_unavailable": "构建日志暂时不可用。",
|
||||
"already_reviewed": "该提交已经审核过了。",
|
||||
"submission_quota_exceeded": "你的提交配额已满:待审核提交过多,或已存上传总量达到上限。",
|
||||
"submission_cooldown": "操作太频繁——请稍后再试。",
|
||||
"submissions_unavailable": "提交流程当前不可用。",
|
||||
"uploads_unavailable": "上传功能当前不可用。",
|
||||
"backup_unavailable": "备份功能当前不可用。",
|
||||
|
||||
@@ -522,6 +522,14 @@ describe("image whitelist and builds wire shapes", () => {
|
||||
expect((opts as RequestInit).body).toBe(blob);
|
||||
expect((opts as RequestInit).headers).toEqual({ "Content-Type": "application/x-gzip" });
|
||||
});
|
||||
|
||||
// The lane's two throttled outcomes (a spent allowance, a closed cooldown)
|
||||
// must surface as their own copy, not the generic forbidden/error text.
|
||||
it("maps the submission quota/cooldown codes to stable human copy", async () => {
|
||||
const { humanizeError } = await import("./api");
|
||||
expect(humanizeError({ code: "submission_quota_exceeded" })).toMatch(/quota/i);
|
||||
expect(humanizeError({ code: "submission_cooldown" })).toMatch(/try again/i);
|
||||
});
|
||||
});
|
||||
|
||||
describe("updates maintenance window", () => {
|
||||
|
||||
@@ -722,6 +722,10 @@ export function humanizeError(e: unknown): string {
|
||||
return t("build_logs_unavailable");
|
||||
case "already_reviewed":
|
||||
return t("already_reviewed");
|
||||
case "submission_quota_exceeded":
|
||||
return t("submission_quota_exceeded");
|
||||
case "submission_cooldown":
|
||||
return t("submission_cooldown");
|
||||
case "submissions_unavailable":
|
||||
return t("submissions_unavailable");
|
||||
case "uploads_unavailable":
|
||||
|
||||
Reference in new issue
Block a user