app/vmauth: add support for authorization via Authorization: Bearer <token>

This commit is contained in:
Aliaksandr Valialkin 2021-04-02 22:14:53 +03:00
parent b88feb631e
commit b1d0028e79
5 changed files with 98 additions and 30 deletions

View File

@ -36,11 +36,15 @@ Auth config is represented in the following simple `yml` format:
# Usernames must be unique. # Usernames must be unique.
users: 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. # The user for querying local single-node VictoriaMetrics.
# All the requests to http://vmauth:8427 with the given Basic Auth (username:password) # All the requests to http://vmauth:8427 with the given Basic Auth (username:password)
# will be routed to http://localhost:8428 . # will be proxied to http://localhost:8428 .
# For example, http://vmauth:8427/api/v1/query is routed to http://localhost:8428/api/v1/query # For example, http://vmauth:8427/api/v1/query is proxied to http://localhost:8428/api/v1/query
- username: "local-single-node" - username: "local-single-node"
password: "***" password: "***"
url_prefix: "http://localhost:8428" url_prefix: "http://localhost:8428"
@ -48,8 +52,8 @@ users:
# The user for querying account 123 in VictoriaMetrics cluster # The user for querying account 123 in VictoriaMetrics cluster
# See https://victoriametrics.github.io/Cluster-VictoriaMetrics.html#url-format # See https://victoriametrics.github.io/Cluster-VictoriaMetrics.html#url-format
# All the requests to http://vmauth:8427 with the given Basic Auth (username:password) # All the requests to http://vmauth:8427 with the given Basic Auth (username:password)
# will be routed to http://vmselect:8481/select/123/prometheus . # will be proxied 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 # 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" - username: "cluster-select-account-123"
password: "***" password: "***"
url_prefix: "http://vmselect:8481/select/123/prometheus" 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 # The user for inserting Prometheus data into VictoriaMetrics cluster under account 42
# See https://victoriametrics.github.io/Cluster-VictoriaMetrics.html#url-format # See https://victoriametrics.github.io/Cluster-VictoriaMetrics.html#url-format
# All the requests to http://vmauth:8427 with the given Basic Auth (username:password) # All the requests to http://vmauth:8427 with the given Basic Auth (username:password)
# will be routed to http://vminsert:8480/insert/42/prometheus . # will be proxied 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 # 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" - username: "cluster-insert-account-42"
password: "***" password: "***"
url_prefix: "http://vminsert:8480/insert/42/prometheus" url_prefix: "http://vminsert:8480/insert/42/prometheus"
@ -66,9 +70,9 @@ users:
# A single user for querying and inserting data: # A single user for querying and inserting data:
# - Requests to http://vmauth:8427/api/v1/query, http://vmauth:8427/api/v1/query_range # - Requests to http://vmauth:8427/api/v1/query, http://vmauth:8427/api/v1/query_range
# and http://vmauth:8427/api/v1/label/<label_name>/values are routed to http://vmselect:8481/select/42/prometheus. # and http://vmauth:8427/api/v1/label/<label_name>/values are proxied 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 # 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 routed to http://vminsert:8480/insert/42/prometheus/api/v1/write # - Requests to http://vmauth:8427/api/v1/write are proxied to http://vminsert:8480/insert/42/prometheus/api/v1/write
- username: "foobar" - username: "foobar"
url_map: url_map:
- src_paths: ["/api/v1/query", "/api/v1/query_range", "/api/v1/label/[^/]+/values"] - src_paths: ["/api/v1/query", "/api/v1/query_range", "/api/v1/label/[^/]+/values"]

View File

