diff --git a/cmd/server/assets/realmadmin/_form_abuse_prevention.html b/cmd/server/assets/realmadmin/_form_abuse_prevention.html index 026fad823..735b31f30 100644 --- a/cmd/server/assets/realmadmin/_form_abuse_prevention.html +++ b/cmd/server/assets/realmadmin/_form_abuse_prevention.html @@ -1,6 +1,8 @@ {{define "realmadmin/_form_abuse_prevention"}} {{$realm := .realm}} +{{$quotaRemaining := .quotaRemaining}} +{{$quotaLimit := .quotaLimit}}
{{ .csrfField }} @@ -27,6 +29,17 @@
+ +
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) }