rate limit: domain ceiling for all IPs, trusted app override

Invert level-2 semantics so domain limits apply to every client IP and
application limits with trusted IPs raise the ceiling above the domain
(still capped by level 1). Panel shows L1, validates maxima, and documents
the model on Settings.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-12 23:19:51 +03:00
parent b0ebe061b5
commit 00e36df553
22 changed files with 497 additions and 171 deletions
+20 -19
View File
@@ -344,19 +344,23 @@
</form>
</div>
<div class="check-col">
<p class="check-col-title">Optional level-2 rate limit</p>
<p class="muted">Caps this application only. The domain level-2
limit (<a href="#domain-settings">Domain settings</a>) and the
global level-1 limit still apply.</p>
<p class="check-col-title">Optional trusted-IP override</p>
<p class="muted">Listed client IPs get a higher ceiling than the
domain limit (still capped by level&nbsp;1:
{{$.L1Messages}} / {{$.L1Window}}s —
<a href="/settings">Settings</a>).
{{if $.DomainHasRL}}Domain ceiling: {{$.DomainRLMaxNum}}.{{else}}No domain ceiling (level&nbsp;1 only for other IPs).{{end}}</p>
<form id="rl-{{.ID}}" method="post" action="/applications/{{.ID}}/ratelimit">
<label>Expected client IPs (one per line or comma-separated)</label>
<label>Trusted client IPs (required; one per line or comma-separated)</label>
<textarea name="allowed_ips" rows="2" placeholder="203.0.113.10">{{.IPsText}}</textarea>
<p class="muted">The limit counts only connections from these
IPs. Leave empty to leave the limit inactive.</p>
<p class="muted">Only these IPs use the application ceiling and
skip the domain limit. Other IPs stay under the domain (or
level&nbsp;1).</p>
<div class="field-pair">
<div>
<label>Message limit</label>
<input name="max_messages" type="number" min="1" value="{{.MaxText}}" placeholder="500">
<input name="max_messages" type="number" min="1" max="{{$.L1Messages}}"
value="{{.MaxText}}" placeholder="{{$.L1Messages}}">
</div>
<div>
<label>Window (seconds)</label>
@@ -370,7 +374,7 @@
<button type="submit" form="rl-{{.ID}}">Save limit</button>
{{if .HasLimit}}
<form class="inline" method="post" action="/applications/{{.ID}}/ratelimit"
data-confirm="Remove the rate limit for {{.Login}}? Only the global level-1 limit will apply.">
data-confirm="Remove the rate limit for {{.Login}}? The domain limit (or level 1) will apply.">
<input type="hidden" name="clear" value="1">
<button type="submit" class="danger">Remove limit</button>
</form>
@@ -417,21 +421,18 @@
<div class="check-col">
<p class="check-col-title">Optional level-2 sending rate limit</p>
<p class="muted">Empty IP list = inactive (level-1 only). Status:
{{if .DomainHasRL}}<strong>active</strong>{{else}}inactive{{end}}.</p>
<p class="muted">Applies to every client IP on this domain. Status:
{{if .DomainHasRL}}<strong>active</strong>{{else}}inactive (level&nbsp;1 only){{end}}.
Level&nbsp;1 backstop: {{.L1Messages}} messages / {{.L1Window}}s
(<a href="/settings">Settings</a>). Leave the message limit empty to
use level&nbsp;1 only.</p>
<form method="post" action="/domains/{{.Domain.ID}}/ratelimit">
<label for="d_ips">Expected client IPs (one per line or comma-separated)</label>
<textarea id="d_ips" name="allowed_ips" rows="2"
placeholder="203.0.113.10">{{.DomainRLIPs}}</textarea>
<p class="muted">The limit counts only connections from these IPs.
Leave empty to leave the limit inactive.</p>
<div class="field-pair">
<div>
<label for="d_max">Message limit</label>
<input id="d_max" name="max_messages" type="number" min="1"
value="{{.DomainRLMax}}" placeholder="1000">
<input id="d_max" name="max_messages" type="number" min="1" max="{{.L1Messages}}"
value="{{.DomainRLMax}}" placeholder="{{.L1Messages}}">
</div>
<div>
<label for="d_win">Window (seconds)</label>
+22
View File
@@ -102,4 +102,26 @@ this one stays signed in.</p>
this one stays signed in.</p>
</div>
{{end}}
<div class="card" id="rate-limits">
<h2>Sending rate limits</h2>
<p class="muted">Configured in <code>.env</code> / Compose; restart the
container to change level&nbsp;1. Domain and application ceilings are set on
each domain's page.</p>
<label>Level 1 — per client IP (Postfix)</label>
<p><strong>{{.L1Messages}}</strong> messages per <strong>{{.L1Window}}</strong>
seconds (<code>RATE_LIMIT_MESSAGES_PER_IP</code> /
<code>RATE_LIMIT_WINDOW_SECONDS</code>). Hard ceiling for every connecting IP;
the panel cannot raise a domain or application limit above this.</p>
<label>Level 2 — domain</label>
<p class="muted">Optional ceiling for <em>all</em> senders on a domain. When
unset, only level&nbsp;1 applies. Must be ≤ level&nbsp;1.</p>
<label>Level 2 — application (trusted IPs)</label>
<p class="muted">Optional override: list client IPs and a ceiling
<em>strictly above</em> the domain limit (still ≤ level&nbsp;1). Those IPs
skip the domain check; everyone else stays under the domain (or level&nbsp;1).</p>
</div>
{{end}}
+29 -4
View File
@@ -241,15 +241,40 @@ func TestDomainDetailPageHasPairedCards(t *testing.T) {
if strings.Contains(src, `id="rate-limit"`) {
t.Error("domain rate limit should live inside domain-settings, not its own card")
}
if strings.Contains(src, `id="d_ips"`) {
t.Error("domain rate limit must not ask for client IPs")
}
if !strings.Contains(src, "level&nbsp;1") {
t.Error("domain rate limit should mention the level-1 backstop")
}
if !strings.Contains(src, "Trusted client IPs") {
t.Error("application override should ask for trusted client IPs")
}
if strings.Contains(src, `id="spf-dmarc"`) {
t.Error("SPF should sit with DKIM, not with DMARC")
}
}
// Drill-down pages carry an up-link directly under the heading and above the
// cards. A link at the bottom of a form is easy to miss and drifts from the
// rest of the panel, so the shared back_link template is mandatory on those
// pages and TestDrillDownPagesPlaceBackLinkAboveContent guards its position.
func TestSettingsPageDocumentsRateLimits(t *testing.T) {
body, err := fs.ReadFile(assetsFS, "templates/settings.html")
if err != nil {
t.Fatalf("read settings: %v", err)
}
src := string(body)
if !strings.Contains(src, `id="rate-limits"`) {
t.Error("settings should include a sending rate limits card")
}
for _, want := range []string{
"RATE_LIMIT_MESSAGES_PER_IP",
"Level 2 — domain",
"trusted IPs",
} {
if !strings.Contains(src, want) {
t.Errorf("settings rate limits card missing %q", want)
}
}
}
func TestDrillDownPagesPlaceBackLinkAboveContent(t *testing.T) {
drillDown := map[string]bool{
"user_form.html": true,