mirror of
https://github.com/VictoriaMetrics/VictoriaMetrics.git
synced 2024-11-21 14:44:00 +00:00
app/vmauth: support regex matching in src_query_args
(#6115)
Support regex matching when routing incoming requests based on HTTP query args via `src_query_args` option at `url_map`. https://github.com/VictoriaMetrics/VictoriaMetrics/issues/6070 Signed-off-by: hagen1778 <roman@victoriametrics.com>
This commit is contained in:
parent
b4d8837917
commit
b155b20de4
6 changed files with 92 additions and 15 deletions
|
@ -189,7 +189,7 @@ type Regex struct {
|
|||
// QueryArg represents HTTP query arg
|
||||
type QueryArg struct {
|
||||
Name string
|
||||
Value string
|
||||
Value *Regex
|
||||
|
||||
sOriginal string
|
||||
}
|
||||
|
@ -203,10 +203,17 @@ func (qa *QueryArg) UnmarshalYAML(f func(interface{}) error) error {
|
|||
qa.sOriginal = s
|
||||
|
||||
n := strings.IndexByte(s, '=')
|
||||
if n >= 0 {
|
||||
qa.Name = s[:n]
|
||||
qa.Value = s[n+1:]
|
||||
if n < 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
qa.Name = s[:n]
|
||||
expr := []byte(s[n+1:])
|
||||
var re Regex
|
||||
if err := yaml.Unmarshal(expr, &re); err != nil {
|
||||
return fmt.Errorf("failed to unmarshal regex %q: %s", expr, err)
|
||||
}
|
||||
qa.Value = &re
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
@ -386,8 +386,11 @@ users:
|
|||
SrcPaths: getRegexs([]string{"/api/v1/write"}),
|
||||
SrcQueryArgs: []QueryArg{
|
||||
{
|
||||
Name: "foo",
|
||||
Value: "bar",
|
||||
Name: "foo",
|
||||
Value: &Regex{
|
||||
sOriginal: "bar",
|
||||
re: regexp.MustCompile("^(?:bar)$"),
|
||||
},
|
||||
},
|
||||
},
|
||||
SrcHeaders: []Header{
|
||||
|
|
|
@ -91,8 +91,14 @@ func matchAnyQueryArg(qas []QueryArg, args url.Values) bool {
|
|||
return true
|
||||
}
|
||||
for _, qa := range qas {
|
||||
if slices.Contains(args[qa.Name], qa.Value) {
|
||||
return true
|
||||
vs, ok := args[qa.Name]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
for _, v := range vs {
|
||||
if qa.Value.match(v) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
|
|
|
@ -4,6 +4,7 @@ import (
|
|||
"fmt"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
@ -97,8 +98,13 @@ func TestCreateTargetURLSuccess(t *testing.T) {
|
|||
bu := up.getBackendURL()
|
||||
target := mergeURLs(bu.url, u, up.dropSrcPathPrefixParts)
|
||||
bu.put()
|
||||
if target.String() != expectedTarget {
|
||||
t.Fatalf("unexpected target; got %q; want %q", target, expectedTarget)
|
||||
|
||||
gotTarget, err := url.QueryUnescape(target.String())
|
||||
if err != nil {
|
||||
t.Fatalf("failed to unescape query %q: %s", target, err)
|
||||
}
|
||||
if gotTarget != expectedTarget {
|
||||
t.Fatalf("unexpected target; \ngot:\n%q;\nwant:\n%q", gotTarget, expectedTarget)
|
||||
}
|
||||
if s := headersToString(hc.RequestHeaders); s != expectedRequestHeaders {
|
||||
t.Fatalf("unexpected request headers; got %q; want %q", s, expectedRequestHeaders)
|
||||
|
@ -154,7 +160,7 @@ func TestCreateTargetURLSuccess(t *testing.T) {
|
|||
}, "/../../aaa", "https://sss:3894/x/y/aaa", "", "", nil, "least_loaded", 0)
|
||||
f(&UserInfo{
|
||||
URLPrefix: mustParseURL("https://sss:3894/x/y"),
|
||||
}, "/./asd/../../aaa?a=d&s=s/../d", "https://sss:3894/x/y/aaa?a=d&s=s%2F..%2Fd", "", "", nil, "least_loaded", 0)
|
||||
}, "/./asd/../../aaa?a=d&s=s/../d", "https://sss:3894/x/y/aaa?a=d&s=s/../d", "", "", nil, "least_loaded", 0)
|
||||
|
||||
// Complex routing with `url_map`
|
||||
ui := &UserInfo{
|
||||
|
@ -164,8 +170,11 @@ func TestCreateTargetURLSuccess(t *testing.T) {
|
|||
SrcPaths: getRegexs([]string{"/vmsingle/api/v1/query"}),
|
||||
SrcQueryArgs: []QueryArg{
|
||||
{
|
||||
Name: "db",
|
||||
Value: "foo",
|
||||
Name: "db",
|
||||
Value: &Regex{
|
||||
sOriginal: "foo",
|
||||
re: regexp.MustCompile("^(?:foo)$"),
|
||||
},
|
||||
},
|
||||
},
|
||||
URLPrefix: mustParseURL("http://vmselect/0/prometheus"),
|
||||
|
@ -249,7 +258,43 @@ func TestCreateTargetURLSuccess(t *testing.T) {
|
|||
}, "/api/v1/query", "http://foo.bar/api/v1/query?extra_label=team=dev", "", "", nil, "least_loaded", 0)
|
||||
f(&UserInfo{
|
||||
URLPrefix: mustParseURL("http://foo.bar?extra_label=team=mobile"),
|
||||
}, "/api/v1/query?extra_label=team=dev", "http://foo.bar/api/v1/query?extra_label=team%3Dmobile", "", "", nil, "least_loaded", 0)
|
||||
}, "/api/v1/query?extra_label=team=dev", "http://foo.bar/api/v1/query?extra_label=team=mobile", "", "", nil, "least_loaded", 0)
|
||||
|
||||
// Complex routing regexp query args in `url_map`
|
||||
ui = &UserInfo{
|
||||
URLMaps: []URLMap{
|
||||
{
|
||||
SrcPaths: getRegexs([]string{"/api/v1/query"}),
|
||||
SrcQueryArgs: []QueryArg{
|
||||
{
|
||||
Name: "query",
|
||||
Value: &Regex{
|
||||
sOriginal: "foo",
|
||||
re: regexp.MustCompile(`^(?:.*env="dev".*)$`),
|
||||
},
|
||||
},
|
||||
},
|
||||
URLPrefix: mustParseURL("http://vmselect/0/prometheus"),
|
||||
},
|
||||
{
|
||||
SrcPaths: getRegexs([]string{"/api/v1/query"}),
|
||||
SrcQueryArgs: []QueryArg{
|
||||
{
|
||||
Name: "query",
|
||||
Value: &Regex{
|
||||
sOriginal: "foo",
|
||||
re: regexp.MustCompile(`^(?:.*env="prod".*)$`),
|
||||
},
|
||||
},
|
||||
},
|
||||
URLPrefix: mustParseURL("http://vmselect/1/prometheus"),
|
||||
},
|
||||
},
|
||||
URLPrefix: mustParseURL("http://default-server"),
|
||||
}
|
||||
f(ui, `/api/v1/query?query=up{env="prod"}`, `http://vmselect/1/prometheus/api/v1/query?query=up{env="prod"}`, "", "", nil, "least_loaded", 0)
|
||||
f(ui, `/api/v1/query?query=up{foo="bar", env="dev", pod!=""}`, `http://vmselect/0/prometheus/api/v1/query?query=up{foo="bar", env="dev", pod!=""}`, "", "", nil, "least_loaded", 0)
|
||||
f(ui, `/api/v1/query?query=up{foo="bar"}`, `http://default-server/api/v1/query?query=up{foo="bar"}`, "", "", nil, "least_loaded", 0)
|
||||
}
|
||||
|
||||
func TestCreateTargetURLFailure(t *testing.T) {
|
||||
|
|
|
@ -30,6 +30,8 @@ See also [LTS releases](https://docs.victoriametrics.com/lts-releases/).
|
|||
|
||||
## tip
|
||||
|
||||
* FEATURE: [vmauth](https://docs.victoriametrics.com/vmauth/): support regex matching when routing incoming requests based on HTTP [query args](https://en.wikipedia.org/wiki/Query_string) via `src_query_args` option at `url_map`. See [these docs](https://docs.victoriametrics.com/vmauth/#generic-http-proxy-for-different-backends) and [this feature request](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/6070).
|
||||
|
||||
* BUGFIX: [vmalert](https://docs.victoriametrics.com/vmalert.html): supported any status codes from the range 200-299 from alertmanager. Previously, only 200 status code considered a successful action. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/6110).
|
||||
* BUGFIX: [vmauth](https://docs.victoriametrics.com/vmauth/): don't treat concurrency limit hit as an error of the backend. Previously, hitting the concurrency limit would increment both `vmauth_concurrent_requests_limit_reached_total` and `vmauth_user_request_backend_errors_total` counters. Now, only concurrency limit counter is incremented. Updates [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/5565).
|
||||
|
||||
|
|
|
@ -117,7 +117,7 @@ if the whole request path matches at least one `src_paths` entry. The incoming r
|
|||
If both `src_paths` and `src_hosts` lists are specified, then the request is routed to the given `url_prefix` when both request path and request host match at least one entry
|
||||
in the corresponding lists.
|
||||
|
||||
An optional `src_query_args` can be used for routing requests based on [HTTP query args](https://en.wikipedia.org/wiki/Query_string) additionaly to hostname and path.
|
||||
An optional `src_query_args` can be used for routing requests based on [HTTP query args](https://en.wikipedia.org/wiki/Query_string) additionally to hostname and path.
|
||||
For example, the following config routes requests to `http://app1-backend/` if `db=foo` query arg is present in the request,
|
||||
while routing requests with `db=bar` query arg to `http://app2-backend`:
|
||||
|
||||
|
@ -135,6 +135,20 @@ If `src_query_args` contains multiple entries, then it is enough to match only a
|
|||
If `src_query_args` are specified together with `src_hosts`, `src_paths` or `src_headers`, then the request is routed to the given `url_prefix`
|
||||
if its query args, host, path and headers match the given lists simultaneously.
|
||||
|
||||
`src_query_args` supports regex matching:
|
||||
```yaml
|
||||
unauthorized_user:
|
||||
url_map:
|
||||
- src_query_args: [ "query=.*env=\"prod\".*" ]
|
||||
url_prefix: "http://prod-backend/"
|
||||
- src_query_args: [ "query=.*env=\"dev\".*" ]
|
||||
url_prefix: "http://dev-backend/"
|
||||
```
|
||||
The config above will route requests like `/api/v1/query?query=up{env="prod"}` to `http://prod-backend/`.
|
||||
And queries matching `.*env=\"dev\".*` will be routed to `http://dev-backend/`.
|
||||
_Please note, by default Grafana sends `query` param in request's body and vmauth won't be able to read it.
|
||||
You need to manually switch datasource settings in Grafana to use GET method for sending queries._
|
||||
|
||||
An optional `src_headers` can be used for routing requests based on HTTP request headers additionally to hostname, path and [HTTP query args](https://en.wikipedia.org/wiki/Query_string).
|
||||
For example, the following config routes requests to `http://app1-backend` if `TenantID` request header equals to `42`, while routing requests to `http://app2-backend`
|
||||
if `TenantID` request header equals to `123:456`:
|
||||
|
|
Loading…
Reference in a new issue