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
+21
-2
@@ -4266,6 +4266,15 @@ paths:
|
||||
$ref: '#/components/responses/BadRequest'
|
||||
'401':
|
||||
$ref: '#/components/responses/Unauthorized'
|
||||
'403':
|
||||
description: >-
|
||||
The per-user submission allowance is spent — too many of the
|
||||
caller's submissions are awaiting review, or their stored-upload
|
||||
budget is full (submission_quota_exceeded).
|
||||
'429':
|
||||
description: >-
|
||||
A submission was created within the per-user cooldown window
|
||||
(submission_cooldown).
|
||||
'503':
|
||||
$ref: '#/components/responses/ServiceUnavailable'
|
||||
get:
|
||||
@@ -4307,8 +4316,10 @@ paths:
|
||||
principal; a submission the caller does not own is reported as 404, so
|
||||
this endpoint cannot upload to or probe another user's submission. Only a
|
||||
pending_review submission accepts a context (409 otherwise); a wrong-format
|
||||
or oversize body is rejected with 400. Returns 503 when the deployment's
|
||||
context store has no implemented upload transport.
|
||||
or oversize body is rejected with 400, and an upload that would push the
|
||||
caller past their per-user stored-context budget is refused with 403
|
||||
before the excess is persisted. Returns 503 when the deployment's context
|
||||
store has no implemented upload transport.
|
||||
x-felis-face: [external]
|
||||
x-felis-tier: app
|
||||
security: [{ accessJWT: [] }]
|
||||
@@ -4329,10 +4340,18 @@ paths:
|
||||
$ref: '#/components/responses/BadRequest'
|
||||
'401':
|
||||
$ref: '#/components/responses/Unauthorized'
|
||||
'403':
|
||||
description: >-
|
||||
The upload would exceed the caller's per-user stored-context budget
|
||||
(submission_quota_exceeded).
|
||||
'404':
|
||||
$ref: '#/components/responses/NotFound'
|
||||
'409':
|
||||
$ref: '#/components/responses/Conflict'
|
||||
'429':
|
||||
description: >-
|
||||
An upload was accepted within the per-user cooldown window
|
||||
(submission_cooldown).
|
||||
'503':
|
||||
$ref: '#/components/responses/ServiceUnavailable'
|
||||
|
||||
|
||||
Reference in new issue
Block a user