From b1d0028e79a20e40f9f93f48dc588a1ee27c30b1 Mon Sep 17 00:00:00 2001 From: Aliaksandr Valialkin Date: Fri, 2 Apr 2021 22:14:53 +0300 Subject: [PATCH] app/vmauth: add support for authorization via `Authorization: Bearer ` --- app/vmauth/README.md | 22 ++++++++------ app/vmauth/auth_config.go | 54 ++++++++++++++++++++++++++++------ app/vmauth/auth_config_test.go | 39 ++++++++++++++++++++---- app/vmauth/main.go | 12 ++++---- docs/CHANGELOG.md | 1 + 5 files changed, 98 insertions(+), 30 deletions(-) diff --git a/app/vmauth/README.md b/app/vmauth/README.md index 6cb3fc6bf..41953e32c 100644 --- a/app/vmauth/README.md +++ b/app/vmauth/README.md @@ -36,11 +36,15 @@ Auth config is represented in the following simple `yml` format: # Usernames must be unique. users: + # Requests with the 'Authorization: Bearer XXXX' header are proxied to http://localhost:8428 . + # For example, http://vmauth:8427/api/v1/query is proxied to http://localhost:8428/api/v1/query +- bearer_token: "XXXX" + url_prefix: "http://localhost:8428" # The user for querying local single-node VictoriaMetrics. # All the requests to http://vmauth:8427 with the given Basic Auth (username:password) - # will be routed to http://localhost:8428 . - # For example, http://vmauth:8427/api/v1/query is routed to http://localhost:8428/api/v1/query + # will be proxied to http://localhost:8428 . + # For example, http://vmauth:8427/api/v1/query is proxied to http://localhost:8428/api/v1/query - username: "local-single-node" password: "***" url_prefix: "http://localhost:8428" @@ -48,8 +52,8 @@ users: # The user for querying account 123 in VictoriaMetrics cluster # See https://victoriametrics.github.io/Cluster-VictoriaMetrics.html#url-format # All the requests to http://vmauth:8427 with the given Basic Auth (username:password) - # will be routed to http://vmselect:8481/select/123/prometheus . - # For example, http://vmauth:8427/api/v1/query is routed to http://vmselect:8481/select/123/prometheus/api/v1/select + # will be proxied to http://vmselect:8481/select/123/prometheus . + # For example, http://vmauth:8427/api/v1/query is proxied to http://vmselect:8481/select/123/prometheus/api/v1/select - username: "cluster-select-account-123" password: "***" url_prefix: "http://vmselect:8481/select/123/prometheus" @@ -57,8 +61,8 @@ users: # The user for inserting Prometheus data into VictoriaMetrics cluster under account 42 # See https://victoriametrics.github.io/Cluster-VictoriaMetrics.html#url-format # All the requests to http://vmauth:8427 with the given Basic Auth (username:password) - # will be routed to http://vminsert:8480/insert/42/prometheus . - # For example, http://vmauth:8427/api/v1/write is routed to http://vminsert:8480/insert/42/prometheus/api/v1/write + # will be proxied to http://vminsert:8480/insert/42/prometheus . + # For example, http://vmauth:8427/api/v1/write is proxied to http://vminsert:8480/insert/42/prometheus/api/v1/write - username: "cluster-insert-account-42" password: "***" url_prefix: "http://vminsert:8480/insert/42/prometheus" @@ -66,9 +70,9 @@ users: # A single user for querying and inserting data: # - Requests to http://vmauth:8427/api/v1/query, http://vmauth:8427/api/v1/query_range - # and http://vmauth:8427/api/v1/label//values are routed to http://vmselect:8481/select/42/prometheus. - # For example, http://vmauth:8427/api/v1/query is routed to http://vmselect:8480/select/42/prometheus/api/v1/query - # - Requests to http://vmauth:8427/api/v1/write are routed to http://vminsert:8480/insert/42/prometheus/api/v1/write + # and http://vmauth:8427/api/v1/label//values are proxied to http://vmselect:8481/select/42/prometheus. + # For example, http://vmauth:8427/api/v1/query is proxied to http://vmselect:8480/select/42/prometheus/api/v1/query + # - Requests to http://vmauth:8427/api/v1/write are proxied to http://vminsert:8480/insert/42/prometheus/api/v1/write - username: "foobar" url_map: - src_paths: ["/api/v1/query", "/api/v1/query_range", "/api/v1/label/[^/]+/values"] diff --git a/app/vmauth/auth_config.go b/app/vmauth/auth_config.go index dc8c1da2f..139056ffd 100644 --- a/app/vmauth/auth_config.go +++ b/app/vmauth/auth_config.go @@ -1,6 +1,7 @@ package main import ( + "encoding/base64" "flag" "fmt" "io/ioutil" @@ -29,10 +30,11 @@ type AuthConfig struct { // UserInfo is user information read from authConfigPath type UserInfo struct { - Username string `yaml:"username"` - Password string `yaml:"password"` - URLPrefix string `yaml:"url_prefix"` - URLMap []URLMap `yaml:"url_map"` + BearerToken string `yaml:"bearer_token"` + Username string `yaml:"username"` + Password string `yaml:"password"` + URLPrefix string `yaml:"url_prefix"` + URLMap []URLMap `yaml:"url_map"` requests *metrics.Counter } @@ -150,12 +152,27 @@ func parseAuthConfig(data []byte) (map[string]*UserInfo, error) { if len(uis) == 0 { return nil, fmt.Errorf("`users` section cannot be empty in AuthConfig") } - m := make(map[string]*UserInfo, len(uis)) + byAuthToken := make(map[string]*UserInfo, len(uis)) + byUsername := make(map[string]bool, len(uis)) + byBearerToken := make(map[string]bool, len(uis)) for i := range uis { ui := &uis[i] - if m[ui.Username] != nil { + if ui.BearerToken == "" && ui.Username == "" { + return nil, fmt.Errorf("either bearer_token or username must be set") + } + if ui.BearerToken != "" && ui.Username != "" { + return nil, fmt.Errorf("bearer_token=%q and username=%q cannot be set simultaneously", ui.BearerToken, ui.Username) + } + if byBearerToken[ui.BearerToken] { + return nil, fmt.Errorf("duplicate bearer_token found; bearer_token: %q", ui.BearerToken) + } + if byUsername[ui.Username] { return nil, fmt.Errorf("duplicate username found; username: %q", ui.Username) } + authToken := getAuthToken(ui.BearerToken, ui.Username, ui.Password) + if byAuthToken[authToken] != nil { + return nil, fmt.Errorf("duplicate auth token found for bearer_token=%q, username=%q: %q", authToken, ui.BearerToken, ui.Username) + } if len(ui.URLPrefix) > 0 { urlPrefix, err := sanitizeURLPrefix(ui.URLPrefix) if err != nil { @@ -176,10 +193,29 @@ func parseAuthConfig(data []byte) (map[string]*UserInfo, error) { if len(ui.URLMap) == 0 && len(ui.URLPrefix) == 0 { return nil, fmt.Errorf("missing `url_prefix`") } - ui.requests = metrics.GetOrCreateCounter(fmt.Sprintf(`vmauth_user_requests_total{username=%q}`, ui.Username)) - m[ui.Username] = ui + if ui.BearerToken != "" { + if ui.Password != "" { + return nil, fmt.Errorf("password shouldn't be set for bearer_token %q", ui.BearerToken) + } + ui.requests = metrics.GetOrCreateCounter(`vmauth_user_requests_total{username="bearer_token"}`) + byBearerToken[ui.BearerToken] = true + } + if ui.Username != "" { + ui.requests = metrics.GetOrCreateCounter(fmt.Sprintf(`vmauth_user_requests_total{username=%q}`, ui.Username)) + byUsername[ui.Username] = true + } + byAuthToken[authToken] = ui } - return m, nil + return byAuthToken, nil +} + +func getAuthToken(bearerToken, username, password string) string { + if bearerToken != "" { + return "Bearer " + bearerToken + } + token := username + ":" + password + token64 := base64.StdEncoding.EncodeToString([]byte(token)) + return "Basic " + token64 } func sanitizeURLPrefix(urlPrefix string) (string, error) { diff --git a/app/vmauth/auth_config_test.go b/app/vmauth/auth_config_test.go index 0033b2b42..759136e5f 100644 --- a/app/vmauth/auth_config_test.go +++ b/app/vmauth/auth_config_test.go @@ -56,6 +56,22 @@ users: url_prefix: http:///bar `) + // Username and bearer_token in a single config + f(` +users: +- username: foo + bearer_token: bbb + url_prefix: http://foo.bar +`) + + // Bearer_token and password in a single config + f(` +users: +- password: foo + bearer_token: bbb + url_prefix: http://foo.bar +`) + // Duplicate users f(` users: @@ -67,6 +83,17 @@ users: url_prefix: https://sss.sss `) + // Duplicate bearer_tokens + f(` +users: +- bearer_token: foo + url_prefix: http://foo.bar +- username: bar + url_prefix: http://xxx.yyy +- bearer_token: foo + url_prefix: https://sss.sss +`) + // Missing url_prefix in url_map f(` users: @@ -113,7 +140,7 @@ users: password: bar url_prefix: http://aaa:343/bbb `, map[string]*UserInfo{ - "foo": { + getAuthToken("", "foo", "bar"): { Username: "foo", Password: "bar", URLPrefix: "http://aaa:343/bbb", @@ -128,11 +155,11 @@ users: - username: bar url_prefix: https://bar/x/// `, map[string]*UserInfo{ - "foo": { + getAuthToken("", "foo", ""): { Username: "foo", URLPrefix: "http://foo", }, - "bar": { + getAuthToken("", "bar", ""): { Username: "bar", URLPrefix: "https://bar/x", }, @@ -141,15 +168,15 @@ users: // non-empty URLMap f(` users: -- username: foo +- bearer_token: foo url_map: - src_paths: ["/api/v1/query","/api/v1/query_range","/api/v1/label/[^./]+/.+"] url_prefix: http://vmselect/select/0/prometheus - src_paths: ["/api/v1/write"] url_prefix: http://vminsert/insert/0/prometheus `, map[string]*UserInfo{ - "foo": { - Username: "foo", + getAuthToken("foo", "", ""): { + BearerToken: "foo", URLMap: []URLMap{ { SrcPaths: getSrcPaths([]string{"/api/v1/query", "/api/v1/query_range", "/api/v1/label/[^./]+/.+"}), diff --git a/app/vmauth/main.go b/app/vmauth/main.go index 282467347..23c22d282 100644 --- a/app/vmauth/main.go +++ b/app/vmauth/main.go @@ -47,16 +47,16 @@ func main() { } func requestHandler(w http.ResponseWriter, r *http.Request) bool { - username, password, ok := r.BasicAuth() - if !ok { + authToken := r.Header.Get("Authorization") + if authToken == "" { w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`) - http.Error(w, "missing `Authorization: Basic *` header", http.StatusUnauthorized) + http.Error(w, "missing `Authorization` request header", http.StatusUnauthorized) return true } ac := authConfig.Load().(map[string]*UserInfo) - ui := ac[username] - if ui == nil || ui.Password != password { - httpserver.Errorf(w, r, "cannot find the provided username %q or password in config", username) + ui := ac[authToken] + if ui == nil { + httpserver.Errorf(w, r, "cannot find the provided auth token %q in config", authToken) return true } ui.requests.Inc() diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 86c60f132..9030fef97 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -8,6 +8,7 @@ * FEATURE: vmagent: add support for `authorization` section in `-promscrape.config` in the same way as [Prometheus 2.26 does](https://github.com/prometheus/prometheus/pull/8512). * FEATURE: vmagent: reduce memory usage when `-remoteWrite.queues` is set to a big value. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1167). * FEATURE: vmagent: add AWS IAM roles for tasks support for EC2 service discovery according to [these docs](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-iam-roles.html). +FEATURE: vmauth: add support for authorization via [bearer token](https://swagger.io/docs/specification/authentication/bearer-authentication/). See [the docs](https://victoriametrics.github.io/vmauth.html#auth-config) for details. * BUGFIX: vmagent: properly discovery targets if multiple namespace selectors are put inside `kubernetes_sd_config`. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1170). * BUGFIX: properly generate filename for `*.tar.gz` archive inside `_checksums.txt` file posted at [releases page](https://github.com/VictoriaMetrics/VictoriaMetrics/releases). See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1171).