@ -1,6 +1,7 @@
package main package main
import ( import (
"encoding/base64"
"flag" "flag"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
@ -29,10 +30,11 @@ type AuthConfig struct {
// UserInfo is user information read from authConfigPath // UserInfo is user information read from authConfigPath
type UserInfo struct { type UserInfo struct {
Username string `yaml:"username"` BearerToken string `yaml:"bearer_token"`
Password string `yaml:"password"` Username string `yaml:"username"`
URLPrefix string `yaml:"url_prefix"` Password string `yaml:"password"`
URLMap []URLMap `yaml:"url_map"` URLPrefix string `yaml:"url_prefix"`
URLMap []URLMap `yaml:"url_map"`
requests *metrics.Counter requests *metrics.Counter
} }
@ -150,12 +152,27 @@ func parseAuthConfig(data []byte) (map[string]*UserInfo, error) {
if len(uis) == 0 { if len(uis) == 0 {
return nil, fmt.Errorf("`users` section cannot be empty in AuthConfig") 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 { for i := range uis {
ui := &uis[i] 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) 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 { if len(ui.URLPrefix) > 0 {
urlPrefix, err := sanitizeURLPrefix(ui.URLPrefix) urlPrefix, err := sanitizeURLPrefix(ui.URLPrefix)
if err != nil { if err != nil {
@ -176,10 +193,29 @@ func parseAuthConfig(data []byte) (map[string]*UserInfo, error) {
if len(ui.URLMap) == 0 && len(ui.URLPrefix) == 0 { if len(ui.URLMap) == 0 && len(ui.URLPrefix) == 0 {
return nil, fmt.Errorf("missing `url_prefix`") return nil, fmt.Errorf("missing `url_prefix`")
} }
ui.requests = metrics.GetOrCreateCounter(fmt.Sprintf(`vmauth_user_requests_total{username=%q}`, ui.Username)) if ui.BearerToken != "" {
m[ui.Username] = ui 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) { func sanitizeURLPrefix(urlPrefix string) (string, error) {

View File

@ -56,6 +56,22 @@ users:
url_prefix: http:///bar 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 // Duplicate users
f(` f(`
users: users:
@ -67,6 +83,17 @@ users:
url_prefix: https://sss.sss 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 // Missing url_prefix in url_map
f(` f(`
users: users:
@ -113,7 +140,7 @@ users:
password: bar password: bar
url_prefix: http://aaa:343/bbb url_prefix: http://aaa:343/bbb
`, map[string]*UserInfo{ `, map[string]*UserInfo{
"foo": { getAuthToken("", "foo", "bar"): {
Username: "foo", Username: "foo",
Password: "bar", Password: "bar",
URLPrefix: "http://aaa:343/bbb", URLPrefix: "http://aaa:343/bbb",
@ -128,11 +155,11 @@ users:
- username: bar - username: bar
url_prefix: https://bar/x/// url_prefix: https://bar/x///
`, map[string]*UserInfo{ `, map[string]*UserInfo{
"foo": { getAuthToken("", "foo", ""): {
Username: "foo", Username: "foo",
URLPrefix: "http://foo", URLPrefix: "http://foo",
}, },
"bar": { getAuthToken("", "bar", ""): {
Username: "bar", Username: "bar",
URLPrefix: "https://bar/x", URLPrefix: "https://bar/x",
}, },
@ -141,15 +168,15 @@ users:
// non-empty URLMap // non-empty URLMap
f(` f(`
users: users:
- username: foo - bearer_token: foo
url_map: url_map:
- src_paths: ["/api/v1/query","/api/v1/query_range","/api/v1/label/[^./]+/.+"] - src_paths: ["/api/v1/query","/api/v1/query_range","/api/v1/label/[^./]+/.+"]
url_prefix: http://vmselect/select/0/prometheus url_prefix: http://vmselect/select/0/prometheus
- src_paths: ["/api/v1/write"] - src_paths: ["/api/v1/write"]
url_prefix: http://vminsert/insert/0/prometheus url_prefix: http://vminsert/insert/0/prometheus
`, map[string]*UserInfo{ `, map[string]*UserInfo{
"foo": { getAuthToken("foo", "", ""): {
Username: "foo", BearerToken: "foo",
URLMap: []URLMap{ URLMap: []URLMap{
{ {
SrcPaths: getSrcPaths([]string{"/api/v1/query", "/api/v1/query_range", "/api/v1/label/[^./]+/.+"}), SrcPaths: getSrcPaths([]string{"/api/v1/query", "/api/v1/query_range", "/api/v1/label/[^./]+/.+"}),

View File

@ -47,16 +47,16 @@ func main() {
} }
func requestHandler(w http.ResponseWriter, r *http.Request) bool { func requestHandler(w http.ResponseWriter, r *http.Request) bool {
username, password, ok := r.BasicAuth() authToken := r.Header.Get("Authorization")
if !ok { if authToken == "" {
w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`) 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 return true
} }
ac := authConfig.Load().(map[string]*UserInfo) ac := authConfig.Load().(map[string]*UserInfo)
ui := ac[username] ui := ac[authToken]
if ui == nil || ui.Password != password { if ui == nil {
httpserver.Errorf(w, r, "cannot find the provided username %q or password in config", username) httpserver.Errorf(w, r, "cannot find the provided auth token %q in config", authToken)
return true return true
} }
ui.requests.Inc() ui.requests.Inc()

View File

@ -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: 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: 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: 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: 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). * 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).