+
+ Your current remaining daily quota is:
+ {{$quotaRemaining}}/{{$quotaLimit}}. This value resets at
+ midnight UTC.
+
+ {{if gt $quotaRemaining $quotaLimit}}
+ If your remaining quota exceeds the maximum quota, it means a realm
+ administrator added a temporary burst.
+ {{end}}
+
+
diff --git a/go.mod b/go.mod
index f52e6dbf3..df6dcb57a 100644
--- a/go.mod
+++ b/go.mod
@@ -47,9 +47,9 @@ require (
github.com/rakutentech/jwk-go v1.0.1
github.com/russross/blackfriday/v2 v2.0.1
github.com/sethvargo/go-envconfig v0.3.2
- github.com/sethvargo/go-limiter v0.5.2
+ github.com/sethvargo/go-limiter v0.6.0
github.com/sethvargo/go-password v0.2.0
- github.com/sethvargo/go-redisstore v0.2.1-opencensus
+ github.com/sethvargo/go-redisstore v0.3.0-opencensus
github.com/sethvargo/go-retry v0.1.0
github.com/sethvargo/go-signalcontext v0.1.0
github.com/sirupsen/logrus v1.7.0 // indirect
diff --git a/go.sum b/go.sum
index 4e98cc2e7..8bc1e183b 100644
--- a/go.sum
+++ b/go.sum
@@ -1153,12 +1153,12 @@ github.com/sethvargo/go-envconfig v0.3.2 h1:277Lb2iTpUZjUZu1qLoLa/aetwvtZbKh8wNW
github.com/sethvargo/go-envconfig v0.3.2/go.mod h1:XZ2JRR7vhlBEO5zMmOpLgUhgYltqYqq4d4tKagtPUv0=
github.com/sethvargo/go-gcpkms v0.1.0 h1:pyjDLqLwpk9pMjDSTilPpaUjgP1AfSjX9WGzitZwGUY=
github.com/sethvargo/go-gcpkms v0.1.0/go.mod h1:33BuvqUjsYk0bpMgn+WCclCYtMLOyaqtn5j0fCo4vvk=
-github.com/sethvargo/go-limiter v0.5.2 h1:NIFp7xy3NyE2+mEHbengdLQF0C0STOpwF5Qw5JtayIs=
-github.com/sethvargo/go-limiter v0.5.2/go.mod h1:C0kbSFbiriE5k2FFOe18M1YZbAR2Fiwf72uGu0CXCcU=
+github.com/sethvargo/go-limiter v0.6.0 h1:186jmCdl1ItQUXbHFdTBrFSZztN6/bL9855C5jfMlKU=
+github.com/sethvargo/go-limiter v0.6.0/go.mod h1:C0kbSFbiriE5k2FFOe18M1YZbAR2Fiwf72uGu0CXCcU=
github.com/sethvargo/go-password v0.2.0 h1:BTDl4CC/gjf/axHMaDQtw507ogrXLci6XRiLc7i/UHI=
github.com/sethvargo/go-password v0.2.0/go.mod h1:Ym4Mr9JXLBycr02MFuVQ/0JHidNetSgbzutTr3zsYXE=
-github.com/sethvargo/go-redisstore v0.2.1-opencensus h1:EAwZAuZr5DJdLmEruJTj2zeBvmZsIXI7wqgMueuaxks=
-github.com/sethvargo/go-redisstore v0.2.1-opencensus/go.mod h1:TMFAy7azG5hDd/Hb5ng2CDsawcxg1+oEuGhuVp7eycI=
+github.com/sethvargo/go-redisstore v0.3.0-opencensus h1:H9W15fuJHwHmttV+G6oY94J/YjxhRz0E/1S5y7elxlg=
+github.com/sethvargo/go-redisstore v0.3.0-opencensus/go.mod h1:byILvIz3sOqWiKLQmL7KUK0CzD3MWHajkksZH7V43yk=
github.com/sethvargo/go-retry v0.1.0 h1:8sPqlWannzcReEcYjHSNw9becsiYudcwTD7CasGjQaI=
github.com/sethvargo/go-retry v0.1.0/go.mod h1:JzIOdZqQDNpPkQDmcqgtteAcxFLtYpNF/zJCM1ysDg8=
github.com/sethvargo/go-signalcontext v0.1.0 h1:3IU7HOlmRXF0PSDf85C4nJ/zjYDjF+DS+LufcKfLvyk=
diff --git a/pkg/controller/issueapi/issue.go b/pkg/controller/issueapi/issue.go
index 41b79dc49..b047586a3 100644
--- a/pkg/controller/issueapi/issue.go
+++ b/pkg/controller/issueapi/issue.go
@@ -192,31 +192,35 @@ func (c *Controller) HandleIssue() http.Handler {
}
}
- // If we got this far, we're about to issue a code.
- dig, err := digest.HMACUint(realm.ID, c.config.GetRateLimitConfig().HMACKey)
- if err != nil {
- controller.InternalError(w, r, c.h, err)
- return
- }
- key := fmt.Sprintf("realm:quota:%s", dig)
- limit, remaining, reset, ok, err := c.limiter.Take(ctx, key)
- if err != nil {
- logger.Errorw("failed to take from limiter", "error", err)
- stats.Record(ctx, c.metrics.QuotaErrors.M(1))
- c.h.RenderJSON(w, http.StatusInternalServerError, api.Errorf("failed to verify realm stats, please try again"))
- return
- }
- if !ok {
- logger.Warnw("realm has exceeded daily quota",
- "realm", realm.ID,
- "limit", limit,
- "reset", reset)
- stats.Record(ctx, c.metrics.QuotaExceeded.M(1))
-
- if c.config.GetEnforceRealmQuotas() {
- c.h.RenderJSON(w, http.StatusTooManyRequests, api.Errorf("exceeded realm quota"))
+ // If we got this far, we're about to issue a code - take from the limiter
+ // to ensure this is permitted.
+ if realm.AbusePreventionEnabled {
+ dig, err := digest.HMACUint(realm.ID, c.config.GetRateLimitConfig().HMACKey)
+ if err != nil {
+ controller.InternalError(w, r, c.h, err)
return
}
+ key := fmt.Sprintf("realm:quota:%s", dig)
+ limit, remaining, reset, ok, err := c.limiter.Take(ctx, key)
+ c.recordCapacity(ctx, limit, remaining)
+ if err != nil {
+ logger.Errorw("failed to take from limiter", "error", err)
+ stats.Record(ctx, c.metrics.QuotaErrors.M(1))
+ c.h.RenderJSON(w, http.StatusInternalServerError, api.Errorf("failed to verify realm stats, please try again"))
+ return
+ }
+ if !ok {
+ logger.Warnw("realm has exceeded daily quota",
+ "realm", realm.ID,
+ "limit", limit,
+ "reset", reset)
+ stats.Record(ctx, c.metrics.QuotaExceeded.M(1))
+
+ if c.config.GetEnforceRealmQuotas() {
+ c.h.RenderJSON(w, http.StatusTooManyRequests, api.Errorf("exceeded realm quota"))
+ return
+ }
+ }
}
now := time.Now().UTC()
@@ -251,8 +255,6 @@ func (c *Controller) HandleIssue() http.Handler {
return
}
- c.recordCapacity(ctx, realm, remaining)
-
if request.Phone != "" && smsProvider != nil {
message := realm.BuildSMSText(code, longCode, c.config.GetENXRedirectDomain())
if err := smsProvider.SendSMS(ctx, request.Phone, message); err != nil {
@@ -296,13 +298,9 @@ func (c *Controller) getAuthorizationFromContext(r *http.Request) (*database.Aut
return authorizedApp, currentUser, nil
}
-func (c *Controller) recordCapacity(ctx context.Context, realm *database.Realm, remaining uint64) {
- if !realm.AbusePreventionEnabled {
- return
- }
+func (c *Controller) recordCapacity(ctx context.Context, limit, remaining uint64) {
stats.Record(ctx, c.metrics.RealmTokenRemaining.M(int64(remaining)))
- limit := realm.AbusePreventionEffectiveLimit()
issued := uint64(limit) - remaining
stats.Record(ctx, c.metrics.RealmTokenIssued.M(int64(issued)))
diff --git a/pkg/controller/realmadmin/express.go b/pkg/controller/realmadmin/express.go
index 4e7fc0fc3..1c214a3a6 100644
--- a/pkg/controller/realmadmin/express.go
+++ b/pkg/controller/realmadmin/express.go
@@ -46,7 +46,7 @@ func (c *Controller) HandleDisableExpress() http.Handler {
if !realm.EnableENExpress {
flash.Error("Realm is not currently enrolled in EN Express.")
- c.renderSettings(ctx, w, r, realm, nil)
+ c.renderSettings(ctx, w, r, realm, nil, 0, 0)
return
}
@@ -56,7 +56,7 @@ func (c *Controller) HandleDisableExpress() http.Handler {
if err := c.db.SaveRealm(realm, currentUser); err != nil {
flash.Error("Failed to disable EN Express: %v", err)
- c.renderSettings(ctx, w, r, realm, nil)
+ c.renderSettings(ctx, w, r, realm, nil, 0, 0)
return
}
@@ -90,7 +90,7 @@ func (c *Controller) HandleEnableExpress() http.Handler {
if realm.EnableENExpress {
flash.Error("Realm already has EN Express Enabled.")
- c.renderSettings(ctx, w, r, realm, nil)
+ c.renderSettings(ctx, w, r, realm, nil, 0, 0)
return
}
@@ -110,7 +110,7 @@ func (c *Controller) HandleEnableExpress() http.Handler {
// This will allow the user to correct other validation errors and then click "uprade" again.
realm.EnableENExpress = false
realm.SMSTextTemplate = enxSettings.SMSTextTemplate
- c.renderSettings(ctx, w, r, realm, nil)
+ c.renderSettings(ctx, w, r, realm, nil, 0, 0)
return
}
diff --git a/pkg/controller/realmadmin/settings.go b/pkg/controller/realmadmin/settings.go
index 9d3bd5069..708348ccb 100644
--- a/pkg/controller/realmadmin/settings.go
+++ b/pkg/controller/realmadmin/settings.go
@@ -106,15 +106,31 @@ func (c *Controller) HandleSettings() http.Handler {
return
}
+ var quotaLimit, quotaRemaining uint64
+ if realm.AbusePreventionEnabled {
+ dig, err := digest.HMACUint(realm.ID, c.config.RateLimit.HMACKey)
+ if err != nil {
+ controller.InternalError(w, r, c.h, err)
+ return
+ }
+ key := fmt.Sprintf("realm:quota:%s", dig)
+
+ quotaLimit, quotaRemaining, err = c.limiter.Get(ctx, key)
+ if err != nil {
+ controller.InternalError(w, r, c.h, err)
+ return
+ }
+ }
+
if r.Method == http.MethodGet {
- c.renderSettings(ctx, w, r, realm, nil)
+ c.renderSettings(ctx, w, r, realm, nil, quotaLimit, quotaRemaining)
return
}
var form FormData
if err := controller.BindForm(w, r, &form); err != nil {
flash.Error("Failed to process form: %v", err)
- c.renderSettings(ctx, w, r, realm, nil)
+ c.renderSettings(ctx, w, r, realm, nil, quotaLimit, quotaRemaining)
return
}
@@ -158,7 +174,7 @@ func (c *Controller) HandleSettings() http.Handler {
if err != nil {
realm.AddError("allowedCIDRsAdminAPI", err.Error())
flash.Error("Failed to update realm")
- c.renderSettings(ctx, w, r, realm, nil)
+ c.renderSettings(ctx, w, r, realm, nil, quotaLimit, quotaRemaining)
return
}
realm.AllowedCIDRsAdminAPI = allowedCIDRsAdminADPI
@@ -167,7 +183,7 @@ func (c *Controller) HandleSettings() http.Handler {
if err != nil {
realm.AddError("allowedCIDRsAPIServer", err.Error())
flash.Error("Failed to update realm")
- c.renderSettings(ctx, w, r, realm, nil)
+ c.renderSettings(ctx, w, r, realm, nil, quotaLimit, quotaRemaining)
return
}
realm.AllowedCIDRsAPIServer = allowedCIDRsAPIServer
@@ -176,7 +192,7 @@ func (c *Controller) HandleSettings() http.Handler {
if err != nil {
realm.AddError("allowedCIDRsServer", err.Error())
flash.Error("Failed to update realm")
- c.renderSettings(ctx, w, r, realm, nil)
+ c.renderSettings(ctx, w, r, realm, nil, quotaLimit, quotaRemaining)
return
}
realm.AllowedCIDRsServer = allowedCIDRsServer
@@ -191,7 +207,7 @@ func (c *Controller) HandleSettings() http.Handler {
// Save realm
if err := c.db.SaveRealm(realm, currentUser); err != nil {
flash.Error("Failed to update realm: %v", err)
- c.renderSettings(ctx, w, r, realm, nil)
+ c.renderSettings(ctx, w, r, realm, nil, quotaLimit, quotaRemaining)
return
}
@@ -213,7 +229,7 @@ func (c *Controller) HandleSettings() http.Handler {
if err := c.db.SaveSMSConfig(smsConfig); err != nil {
flash.Error("Failed to update realm: %v", err)
- c.renderSettings(ctx, w, r, realm, smsConfig)
+ c.renderSettings(ctx, w, r, realm, smsConfig, quotaLimit, quotaRemaining)
return
}
} else {
@@ -229,7 +245,7 @@ func (c *Controller) HandleSettings() http.Handler {
if err := c.db.SaveSMSConfig(smsConfig); err != nil {
flash.Error("Failed to update realm: %v", err)
- c.renderSettings(ctx, w, r, realm, smsConfig)
+ c.renderSettings(ctx, w, r, realm, smsConfig, quotaLimit, quotaRemaining)
return
}
}
@@ -256,7 +272,7 @@ func (c *Controller) HandleSettings() http.Handler {
})
}
-func (c *Controller) renderSettings(ctx context.Context, w http.ResponseWriter, r *http.Request, realm *database.Realm, smsConfig *database.SMSConfig) {
+func (c *Controller) renderSettings(ctx context.Context, w http.ResponseWriter, r *http.Request, realm *database.Realm, smsConfig *database.SMSConfig, quotaLimit, quotaRemaining uint64) {
if smsConfig == nil {
var err error
smsConfig, err = realm.SMSConfig(c.db)
@@ -309,5 +325,9 @@ func (c *Controller) renderSettings(ctx context.Context, w http.ResponseWriter,
m["longCodeLengths"] = longCodeLengths
m["longCodeHours"] = longCodeHours
m["enxRedirectDomain"] = c.config.GetENXRedirectDomain()
+
+ m["quotaLimit"] = quotaLimit
+ m["quotaRemaining"] = quotaRemaining
+
c.h.RenderHTML(w, "realmadmin/edit", m)
}