tenseleyflow/shithub / ffd6659

Browse files

S35: migration 0039 — rate_limits + signup_ip_throttle

Authored by espadonne
SHA
ffd66593e0e892212160d9bd55afdb5e3b8fc94b
Parents
b9c83c2
Tree
665db21

1 changed file

StatusFile+-
A internal/migrationsfs/migrations/0039_rate_limits.sql 60 0
internal/migrationsfs/migrations/0039_rate_limits.sqladded
@@ -0,0 +1,60 @@
1
+-- SPDX-License-Identifier: AGPL-3.0-or-later
2
+--
3
+-- S35 — Generalized rate-limiting + per-/24 signup throttle.
4
+--
5
+--   * rate_limits          — counter table for any (scope, key) pair,
6
+--                            generalizing S05's auth_throttle. The
7
+--                            ratelimit package is the single writer;
8
+--                            auth_throttle stays in place for the
9
+--                            existing auth surface (kept for back-
10
+--                            compat — generalising S05 callers can
11
+--                            land in a follow-up if profiling shows
12
+--                            the dual table to be wasteful).
13
+--
14
+--   * signup_ip_throttle   — per-/24 signup counter. Distinct from
15
+--                            rate_limits because the key is a CIDR
16
+--                            block (not a string). Used to throw a
17
+--                            soft-block at 5 signups/hour and a hard
18
+--                            block at 20/24h, matching the spec's
19
+--                            anti-abuse heuristics. (Captcha gating
20
+--                            is the natural next step for the soft
21
+--                            block; vendor decision is deferred —
22
+--                            the gate stays here as a 429 today.)
23
+--
24
+-- Pruning: a periodic worker (sweep job, S34's worker pool) deletes
25
+-- rows whose window started more than 24h ago. The covering index
26
+-- on window_started_at keeps the prune cheap.
27
+
28
+-- +goose Up
29
+CREATE TABLE rate_limits (
30
+    scope             text        NOT NULL,
31
+    key               text        NOT NULL,
32
+    hits              integer     NOT NULL DEFAULT 0,
33
+    window_started_at timestamptz NOT NULL DEFAULT now(),
34
+
35
+    PRIMARY KEY (scope, key),
36
+    CONSTRAINT rate_limits_scope_length CHECK (char_length(scope) BETWEEN 1 AND 64),
37
+    CONSTRAINT rate_limits_key_length   CHECK (char_length(key)   BETWEEN 1 AND 256)
38
+);
39
+
40
+-- Periodic prune scans by window_started_at; partial index on the
41
+-- "old enough to delete" predicate isn't worth it because the cutoff
42
+-- moves continuously.
43
+CREATE INDEX rate_limits_window_started_idx ON rate_limits (window_started_at);
44
+
45
+CREATE TABLE signup_ip_throttle (
46
+    -- inet column accepts the CIDR (/24 for v4, /48 for v6) as a
47
+    -- subtype. Storing the network and the rolling counter together.
48
+    cidr              inet        NOT NULL,
49
+    hits              integer     NOT NULL DEFAULT 0,
50
+    window_started_at timestamptz NOT NULL DEFAULT now(),
51
+
52
+    PRIMARY KEY (cidr)
53
+);
54
+
55
+CREATE INDEX signup_ip_throttle_window_started_idx
56
+    ON signup_ip_throttle (window_started_at);
57
+
58
+-- +goose Down
59
+DROP TABLE IF EXISTS signup_ip_throttle;
60
+DROP TABLE IF EXISTS rate_limits;