diff --git a/app/vmauth/auth_config.go b/app/vmauth/auth_config.go index 69ec0b1a9..751c85825 100644 --- a/app/vmauth/auth_config.go +++ b/app/vmauth/auth_config.go @@ -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 } diff --git a/app/vmauth/auth_config_test.go b/app/vmauth/auth_config_test.go index f6ae94ba9..210fdafbe 100644 --- a/app/vmauth/auth_config_test.go +++ b/app/vmauth/auth_config_test.go @@ -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{ diff --git a/app/vmauth/target_url.go b/app/vmauth/target_url.go index 9d43ef828..086c2e080 100644 --- a/app/vmauth/target_url.go +++ b/app/vmauth/target_url.go @@ -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 diff --git a/app/vmauth/target_url_test.go b/app/vmauth/target_url_test.go index 17e298308..1a2d22a40 100644 --- a/app/vmauth/target_url_test.go +++ b/app/vmauth/target_url_test.go @@ -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) { diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index c383aa416..dea5ebcbb 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -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). diff --git a/docs/vmauth.md b/docs/vmauth.md index 314f2cba0..d560fe9ed 100644 --- a/docs/vmauth.md +++ b/docs/vmauth.md @@ -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`: