-
Notifications
You must be signed in to change notification settings - Fork 544
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
frontend: Allow blocking raw http requests (#10484)
* frontend: Allow blocking raw http requests This is a setting that can be set in user overrides like this: ``` user1: blocked_requests: - path: /api/v1/series - method: DELETE - query_params foo: bar ``` Or a combination of these. Each entry is an AND condition * Add Roundtripper test * linting... * Allow for regexps on the query params + add changelog * Add test for unmarshalling * Fix linting (cherry picked from commit 557830d)
- Loading branch information
1 parent
d5e7264
commit d8c9a77
Showing
17 changed files
with
462 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
File renamed without changes.
File renamed without changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,96 @@ | ||
// SPDX-License-Identifier: AGPL-3.0-only | ||
|
||
package querymiddleware | ||
|
||
import ( | ||
"net/http" | ||
|
||
"github.com/go-kit/log" | ||
"github.com/go-kit/log/level" | ||
"github.com/grafana/dskit/tenant" | ||
"github.com/grafana/regexp" | ||
"github.com/prometheus/client_golang/prometheus" | ||
"github.com/prometheus/client_golang/prometheus/promauto" | ||
|
||
apierror "github.com/grafana/mimir/pkg/api/error" | ||
"github.com/grafana/mimir/pkg/util/globalerror" | ||
) | ||
|
||
func newRequestBlockedError() error { | ||
return apierror.New(apierror.TypeBadData, globalerror.RequestBlocked.Message("the request has been blocked by the cluster administrator")) | ||
} | ||
|
||
type requestBlocker struct { | ||
limits Limits | ||
logger log.Logger | ||
blockedRequestsCounter *prometheus.CounterVec | ||
} | ||
|
||
func newRequestBlocker( | ||
limits Limits, | ||
logger log.Logger, | ||
registerer prometheus.Registerer, | ||
) *requestBlocker { | ||
blockedRequestsCounter := promauto.With(registerer).NewCounterVec(prometheus.CounterOpts{ | ||
Name: "cortex_query_frontend_rejected_requests_total", | ||
Help: "Number of HTTP requests that were rejected by the cluster administrator.", | ||
}, []string{"user"}) | ||
return &requestBlocker{ | ||
limits: limits, | ||
logger: logger, | ||
blockedRequestsCounter: blockedRequestsCounter, | ||
} | ||
} | ||
|
||
func (rb *requestBlocker) isBlocked(r *http.Request) error { | ||
tenants, err := tenant.TenantIDs(r.Context()) | ||
if err != nil { | ||
return nil | ||
} | ||
|
||
for _, tenant := range tenants { | ||
blockedRequests := rb.limits.BlockedRequests(tenant) | ||
|
||
for _, blockedRequest := range blockedRequests { | ||
if blockedPath := blockedRequest.Path; blockedPath != "" && blockedPath != r.URL.Path { | ||
continue | ||
} | ||
|
||
if blockedMethod := blockedRequest.Method; blockedMethod != "" && blockedMethod != r.Method { | ||
continue | ||
} | ||
|
||
if blockedParams := blockedRequest.QueryParams; len(blockedParams) > 0 { | ||
query := r.URL.Query() | ||
blockedByParams := false | ||
for key, blocked := range blockedParams { | ||
if blocked.IsRegexp { | ||
blockedRegexp, err := regexp.Compile(blocked.Value) | ||
if err != nil { | ||
level.Error(rb.logger).Log("msg", "failed to compile regexp. Not blocking", "regexp", blocked.Value, "err", err) | ||
continue | ||
} | ||
|
||
if blockedRegexp.MatchString(query.Get(key)) { | ||
blockedByParams = true | ||
break | ||
} | ||
} else if query.Get(key) == blocked.Value { | ||
blockedByParams = true | ||
break | ||
} | ||
} | ||
|
||
if !blockedByParams { | ||
continue | ||
} | ||
} | ||
|
||
level.Info(rb.logger).Log("msg", "request blocked", "user", tenant, "url", r.URL.String(), "method", r.Method) | ||
rb.blockedRequestsCounter.WithLabelValues(tenant).Inc() | ||
return newRequestBlockedError() | ||
} | ||
} | ||
return nil | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,157 @@ | ||
// SPDX-License-Identifier: AGPL-3.0-only | ||
|
||
package querymiddleware | ||
|
||
import ( | ||
"context" | ||
"net/http" | ||
"testing" | ||
|
||
"github.com/go-kit/log" | ||
"github.com/grafana/dskit/user" | ||
"github.com/prometheus/client_golang/prometheus" | ||
"github.com/stretchr/testify/assert" | ||
"github.com/stretchr/testify/require" | ||
|
||
"github.com/grafana/mimir/pkg/util/validation" | ||
) | ||
|
||
func TestRequestBlocker_IsBlocked(t *testing.T) { | ||
const userID = "user-1" | ||
// Mock the limits. | ||
limits := multiTenantMockLimits{ | ||
byTenant: map[string]mockLimits{ | ||
userID: { | ||
blockedRequests: []*validation.BlockedRequest{ | ||
{Path: "/blocked-by-path"}, | ||
{Method: "POST"}, | ||
{ | ||
QueryParams: map[string]validation.BlockedRequestQueryParam{"foo": {Value: "bar"}}, | ||
}, | ||
{ | ||
Path: "/blocked-by-path2", Method: "GET", | ||
QueryParams: map[string]validation.BlockedRequestQueryParam{"foo": {Value: "bar2"}}, | ||
}, | ||
{ | ||
Path: "/block-by-query-regexp", Method: "GET", | ||
QueryParams: map[string]validation.BlockedRequestQueryParam{"foo": {Value: ".*hello.*", IsRegexp: true}}, | ||
}, | ||
// Invalid regexp should not block anything. | ||
{ | ||
QueryParams: map[string]validation.BlockedRequestQueryParam{"foo": {Value: "\bar", IsRegexp: true}}, | ||
}, | ||
}, | ||
}, | ||
}, | ||
} | ||
|
||
tests := []struct { | ||
name string | ||
request func() *http.Request | ||
expected error | ||
}{ | ||
{ | ||
name: "request is not blocked", | ||
request: func() *http.Request { | ||
req, err := http.NewRequest(http.MethodGet, "/not-blocked", nil) | ||
require.NoError(t, err) | ||
return req | ||
}, | ||
expected: nil, | ||
}, | ||
{ | ||
name: "request is blocked by path", | ||
request: func() *http.Request { | ||
req, err := http.NewRequest(http.MethodGet, "/blocked-by-path", nil) | ||
require.NoError(t, err) | ||
return req | ||
}, | ||
expected: newRequestBlockedError(), | ||
}, | ||
{ | ||
name: "request is blocked by method", | ||
request: func() *http.Request { | ||
req, err := http.NewRequest(http.MethodPost, "/not-blocked", nil) | ||
require.NoError(t, err) | ||
return req | ||
}, | ||
expected: newRequestBlockedError(), | ||
}, | ||
{ | ||
name: "request is blocked by query params", | ||
request: func() *http.Request { | ||
req, err := http.NewRequest(http.MethodGet, "/not-blocked?foo=bar", nil) | ||
require.NoError(t, err) | ||
return req | ||
}, | ||
expected: newRequestBlockedError(), | ||
}, | ||
{ | ||
name: "request is blocked by path, method and query params", | ||
request: func() *http.Request { | ||
req, err := http.NewRequest(http.MethodGet, "/blocked-by-path2?foo=bar2", nil) | ||
require.NoError(t, err) | ||
return req | ||
}, | ||
expected: newRequestBlockedError(), | ||
}, | ||
{ | ||
name: "request does not fully match blocked request (different method)", | ||
request: func() *http.Request { | ||
req, err := http.NewRequest(http.MethodDelete, "/blocked-by-path2?foo=bar2", nil) | ||
require.NoError(t, err) | ||
return req | ||
}, | ||
expected: nil, | ||
}, | ||
{ | ||
name: "request does not fully match blocked request (different query params)", | ||
request: func() *http.Request { | ||
req, err := http.NewRequest(http.MethodGet, "/blocked-by-path2?foo=bar3", nil) | ||
require.NoError(t, err) | ||
return req | ||
}, | ||
expected: nil, | ||
}, | ||
{ | ||
name: "request does not fully match blocked request (different path)", | ||
request: func() *http.Request { | ||
req, err := http.NewRequest(http.MethodGet, "/blocked-by-path3?foo=bar2", nil) | ||
require.NoError(t, err) | ||
return req | ||
}, | ||
expected: nil, | ||
}, | ||
{ | ||
name: "regexp does not match", | ||
request: func() *http.Request { | ||
req, err := http.NewRequest(http.MethodGet, "/block-by-query-regexp?foo=test", nil) | ||
require.NoError(t, err) | ||
return req | ||
}, | ||
expected: nil, | ||
}, | ||
{ | ||
name: "regexp matches", | ||
request: func() *http.Request { | ||
req, err := http.NewRequest(http.MethodGet, "/block-by-query-regexp?foo=my-value-hello-test", nil) | ||
require.NoError(t, err) | ||
return req | ||
}, | ||
expected: newRequestBlockedError(), | ||
}, | ||
} | ||
|
||
for _, tt := range tests { | ||
t.Run(tt.name, func(t *testing.T) { | ||
blocker := newRequestBlocker(limits, log.NewNopLogger(), prometheus.NewRegistry()) | ||
|
||
req := tt.request() | ||
ctx := user.InjectOrgID(context.Background(), userID) | ||
req = req.WithContext(ctx) | ||
|
||
err := blocker.isBlocked(req) | ||
assert.Equal(t, err, tt.expected) | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.