Files
threshold/internal/ratelimit/handler.go
T
dependabot[bot]andJohan Brandhorst-Satzkorn 959fe3ebd3 chore(deps): bump the go group across 7 directories with 5 updates (#6035)
* chore(deps): bump the go group across 7 directories with 5 updates

Bumps the go group with 4 updates in the / directory: [github.com/coder/websocket](https://github.com/coder/websocket), [github.com/docker/docker](https://github.com/docker/docker), [github.com/prometheus/client_golang](https://github.com/prometheus/client_golang) and [mvdan.cc/gofumpt](https://github.com/mvdan/gofumpt).
Bumps the go group with 2 updates in the /api directory: [github.com/coder/websocket](https://github.com/coder/websocket) and [github.com/hashicorp/boundary/sdk](https://github.com/hashicorp/boundary).
Bumps the go group with 1 update in the /plugins/boundary/mains/aws directory: [github.com/hashicorp/boundary/sdk](https://github.com/hashicorp/boundary).
Bumps the go group with 1 update in the /plugins/boundary/mains/azure directory: [github.com/hashicorp/boundary/sdk](https://github.com/hashicorp/boundary).
Bumps the go group with 1 update in the /plugins/boundary/mains/gcp directory: [github.com/hashicorp/boundary/sdk](https://github.com/hashicorp/boundary).
Bumps the go group with 1 update in the /plugins/boundary/mains/minio directory: [github.com/hashicorp/boundary/sdk](https://github.com/hashicorp/boundary).
Bumps the go group with 1 update in the /sdk directory: [github.com/coder/websocket](https://github.com/coder/websocket).

Updates `github.com/coder/websocket` from 1.8.13 to 1.8.14
- [Release notes](https://github.com/coder/websocket/releases)
- [Commits](https://github.com/coder/websocket/compare/v1.8.13...v1.8.14)

Updates `github.com/docker/docker` from 28.3.3+incompatible to 28.4.0+incompatible
- [Release notes](https://github.com/docker/docker/releases)
- [Commits](https://github.com/docker/docker/compare/v28.3.3...v28.4.0)

Updates `github.com/prometheus/client_golang` from 1.23.0 to 1.23.2
- [Release notes](https://github.com/prometheus/client_golang/releases)
- [Changelog](https://github.com/prometheus/client_golang/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prometheus/client_golang/compare/v1.23.0...v1.23.2)

Updates `mvdan.cc/gofumpt` from 0.8.0 to 0.9.0
- [Release notes](https://github.com/mvdan/gofumpt/releases)
- [Changelog](https://github.com/mvdan/gofumpt/blob/master/CHANGELOG.md)
- [Commits](https://github.com/mvdan/gofumpt/compare/v0.8.0...v0.9.0)

Updates `github.com/coder/websocket` from 1.8.13 to 1.8.14
- [Release notes](https://github.com/coder/websocket/releases)
- [Commits](https://github.com/coder/websocket/compare/v1.8.13...v1.8.14)

Updates `github.com/hashicorp/boundary/sdk` from 0.0.54 to 0.0.55
- [Release notes](https://github.com/hashicorp/boundary/releases)
- [Changelog](https://github.com/hashicorp/boundary/blob/main/CHANGELOG.md)
- [Commits](https://github.com/hashicorp/boundary/compare/api/v0.0.54...api/v0.0.55)

Updates `github.com/hashicorp/boundary/sdk` from 0.0.54 to 0.0.55
- [Release notes](https://github.com/hashicorp/boundary/releases)
- [Changelog](https://github.com/hashicorp/boundary/blob/main/CHANGELOG.md)
- [Commits](https://github.com/hashicorp/boundary/compare/api/v0.0.54...api/v0.0.55)

Updates `github.com/hashicorp/boundary/sdk` from 0.0.54 to 0.0.55
- [Release notes](https://github.com/hashicorp/boundary/releases)
- [Changelog](https://github.com/hashicorp/boundary/blob/main/CHANGELOG.md)
- [Commits](https://github.com/hashicorp/boundary/compare/api/v0.0.54...api/v0.0.55)

Updates `github.com/hashicorp/boundary/sdk` from 0.0.54 to 0.0.55
- [Release notes](https://github.com/hashicorp/boundary/releases)
- [Changelog](https://github.com/hashicorp/boundary/blob/main/CHANGELOG.md)
- [Commits](https://github.com/hashicorp/boundary/compare/api/v0.0.54...api/v0.0.55)

Updates `github.com/hashicorp/boundary/sdk` from 0.0.54 to 0.0.55
- [Release notes](https://github.com/hashicorp/boundary/releases)
- [Changelog](https://github.com/hashicorp/boundary/blob/main/CHANGELOG.md)
- [Commits](https://github.com/hashicorp/boundary/compare/api/v0.0.54...api/v0.0.55)

Updates `github.com/coder/websocket` from 1.8.13 to 1.8.14
- [Release notes](https://github.com/coder/websocket/releases)
- [Commits](https://github.com/coder/websocket/compare/v1.8.13...v1.8.14)

---
updated-dependencies:
- dependency-name: github.com/coder/websocket
  dependency-version: 1.8.14
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: go
- dependency-name: github.com/docker/docker
  dependency-version: 28.4.0+incompatible
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: go
- dependency-name: github.com/prometheus/client_golang
  dependency-version: 1.23.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: go
- dependency-name: mvdan.cc/gofumpt
  dependency-version: 0.9.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: go
- dependency-name: github.com/coder/websocket
  dependency-version: 1.8.14
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: go
- dependency-name: github.com/hashicorp/boundary/sdk
  dependency-version: 0.0.55
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: go
- dependency-name: github.com/hashicorp/boundary/sdk
  dependency-version: 0.0.55
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: go
- dependency-name: github.com/hashicorp/boundary/sdk
  dependency-version: 0.0.55
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: go
- dependency-name: github.com/hashicorp/boundary/sdk
  dependency-version: 0.0.55
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: go
- dependency-name: github.com/hashicorp/boundary/sdk
  dependency-version: 0.0.55
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: go
- dependency-name: github.com/coder/websocket
  dependency-version: 1.8.14
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: go
...

Signed-off-by: dependabot[bot] <support@github.com>

* Format files with latest gofumpt

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Johan Brandhorst-Satzkorn <johan.brandhorst@gmail.com>
2025-09-08 15:17:46 -04:00

182 lines
4.9 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package ratelimit
import (
"context"
"fmt"
"math"
"net/http"
"regexp"
"github.com/hashicorp/boundary/globals"
"github.com/hashicorp/boundary/internal/event"
"github.com/hashicorp/boundary/internal/types/action"
"github.com/hashicorp/boundary/internal/types/resource"
"github.com/hashicorp/go-rate"
)
var (
errUnknownResource = &extractResourceActionErr{http.StatusNotFound, "unknown resource"}
errUnknownAction = &extractResourceActionErr{http.StatusBadRequest, "unknown action"}
errUnsupportedAction = &extractResourceActionErr{http.StatusMethodNotAllowed, "invalid action"}
)
type extractResourceActionErr struct {
statusCode int
msg string
}
func (e *extractResourceActionErr) Error() string {
return e.msg
}
var pathRegex = regexp.MustCompile(`/v1/(?P<resource>[\w-]+)((/(?P<id>[^:]+))?(:(?P<action>[\w-:]+)?)?)?`)
func extractResourceAction(path, method string) (res, act string, err error) {
var id string
var r resource.Type
var ok bool
var actionSet action.ActionSet
// TODO: replace regex with lexer
match := pathRegex.FindStringSubmatch(path)
for i, name := range pathRegex.SubexpNames() {
switch name {
case "resource":
res = match[i]
r, ok = resource.FromPlural(res)
if !ok {
res = resource.Unknown.String()
err = errUnknownResource
return res, act, err
}
res = r.String()
case "action":
act = match[i]
if act != "" {
actionSet, err = action.ActionSetForResource(r)
if err != nil {
act = action.Unknown.String()
err = errUnknownAction
return res, act, err
}
at, ok := action.Map[act]
if !ok {
act = action.Unknown.String()
err = errUnknownAction
return res, act, err
}
if !actionSet.HasAction(at) {
err = errUnsupportedAction
return res, act, err
}
}
case "id":
id = match[i]
}
}
switch act {
case "":
switch id {
case "":
switch method {
case http.MethodGet:
act = action.List.String()
case http.MethodPost:
act = action.Create.String()
default:
act = action.Unknown.String()
err = errUnsupportedAction
return res, act, err
}
default:
switch method {
case http.MethodGet:
act = action.Read.String()
case http.MethodDelete:
act = action.Delete.String()
case http.MethodPatch:
act = action.Update.String()
default:
act = action.Unknown.String()
err = errUnsupportedAction
return res, act, err
}
}
}
return res, act, err
}
// LimiterFunc returns a rate.Limiter
type LimiterFunc func() Limiter
// Handler is an http middleware handler that checks if a request is allowed
// using the rate limiter returned by f. If the request is allowed, the next handler
// is called. Otherwise a 429 is returned with the Retry-After response header
// set to the number of seconds the client should wait to make it's next request.
func Handler(ctx context.Context, f LimiterFunc, next http.Handler) http.Handler {
const op = "ratelimit.Handler"
return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
l := f()
reqInfo, ok := event.RequestInfoFromContext(req.Context())
if !ok || reqInfo == nil || reqInfo.ClientIp == "" {
rw.WriteHeader(http.StatusInternalServerError)
return
}
authtoken, ok := req.Context().Value(globals.ContextAuthTokenPublicIdKey).(string)
if !ok {
rw.WriteHeader(http.StatusInternalServerError)
return
}
res, a, err := extractResourceAction(req.URL.Path, req.Method)
if err != nil {
if extractErr, ok := err.(*extractResourceActionErr); ok {
rw.WriteHeader(extractErr.statusCode)
return
}
rw.WriteHeader(http.StatusInternalServerError)
return
}
allowed, quota, err := l.Allow(res, a, reqInfo.ClientIp, authtoken)
if err != nil {
if errFull, ok := err.(*rate.ErrLimiterFull); ok {
rw.Header().Add("Retry-After", fmt.Sprintf("%.0f", math.Ceil(errFull.RetryIn.Seconds())))
rw.WriteHeader(http.StatusServiceUnavailable)
return
}
// The only other error here should be rate.ErrLimitNotFound, which
// shouldn't be possible given how we initialize the limiter and
// the checks done by extractResourceAction.
rw.WriteHeader(http.StatusInternalServerError)
return
}
l.SetUsageHeader(quota, rw.Header())
if err := l.SetPolicyHeader(res, a, rw.Header()); err != nil {
// Wrap error to emit an error event. An error here would be
// unexpected, since the only possible error would be
// ErrLimitPolicyNotFound which would have been returned by Allow
// and handled above. And even that would be unexpected, given how
// the limiter is initialized and the checks done by
// extractResourceAction.
event.WriteError(ctx, op, fmt.Errorf("failed to set policy header: %w", err))
}
if !allowed {
rw.Header().Add("Retry-After", fmt.Sprintf("%.0f", math.Ceil(quota.ResetsIn().Seconds())))
rw.WriteHeader(http.StatusTooManyRequests)
return
}
next.ServeHTTP(rw, req)
})
}