mirror of
https://github.com/VictoriaMetrics/VictoriaMetrics.git
synced 2024-11-23 20:37:12 +01: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
@ -189,7 +189,7 @@ type Regex struct {
|
|||||||
// QueryArg represents HTTP query arg
|
// QueryArg represents HTTP query arg
|
||||||
type QueryArg struct {
|
type QueryArg struct {
|
||||||
Name string
|
Name string
|
||||||
Value string
|
Value *Regex
|
||||||
|
|
||||||
sOriginal string
|
sOriginal string
|
||||||
}
|
}
|
||||||
@ -203,10 +203,17 @@ func (qa *QueryArg) UnmarshalYAML(f func(interface{}) error) error {
|
|||||||
qa.sOriginal = s
|
qa.sOriginal = s
|
||||||
|
|
||||||
n := strings.IndexByte(s, '=')
|
n := strings.IndexByte(s, '=')
|
||||||
if n >= 0 {
|
if n < 0 {
|
||||||
qa.Name = s[:n]
|
return nil
|
||||||
qa.Value = s[n+1:]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -387,7 +387,10 @@ users:
|
|||||||
SrcQueryArgs: []QueryArg{
|
SrcQueryArgs: []QueryArg{
|
||||||
{
|
{
|
||||||
Name: "foo",
|
Name: "foo",
|
||||||
Value: "bar",
|
Value: &Regex{
|
||||||
|
sOriginal: "bar",
|
||||||
|
re: regexp.MustCompile("^(?:bar)$"),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
SrcHeaders: []Header{
|
SrcHeaders: []Header{
|
||||||
|
@ -91,10 +91,16 @@ func matchAnyQueryArg(qas []QueryArg, args url.Values) bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
for _, qa := range qas {
|
for _, qa := range qas {
|
||||||
if slices.Contains(args[qa.Name], qa.Value) {
|
vs, ok := args[qa.Name]
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, v := range vs {
|
||||||
|
if qa.Value.match(v) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,6 +4,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
@ -97,8 +98,13 @@ func TestCreateTargetURLSuccess(t *testing.T) {
|
|||||||
bu := up.getBackendURL()
|
bu := up.getBackendURL()
|
||||||
target := mergeURLs(bu.url, u, up.dropSrcPathPrefixParts)
|
target := mergeURLs(bu.url, u, up.dropSrcPathPrefixParts)
|
||||||
bu.put()
|
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 {
|
if s := headersToString(hc.RequestHeaders); s != expectedRequestHeaders {
|
||||||
t.Fatalf("unexpected request headers; got %q; want %q", 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)
|
}, "/../../aaa", "https://sss:3894/x/y/aaa", "", "", nil, "least_loaded", 0)
|
||||||
f(&UserInfo{
|
f(&UserInfo{
|
||||||
URLPrefix: mustParseURL("https://sss:3894/x/y"),
|
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`
|
// Complex routing with `url_map`
|
||||||
ui := &UserInfo{
|
ui := &UserInfo{
|
||||||
@ -165,7 +171,10 @@ func TestCreateTargetURLSuccess(t *testing.T) {
|
|||||||
SrcQueryArgs: []QueryArg{
|
SrcQueryArgs: []QueryArg{
|
||||||
{
|
{
|
||||||
Name: "db",
|
Name: "db",
|
||||||
Value: "foo",
|
Value: &Regex{
|
||||||
|
sOriginal: "foo",
|
||||||
|
re: regexp.MustCompile("^(?:foo)$"),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
URLPrefix: mustParseURL("http://vmselect/0/prometheus"),
|
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)
|
}, "/api/v1/query", "http://foo.bar/api/v1/query?extra_label=team=dev", "", "", nil, "least_loaded", 0)
|
||||||
f(&UserInfo{
|
f(&UserInfo{
|
||||||
URLPrefix: mustParseURL("http://foo.bar?extra_label=team=mobile"),
|
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) {
|
func TestCreateTargetURLFailure(t *testing.T) {
|
||||||
|
@ -30,6 +30,8 @@ See also [LTS releases](https://docs.victoriametrics.com/lts-releases/).
|
|||||||
|
|
||||||
## tip
|
## 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: [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).
|
* 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
|
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.
|
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,
|
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`:
|
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 `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.
|
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).
|
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`
|
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`:
|
if `TenantID` request header equals to `123:456`:
|
||||||
|
Loading…
Reference in New Issue
Block a user