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
@@ -123,6 +123,15 @@ type API struct {
|
||||
// on the wake lever). Zero disables throttling.
|
||||
WakeCooldown time.Duration
|
||||
|
||||
// SubmitCreateCooldown / SubmitUploadCooldown throttle the user-modpack
|
||||
// submission lane per user: create bounds how quickly review-queue rows can
|
||||
// appear, upload bounds how often a user may stream a (up to 1 GiB) build
|
||||
// context. The keys are separate, so the lane's normal shape — create, then
|
||||
// upload — is never blocked by its own throttle. Zero disables each lever
|
||||
// (the same idiom as WakeCooldown); cmd/felis wires positive values.
|
||||
SubmitCreateCooldown time.Duration
|
||||
SubmitUploadCooldown time.Duration
|
||||
|
||||
// MaxRunningServers caps how many servers may be desired-Running cluster-wide
|
||||
// (spec §9.1: the concurrency-上限 lever hanging on the same wake chokepoint as
|
||||
// cooldown and autostartPolicy). Zero — the default — disables it: §9.2 wires
|
||||
@@ -158,6 +167,9 @@ type API struct {
|
||||
otpCooldownOnce sync.Once
|
||||
otpCooldown *cooldownLimiter
|
||||
|
||||
submitCooldownOnce sync.Once
|
||||
submitCooldown *cooldownLimiter
|
||||
|
||||
streamCapOnce sync.Once
|
||||
streamCap *streamLimiter
|
||||
}
|
||||
@@ -204,6 +216,20 @@ func (a *API) otpLimiter() *cooldownLimiter {
|
||||
return a.otpCooldown
|
||||
}
|
||||
|
||||
// submitLimiter lazily builds a SEPARATE cooldown limiter for the user-modpack
|
||||
// submission lane, so its throttles never share state with the wake or OTP
|
||||
// keyspaces. One limiter backs both levers with prefixed keys (see the
|
||||
// submissionCreateKey/UploadKey constants), so create and upload never contend
|
||||
// with each other. Like the other cooldowns it is process-local; with multiple
|
||||
// api replicas the effective spacing is per-replica, the same accepted
|
||||
// KNOWN-LIMITATION the OTP resend throttle carries.
|
||||
func (a *API) submitLimiter() *cooldownLimiter {
|
||||
a.submitCooldownOnce.Do(func() {
|
||||
a.submitCooldown = &cooldownLimiter{now: a.now, last: map[string]time.Time{}}
|
||||
})
|
||||
return a.submitCooldown
|
||||
}
|
||||
|
||||
// streamGate lazily builds the per-principal SSE stream cap bound to
|
||||
// MaxStreamsPerPrincipal. A zero cap yields a disabled limiter that admits every
|
||||
// stream, so a deployment (or test) that leaves it unset pays nothing.
|
||||
|
||||
Reference in new issue
Block a user