Moved common endpointer packages to platform dir. (#37780)

<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** Resolves #37192

- Move /server/service/middleware/endpoint_utils to
/server/platform/endpointer
- Move /server/service/middleware/authzcheck to
/server/platform/middleware/authzcheck
- Move /server/service/middleware/ratelimit to
/server/platform/middleware/ratelimit

# Checklist for submitter

- [x] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.

## Testing

- [x] Added/updated automated tests
- [x] QA'd all new/changed functionality manually


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

## Release Notes

* **Refactor**
* Reorganized internal endpoint utilities to a centralized platform
location for improved code organization and maintainability. No
functional changes to existing features or APIs.

<sub>✏️ Tip: You can customize this high-level summary in your review
settings.</sub>

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Victor Lyuboslavsky
2026-01-06 14:23:07 -06:00
committed by GitHub
parent d7656a9f58
commit 3d2171d2d9
35 changed files with 143 additions and 99 deletions
@@ -0,0 +1,60 @@
// Package authzcheck implements a middleware that ensures that an authorization
// check was performed. This does not ensure that the correct authorization
// check was performed, but offers a backstop in case that a developer misses a
// check.
package authzcheck
import (
"context"
"errors"
authz_ctx "github.com/fleetdm/fleet/v4/server/contexts/authz"
platform_http "github.com/fleetdm/fleet/v4/server/platform/http"
"github.com/go-kit/kit/endpoint"
)
// Middleware is the authzcheck middleware type.
type Middleware struct{}
// NewMiddleware returns a new authzcheck middleware.
func NewMiddleware() *Middleware {
return &Middleware{}
}
func (m *Middleware) AuthzCheck() endpoint.Middleware {
return func(next endpoint.Endpoint) endpoint.Endpoint {
return func(ctx context.Context, req interface{}) (interface{}, error) {
authzctx := &authz_ctx.AuthorizationContext{}
ctx = authz_ctx.NewContext(ctx, authzctx)
response, err := next(ctx, req)
// If authentication check failed, return that error (so that we log
// appropriately).
var authFailedError *platform_http.AuthFailedError
var authRequiredError *platform_http.AuthRequiredError
var authHeaderRequiredError *platform_http.AuthHeaderRequiredError
if errors.As(err, &authFailedError) ||
errors.As(err, &authRequiredError) ||
errors.As(err, &authHeaderRequiredError) ||
errors.Is(err, platform_http.ErrPasswordResetRequired) {
return nil, err
}
// TODO(mna): currently, any error detected before an authorization check gets
// lost and the response is always Unauthorized because of the following condition.
// I _think_ it would be safe to check here of response.error() returns a non-nil
// error and if so, leave that error go through instead of returning a check missing
// authorization error. To look into when addressing #4406.
// If authorization was not checked, return a response that will
// marshal to a generic error and log that the check was missed.
if !authzctx.Checked() {
// Getting to here means there is an authorization-related bug in our code.
return nil, platform_http.CheckMissingWithResponse(response)
}
return response, err
}
}
}
@@ -0,0 +1,70 @@
package authzcheck
import (
"context"
"testing"
"github.com/fleetdm/fleet/v4/server/contexts/authz"
platform_http "github.com/fleetdm/fleet/v4/server/platform/http"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestAuthzCheck(t *testing.T) {
t.Parallel()
checker := NewMiddleware()
check := func(ctx context.Context, req interface{}) (interface{}, error) {
authCtx, ok := authz.FromContext(ctx)
require.True(t, ok)
authCtx.SetChecked()
return struct{}{}, nil
}
check = checker.AuthzCheck()(check)
_, err := check(context.Background(), struct{}{})
assert.NoError(t, err)
}
func TestAuthzCheckAuthFailed(t *testing.T) {
t.Parallel()
checker := NewMiddleware()
check := func(ctx context.Context, req interface{}) (interface{}, error) {
return nil, platform_http.NewAuthFailedError("failed")
}
check = checker.AuthzCheck()(check)
_, err := check(context.Background(), struct{}{})
assert.Error(t, err)
assert.Contains(t, err.Error(), "failed")
}
func TestAuthzCheckAuthRequired(t *testing.T) {
t.Parallel()
checker := NewMiddleware()
check := func(ctx context.Context, req interface{}) (interface{}, error) {
return nil, platform_http.NewAuthRequiredError("required")
}
check = checker.AuthzCheck()(check)
_, err := check(context.Background(), struct{}{})
assert.Error(t, err)
assert.Contains(t, err.Error(), "required")
}
func TestAuthzCheckMissing(t *testing.T) {
t.Parallel()
checker := NewMiddleware()
nocheck := func(context.Context, interface{}) (interface{}, error) { return struct{}{}, nil }
nocheck = checker.AuthzCheck()(nocheck)
_, err := nocheck(context.Background(), struct{}{})
assert.Error(t, err)
}
@@ -0,0 +1,165 @@
package ratelimit
import (
"context"
"fmt"
"net/http"
authz_ctx "github.com/fleetdm/fleet/v4/server/contexts/authz"
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
"github.com/fleetdm/fleet/v4/server/contexts/publicip"
"github.com/go-kit/kit/endpoint"
kitlog "github.com/go-kit/log"
"github.com/go-kit/log/level"
"github.com/throttled/throttled/v2"
)
// Middleware is a rate limiting middleware using the provided store. Each
// function wrapped by the rate limiter receives a separate quota.
type Middleware struct {
store throttled.GCRAStore
}
// NewMiddleware initializes the middleware with the provided store.
func NewMiddleware(store throttled.GCRAStore) *Middleware {
if store == nil {
panic("nil store")
}
return &Middleware{store: store}
}
// Limit returns a new middleware function enforcing the provided quota.
func (m *Middleware) Limit(keyName string, quota throttled.RateQuota) endpoint.Middleware {
return func(next endpoint.Endpoint) endpoint.Endpoint {
limiter, err := throttled.NewGCRARateLimiter(m.store, quota)
if err != nil {
panic(err)
}
return func(ctx context.Context, req interface{}) (response interface{}, err error) {
limited, result, err := limiter.RateLimit(keyName, 1)
if err != nil {
// This can happen if the limit store (e.g. Redis) is unavailable.
//
// We need to set authentication as checked, otherwise we end up returning HTTP 500
// errors.
if az, ok := authz_ctx.FromContext(ctx); ok {
az.SetChecked()
}
return nil, ctxerr.Wrap(ctx, err, "rate limit Middleware: failed to increase rate limit")
}
if limited {
// We need to set authentication as checked, otherwise we end up returning HTTP 500
// errors.
if az, ok := authz_ctx.FromContext(ctx); ok {
az.SetChecked()
}
return nil, ctxerr.Wrap(ctx, &rateLimitError{result: result})
}
return next(ctx, req)
}
}
}
// ErrorMiddleware is a rate limiter that performs limits only when there is an error in the request
type ErrorMiddleware struct {
ipBanner IPBanner
}
// IPBanner is an interface to perform rate limiting based on the request's IP.
type IPBanner interface {
// CheckBanned returns true if the IP is currently banned.
CheckBanned(ip string) (bool, error)
// RunRequest will update the status of the given IP with the result of a request.
RunRequest(ip string, success bool) error
}
// NewErrorMiddleware creates a new instance of ErrorMiddleware
func NewErrorMiddleware(ipBanner IPBanner) *ErrorMiddleware {
if ipBanner == nil {
panic("internal error: nil IP banner")
}
return &ErrorMiddleware{ipBanner: ipBanner}
}
func (m *ErrorMiddleware) Limit(logger kitlog.Logger) endpoint.Middleware {
return func(next endpoint.Endpoint) endpoint.Endpoint {
return func(ctx context.Context, req interface{}) (response interface{}, err error) {
publicIP := publicip.FromContext(ctx)
//
// Requests with empty public IP will fall under the same bucket.
//
banned, err := m.ipBanner.CheckBanned(publicIP)
if err != nil {
// This can happen if the limit store (e.g. Redis) is unavailable.
//
// We need to set authentication as checked, otherwise we end up returning HTTP 500 errors.
if az, ok := authz_ctx.FromContext(ctx); ok {
az.SetChecked()
}
return nil, ctxerr.Wrap(ctx, err, "rate limit ErrorMiddleware: failed to check rate limit")
}
if banned {
// We need to set authentication as checked, otherwise we end up returning HTTP 500 errors.
if az, ok := authz_ctx.FromContext(ctx); ok {
az.SetChecked()
}
level.Warn(logger).Log(
"ip", publicIP,
"msg", "limit exceeded",
)
return nil, ctxerr.Wrap(ctx, &rateLimitError{})
}
resp, err := next(ctx, req)
if rateErr := m.ipBanner.RunRequest(publicIP, err == nil); rateErr != nil {
level.Warn(logger).Log(
"ip", publicIP,
"msg", "fail to run request on IP banner",
"err", rateErr,
)
}
return resp, err
}
}
}
// Error is the interface for rate limiting errors.
type Error interface {
error
Result() throttled.RateLimitResult
}
type rateLimitError struct {
result throttled.RateLimitResult
}
func (r rateLimitError) Error() string {
ra := int(r.result.RetryAfter.Seconds())
if ra > 0 {
return fmt.Sprintf("limit exceeded, retry after: %ds", ra)
}
return "limit exceeded"
}
func (r rateLimitError) StatusCode() int {
return http.StatusTooManyRequests
}
func (r rateLimitError) RetryAfter() int {
return int(r.result.RetryAfter.Seconds())
}
func (r rateLimitError) Result() throttled.RateLimitResult {
return r.result
}
@@ -0,0 +1,85 @@
package ratelimit
import (
"context"
"errors"
"net/http"
"testing"
authz_ctx "github.com/fleetdm/fleet/v4/server/contexts/authz"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/throttled/throttled/v2"
"github.com/throttled/throttled/v2/store/memstore"
)
// Intent is to test the middleware functionality. We rely on the tests within
// Throttled to verify that the rate limiting algorithm works properly.
func TestLimit(t *testing.T) {
t.Parallel()
store, _ := memstore.New(0)
limiter := NewMiddleware(store)
var endpointCallCount uint
endpoint := func(context.Context, interface{}) (interface{}, error) {
endpointCallCount++
return struct{}{}, nil
}
wrapped := limiter.Limit(
"test_limit",
throttled.RateQuota{MaxRate: throttled.PerHour(1), MaxBurst: 0},
)(endpoint)
wrapped2 := limiter.Limit(
"test_limit2",
throttled.RateQuota{MaxRate: throttled.PerHour(1), MaxBurst: 0},
)(endpoint)
sameWrapped := limiter.Limit(
"test_limit",
throttled.RateQuota{MaxRate: throttled.PerHour(1), MaxBurst: 0},
)(endpoint)
authzCtx := &authz_ctx.AuthorizationContext{}
ctx := authz_ctx.NewContext(context.Background(), authzCtx)
_, err := wrapped(ctx, struct{}{})
assert.NoError(t, err)
// Hits rate limit
_, err = wrapped(ctx, struct{}{})
assert.Error(t, err)
var rle Error
assert.True(t, errors.As(err, &rle))
assert.True(t, authzCtx.Checked())
require.Contains(t, rle.Error(), "limit exceeded, retry after: ")
rle_, ok := rle.(*rateLimitError)
require.True(t, ok)
require.NotZero(t, rle_.RetryAfter())
require.Equal(t, http.StatusTooManyRequests, rle_.StatusCode())
// ensure that the same endpoint wrapped with a different limiter doesn't hit the error
_, err = wrapped2(ctx, struct{}{})
assert.NoError(t, err)
// Same underlying key, so hits same limit
_, err = sameWrapped(ctx, struct{}{})
assert.Error(t, err)
assert.True(t, errors.As(err, &rle))
assert.True(t, authzCtx.Checked())
assert.Equal(t, uint(2), endpointCallCount) // when rate limit is exceeded, shouldn't call endpoint
}
func TestNewErrorMiddlewarePanics(t *testing.T) {
defer func() {
if r := recover(); r == nil {
t.Errorf("The code did not panic")
}
}()
NewErrorMiddleware(nil)
}