basic OAuth2 support for remoteWrite and scrape targets (#1316)

* adds OAuth2 support for remoteWrite and scrapping

* adds tests
changes init
This commit is contained in:
Nikolay 2021-05-22 16:20:18 +03:00 committed by GitHub
parent e05dd475f0
commit 5b8176c68e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 396 additions and 10 deletions

View file

@ -44,6 +44,17 @@ var (
"If multiple args are set, then they are applied independently for the corresponding -remoteWrite.url")
bearerToken = flagutil.NewArray("remoteWrite.bearerToken", "Optional bearer auth token to use for -remoteWrite.url. "+
"If multiple args are set, then they are applied independently for the corresponding -remoteWrite.url")
clientID = flagutil.NewArray("remoteWrite.oauth2.clientID", "Optional OAuth2 clientID to use for -remoteWrite.url."+
"If multiple args are set, then they are applied independently for the corresponding -remoteWrite.url")
clientSecret = flagutil.NewArray("remoteWrite.oauth2.clientSecret", "Optional OAuth2 clientSecret to use for -remoteWrite.url."+
"If multiple args are set, then they are applied independently for the corresponding -remoteWrite.url")
clientSecretFile = flagutil.NewArray("remoteWrite.oauth2.clientSecretFile", "Optional OAuth2 clientSecretFile to use for -remoteWrite.url."+
"If multiple args are set, then they are applied independently for the corresponding -remoteWrite.url")
tokenURL = flagutil.NewArray("remoteWrite.oauth2.tokenUrl", "Optional OAuth2 token url to use for -remoteWrite.url."+
"If multiple args are set, then they are applied independently for the corresponding -remoteWrite.url")
oAuth2Scopes = flagutil.NewArray("remoteWrite.oauth2.scopes", "Optional OAuth2 scopes to use for -remoteWrite.url."+
"If multiple args are set, then they are applied independently for the corresponding -remoteWrite.url")
)
type client struct {
@ -53,6 +64,8 @@ type client struct {
fq *persistentqueue.FastQueue
hc *http.Client
authCfg *promauth.Config
rl rateLimiter
bytesSent *metrics.Counter
@ -72,6 +85,7 @@ func newClient(argIdx int, remoteWriteURL, sanitizedURL string, fq *persistentqu
if err != nil {
logger.Panicf("FATAL: cannot initialize TLS config: %s", err)
}
tr := &http.Transport{
Dial: statDial,
TLSClientConfig: tlsCfg,
@ -108,10 +122,18 @@ func newClient(argIdx int, remoteWriteURL, sanitizedURL string, fq *persistentqu
}
authHeader = "Bearer " + token
}
authCfg, err := getAuthConfig(argIdx)
if err != nil {
logger.Fatalf("FATAL: cannot create OAuth2 config for remoteWrite idx: %d, err: %s", argIdx, err)
}
if authCfg != nil && authHeader != "" {
logger.Fatalf("`-remoteWrite.bearerToken`=%q or `-remoteWrite.basicAuth.* cannot be set when `-remoteWrite.oauth2.*` flags are set", token)
}
c := &client{
sanitizedURL: sanitizedURL,
remoteWriteURL: remoteWriteURL,
authHeader: authHeader,
authCfg: authCfg,
fq: fq,
hc: &http.Client{
Transport: tr,
@ -160,7 +182,7 @@ func getTLSConfig(argIdx int) (*tls.Config, error) {
if c.CAFile == "" && c.CertFile == "" && c.KeyFile == "" && c.ServerName == "" && !c.InsecureSkipVerify {
return nil, nil
}
cfg, err := promauth.NewConfig(".", nil, nil, "", "", c)
cfg, err := promauth.NewConfig(".", nil, nil, "", "", nil, c)
if err != nil {
return nil, fmt.Errorf("cannot populate TLS config: %w", err)
}
@ -168,6 +190,25 @@ func getTLSConfig(argIdx int) (*tls.Config, error) {
return tlsCfg, nil
}
func getAuthConfig(argIdx int) (*promauth.Config, error) {
oAuth2Cfg := &promauth.OAuth2Config{
ClientID: clientID.GetOptionalArg(argIdx),
ClientSecret: clientSecret.GetOptionalArg(argIdx),
ClientSecretFile: clientSecretFile.GetOptionalArg(argIdx),
TokenURL: tokenURL.GetOptionalArg(argIdx),
Scopes: strings.Split(oAuth2Scopes.GetOptionalArg(argIdx), ";"),
}
if oAuth2Cfg.ClientSecretFile == "" && oAuth2Cfg.ClientSecret == "" {
return nil, nil
}
authCfg, err := promauth.NewConfig("", nil, nil, "", "", oAuth2Cfg, nil)
if err != nil {
return nil, fmt.Errorf("cannot populate OAuth2 config for remoteWrite idx: %d, err: %w", argIdx, err)
}
return authCfg, nil
}
func (c *client) runWorker() {
var ok bool
var block []byte
@ -229,6 +270,11 @@ again:
if c.authHeader != "" {
req.Header.Set("Authorization", c.authHeader)
}
// add oauth2 header on best effort.
// remote storage may return error with incorrect authorization.
if c.authCfg != nil {
req.Header.Set("Authorization", c.authCfg.GetAuthHeader())
}
startTime := time.Now()
resp, err := c.hc.Do(req)

2
go.mod
View file

@ -31,7 +31,7 @@ require (
github.com/valyala/gozstd v1.10.0
github.com/valyala/histogram v1.1.2
github.com/valyala/quicktemplate v1.6.3
golang.org/x/net v0.0.0-20210510120150-4163338589ed
golang.org/x/net v0.0.0-20210520170846-37e1c6afe023
golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c
golang.org/x/sys v0.0.0-20210514084401-e8d321eab015
google.golang.org/api v0.47.0

4
go.sum
View file

@ -533,7 +533,7 @@ github.com/influxdata/flux v0.113.0/go.mod h1:3TJtvbm/Kwuo5/PEo5P6HUzwVg4bXWkb2w
github.com/influxdata/httprouter v1.3.1-0.20191122104820-ee83e2772f69/go.mod h1:pwymjR6SrP3gD3pRj9RJwdl1j5s3doEEV8gS4X9qSzA=
github.com/influxdata/influxdb v1.8.0/go.mod h1:SIzcnsjaHRFpmlxpJ4S3NT64qtEKYweNTUMb/vh0OMQ=
github.com/influxdata/influxdb v1.8.3/go.mod h1:JugdFhsvvI8gadxOI6noqNeeBHvWNTbfYGtiAn+2jhI=
github.com/influxdata/influxdb v1.9.0 h1:9z/aRmTpWT1rIm4EN+qTJTZqgEdLGZ4xRMgvA276UEA=
github.com/influxdata/influxdb v1.9.0 h1:KefL3i2JdNgsZwKRlHkkV+sYs4ejJ+aGF6glBUoKKio=
github.com/influxdata/influxdb v1.9.0/go.mod h1:UEe3MeD9AaP5rlPIes102IhYua3FhIWZuOXNHxDjSrI=
github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo=
github.com/influxdata/influxql v1.1.0/go.mod h1:KpVI7okXjK6PRi3Z5B+mtKZli+R1DnZgb3N+tzevNgo=
@ -1033,6 +1033,8 @@ golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96b
golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210510120150-4163338589ed h1:p9UgmWI9wKpfYmgaV/IZKGdXc5qEK45tDwwwDyjS26I=
golang.org/x/net v0.0.0-20210510120150-4163338589ed/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210520170846-37e1c6afe023 h1:ADo5wSpq2gqaCGQWzk7S5vd//0iyyLeAratkEoG5dLE=
golang.org/x/net v0.0.0-20210520170846-37e1c6afe023/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=

View file

@ -2,6 +2,7 @@ package promauth
import (
"bytes"
"context"
"crypto/tls"
"crypto/x509"
"encoding/base64"
@ -11,6 +12,8 @@ import (
"github.com/VictoriaMetrics/VictoriaMetrics/lib/fasttime"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
"golang.org/x/oauth2"
"golang.org/x/oauth2/clientcredentials"
)
// TLSConfig represents TLS config.
@ -46,6 +49,7 @@ type HTTPClientConfig struct {
BasicAuth *BasicAuthConfig `yaml:"basic_auth,omitempty"`
BearerToken string `yaml:"bearer_token,omitempty"`
BearerTokenFile string `yaml:"bearer_token_file,omitempty"`
OAuth2 *OAuth2Config `yaml:"oauth2,omitempty"`
TLSConfig *TLSConfig `yaml:"tls_config,omitempty"`
}
@ -58,6 +62,69 @@ type ProxyClientConfig struct {
TLSConfig *TLSConfig `yaml:"proxy_tls_config,omitempty"`
}
// OAuth2Config represent OAuth2 configuration
type OAuth2Config struct {
ClientID string `yaml:"client_id"`
ClientSecretFile string `yaml:"client_secret_file"`
Scopes []string `yaml:"scopes"`
TokenURL string `yaml:"token_url"`
// mu guards tokenSource and client Secret
mu sync.Mutex
ClientSecret string `yaml:"client_secret"`
tokenSource oauth2.TokenSource
}
func (o *OAuth2Config) refreshTokenSourceLocked() {
cfg := clientcredentials.Config{
ClientID: o.ClientID,
ClientSecret: o.ClientSecret,
TokenURL: o.TokenURL,
Scopes: o.Scopes,
}
o.tokenSource = cfg.TokenSource(context.Background())
}
// validate checks given configs.
func (o *OAuth2Config) validate() error {
if o.TokenURL == "" {
return fmt.Errorf("token url cannot be empty")
}
if o.ClientSecret == "" && o.ClientSecretFile == "" {
return fmt.Errorf("ClientSecret or ClientSecretFile must be set")
}
if o.ClientSecret != "" && o.ClientSecretFile != "" {
return fmt.Errorf("only one option can be set ClientSecret or ClientSecretFile, provided both")
}
return nil
}
func (o *OAuth2Config) getAuthHeader() (string, error) {
var needUpdate bool
if o.ClientSecretFile != "" {
newSecret, err := readPasswordFromFile(o.ClientSecretFile)
if err != nil {
return "", fmt.Errorf("cannot read OAuth2 config file with path: %s, err: %w", o.ClientSecretFile, err)
}
o.mu.Lock()
if o.ClientSecret != newSecret {
o.ClientSecret = newSecret
needUpdate = true
}
o.mu.Unlock()
}
o.mu.Lock()
defer o.mu.Unlock()
if needUpdate {
o.refreshTokenSourceLocked()
}
t, err := o.tokenSource.Token()
if err != nil {
return "", fmt.Errorf("cannot fetch token for OAuth2 client: %w", err)
}
return t.Type() + " " + t.AccessToken, nil
}
// Config is auth config.
type Config struct {
// Optional TLS config
@ -131,16 +198,16 @@ func (ac *Config) NewTLSConfig() *tls.Config {
// NewConfig creates auth config for the given hcc.
func (hcc *HTTPClientConfig) NewConfig(baseDir string) (*Config, error) {
return NewConfig(baseDir, hcc.Authorization, hcc.BasicAuth, hcc.BearerToken, hcc.BearerTokenFile, hcc.TLSConfig)
return NewConfig(baseDir, hcc.Authorization, hcc.BasicAuth, hcc.BearerToken, hcc.BearerTokenFile, hcc.OAuth2, hcc.TLSConfig)
}
// NewConfig creates auth config for the given pcc.
func (pcc *ProxyClientConfig) NewConfig(baseDir string) (*Config, error) {
return NewConfig(baseDir, pcc.Authorization, pcc.BasicAuth, pcc.BearerToken, pcc.BearerTokenFile, pcc.TLSConfig)
return NewConfig(baseDir, pcc.Authorization, pcc.BasicAuth, pcc.BearerToken, pcc.BearerTokenFile, nil, pcc.TLSConfig)
}
// NewConfig creates auth config from the given args.
func NewConfig(baseDir string, az *Authorization, basicAuth *BasicAuthConfig, bearerToken, bearerTokenFile string, tlsConfig *TLSConfig) (*Config, error) {
func NewConfig(baseDir string, az *Authorization, basicAuth *BasicAuthConfig, bearerToken, bearerTokenFile string, oauth *OAuth2Config, tlsConfig *TLSConfig) (*Config, error) {
var getAuthHeader func() string
authDigest := ""
if az != nil {
@ -230,6 +297,30 @@ func NewConfig(baseDir string, az *Authorization, basicAuth *BasicAuthConfig, be
}
authDigest = fmt.Sprintf("bearer(token=%q)", bearerToken)
}
if oauth != nil {
if getAuthHeader != nil {
return nil, fmt.Errorf("cannot simultaneously use `authorization`, `basic_auth, `bearer_token` and `ouath2`")
}
if err := oauth.validate(); err != nil {
return nil, err
}
if oauth.ClientSecretFile != "" {
secret, err := readPasswordFromFile(oauth.ClientSecretFile)
if err != nil {
return nil, err
}
oauth.ClientSecret = secret
}
oauth.refreshTokenSourceLocked()
getAuthHeader = func() string {
h, err := oauth.getAuthHeader()
if err != nil {
logger.Errorf("cannot get OAuth2 header: %s", err)
return ""
}
return h
}
}
var tlsRootCA *x509.CertPool
var tlsCertificate *tls.Certificate
tlsServerName := ""

125
lib/promauth/config_test.go Normal file
View file

@ -0,0 +1,125 @@
package promauth
import (
"net/http"
"net/http/httptest"
"testing"
)
func TestNewConfig(t *testing.T) {
type args struct {
baseDir string
az *Authorization
basicAuth *BasicAuthConfig
bearerToken string
bearerTokenFile string
oauth *OAuth2Config
tlsConfig *TLSConfig
}
tests := []struct {
name string
args args
wantErr bool
}{
{
name: "OAuth2 config",
args: args{
oauth: &OAuth2Config{
ClientID: "some-id",
ClientSecret: "some-secret",
TokenURL: "http://localhost:8511",
},
},
},
{
name: "OAuth2 config with file",
args: args{
oauth: &OAuth2Config{
ClientID: "some-id",
ClientSecretFile: "testdata/test_secretfile.txt",
TokenURL: "http://localhost:8511",
},
},
},
{
name: "OAuth2 want err",
args: args{
oauth: &OAuth2Config{
ClientID: "some-id",
ClientSecret: "some-secret",
ClientSecretFile: "testdata/test_secretfile.txt",
TokenURL: "http://localhost:8511",
},
},
wantErr: true,
},
{
name: "basic Auth config",
args: args{
basicAuth: &BasicAuthConfig{
Username: "user",
Password: "password",
},
},
},
{
name: "basic Auth config with file",
args: args{
basicAuth: &BasicAuthConfig{
Username: "user",
PasswordFile: "testdata/test_secretfile.txt",
},
},
},
{
name: "want Authorization",
args: args{
az: &Authorization{
Type: "Bearer ",
Credentials: "Value",
},
},
},
{
name: "token file",
args: args{
bearerTokenFile: "testdata/test_secretfile.txt",
},
},
{
name: "token with tls",
args: args{
bearerToken: "some-token",
tlsConfig: &TLSConfig{
InsecureSkipVerify: true,
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.args.oauth != nil {
r := http.NewServeMux()
r.HandleFunc("/", func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`{"access_token":"some-token","token_type": "Bearer "}`))
})
mock := httptest.NewServer(r)
tt.args.oauth.TokenURL = mock.URL
}
got, err := NewConfig(tt.args.baseDir, tt.args.az, tt.args.basicAuth, tt.args.bearerToken, tt.args.bearerTokenFile, tt.args.oauth, tt.args.tlsConfig)
if (err != nil) != tt.wantErr {
t.Errorf("NewConfig() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != nil {
if ah := got.GetAuthHeader(); ah == "" {
t.Fatalf("unexpected empty auth header")
}
}
})
}
}

View file

@ -0,0 +1 @@
secret-content

View file

@ -50,7 +50,7 @@ func newAPIConfig(sdc *SDConfig, baseDir string) (*apiConfig, error) {
}
token = ""
}
ac, err := promauth.NewConfig(baseDir, nil, ba, token, "", sdc.TLSConfig)
ac, err := promauth.NewConfig(baseDir, nil, ba, token, "", nil, sdc.TLSConfig)
if err != nil {
return nil, fmt.Errorf("cannot parse auth config: %w", err)
}

View file

@ -43,7 +43,7 @@ func newAPIConfig(sdc *SDConfig, baseDir string, swcFunc ScrapeWorkConstructorFu
tlsConfig := promauth.TLSConfig{
CAFile: "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt",
}
acNew, err := promauth.NewConfig(".", nil, nil, "", "/var/run/secrets/kubernetes.io/serviceaccount/token", &tlsConfig)
acNew, err := promauth.NewConfig(".", nil, nil, "", "/var/run/secrets/kubernetes.io/serviceaccount/token", nil, &tlsConfig)
if err != nil {
return nil, fmt.Errorf("cannot initialize service account auth: %w; probably, `kubernetes_sd_config->api_server` is missing in Prometheus configs?", err)
}

View file

@ -81,7 +81,7 @@ func newAPIConfig(sdc *SDConfig, baseDir string) (*apiConfig, error) {
port: sdc.Port,
}
if sdc.TLSConfig != nil {
ac, err := promauth.NewConfig(baseDir, nil, nil, "", "", sdc.TLSConfig)
ac, err := promauth.NewConfig(baseDir, nil, nil, "", "", nil, sdc.TLSConfig)
if err != nil {
return nil, err
}

View file

@ -0,0 +1,120 @@
// Copyright 2014 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package clientcredentials implements the OAuth2.0 "client credentials" token flow,
// also known as the "two-legged OAuth 2.0".
//
// This should be used when the client is acting on its own behalf or when the client
// is the resource owner. It may also be used when requesting access to protected
// resources based on an authorization previously arranged with the authorization
// server.
//
// See https://tools.ietf.org/html/rfc6749#section-4.4
package clientcredentials // import "golang.org/x/oauth2/clientcredentials"
import (
"context"
"fmt"
"net/http"
"net/url"
"strings"
"golang.org/x/oauth2"
"golang.org/x/oauth2/internal"
)
// Config describes a 2-legged OAuth2 flow, with both the
// client application information and the server's endpoint URLs.
type Config struct {
// ClientID is the application's ID.
ClientID string
// ClientSecret is the application's secret.
ClientSecret string
// TokenURL is the resource server's token endpoint
// URL. This is a constant specific to each server.
TokenURL string
// Scope specifies optional requested permissions.
Scopes []string
// EndpointParams specifies additional parameters for requests to the token endpoint.
EndpointParams url.Values
// AuthStyle optionally specifies how the endpoint wants the
// client ID & client secret sent. The zero value means to
// auto-detect.
AuthStyle oauth2.AuthStyle
}
// Token uses client credentials to retrieve a token.
//
// The provided context optionally controls which HTTP client is used. See the oauth2.HTTPClient variable.
func (c *Config) Token(ctx context.Context) (*oauth2.Token, error) {
return c.TokenSource(ctx).Token()
}
// Client returns an HTTP client using the provided token.
// The token will auto-refresh as necessary.
//
// The provided context optionally controls which HTTP client
// is returned. See the oauth2.HTTPClient variable.
//
// The returned Client and its Transport should not be modified.
func (c *Config) Client(ctx context.Context) *http.Client {
return oauth2.NewClient(ctx, c.TokenSource(ctx))
}
// TokenSource returns a TokenSource that returns t until t expires,
// automatically refreshing it as necessary using the provided context and the
// client ID and client secret.
//
// Most users will use Config.Client instead.
func (c *Config) TokenSource(ctx context.Context) oauth2.TokenSource {
source := &tokenSource{
ctx: ctx,
conf: c,
}
return oauth2.ReuseTokenSource(nil, source)
}
type tokenSource struct {
ctx context.Context
conf *Config
}
// Token refreshes the token by using a new client credentials request.
// tokens received this way do not include a refresh token
func (c *tokenSource) Token() (*oauth2.Token, error) {
v := url.Values{
"grant_type": {"client_credentials"},
}
if len(c.conf.Scopes) > 0 {
v.Set("scope", strings.Join(c.conf.Scopes, " "))
}
for k, p := range c.conf.EndpointParams {
// Allow grant_type to be overridden to allow interoperability with
// non-compliant implementations.
if _, ok := v[k]; ok && k != "grant_type" {
return nil, fmt.Errorf("oauth2: cannot overwrite parameter %q", k)
}
v[k] = p
}
tk, err := internal.RetrieveToken(c.ctx, c.conf.ClientID, c.conf.ClientSecret, c.conf.TokenURL, v, internal.AuthStyle(c.conf.AuthStyle))
if err != nil {
if rErr, ok := err.(*internal.RetrieveError); ok {
return nil, (*oauth2.RetrieveError)(rErr)
}
return nil, err
}
t := &oauth2.Token{
AccessToken: tk.AccessToken,
TokenType: tk.TokenType,
RefreshToken: tk.RefreshToken,
Expiry: tk.Expiry,
}
return t.WithExtra(tk.Raw), nil
}

3
vendor/modules.txt vendored
View file

@ -235,7 +235,7 @@ golang.org/x/lint/golint
# golang.org/x/mod v0.4.2
golang.org/x/mod/module
golang.org/x/mod/semver
# golang.org/x/net v0.0.0-20210510120150-4163338589ed
# golang.org/x/net v0.0.0-20210520170846-37e1c6afe023
## explicit
golang.org/x/net/context
golang.org/x/net/context/ctxhttp
@ -251,6 +251,7 @@ golang.org/x/net/trace
## explicit
golang.org/x/oauth2
golang.org/x/oauth2/authhandler
golang.org/x/oauth2/clientcredentials
golang.org/x/oauth2/google
golang.org/x/oauth2/google/internal/externalaccount
golang.org/x/oauth2/internal