From 78dddfb98f78fbedf115dba291378a8237adf8dd Mon Sep 17 00:00:00 2001 From: Aliaksandr Valialkin Date: Sat, 22 May 2021 17:59:23 +0300 Subject: [PATCH] lib/promauth: follow-up after 5b8176c68eabba0bb47835e65e9334811dd8e8ea --- app/vmagent/README.md | 21 +++++ app/vmagent/remotewrite/client.go | 114 ++++++++++------------- docs/CHANGELOG.md | 2 + docs/vmagent.md | 21 +++++ go.sum | 4 +- lib/promauth/config.go | 144 ++++++++++++++++++------------ lib/promauth/config_test.go | 24 +++-- 7 files changed, 194 insertions(+), 136 deletions(-) diff --git a/app/vmagent/README.md b/app/vmagent/README.md index 9924d5f18..6477bfb59 100644 --- a/app/vmagent/README.md +++ b/app/vmagent/README.md @@ -669,12 +669,18 @@ See the docs at https://docs.victoriametrics.com/vmagent.html . -remoteWrite.basicAuth.password array Optional basic auth password to use for -remoteWrite.url. If multiple args are set, then they are applied independently for the corresponding -remoteWrite.url Supports an array of values separated by comma or specified via multiple flags. + -remoteWrite.basicAuth.passwordFile array + Optional path to basic auth password to use for -remoteWrite.url. The file is re-read every second. If multiple args are set, then they are applied independently for the corresponding -remoteWrite.url + Supports an array of values separated by comma or specified via multiple flags. -remoteWrite.basicAuth.username array Optional basic auth username to use for -remoteWrite.url. If multiple args are set, then they are applied independently for the corresponding -remoteWrite.url Supports an array of values separated by comma or specified via multiple flags. -remoteWrite.bearerToken array Optional bearer auth token to use for -remoteWrite.url. If multiple args are set, then they are applied independently for the corresponding -remoteWrite.url Supports an array of values separated by comma or specified via multiple flags. + -remoteWrite.bearerTokenFile array + Optional path to bearer token file to use for -remoteWrite.url. The token is re-read from the file every second. If multiple args are set, then they are applied independently for the corresponding -remoteWrite.url + Supports an array of values separated by comma or specified via multiple flags. -remoteWrite.flushInterval duration Interval for flushing the data to remote storage. This option takes effect only when less than 10K data points per second are pushed to -remoteWrite.url (default 1s) -remoteWrite.label array @@ -690,6 +696,21 @@ See the docs at https://docs.victoriametrics.com/vmagent.html . Supports the following optional suffixes for size values: KB, MB, GB, KiB, MiB, GiB (default 0) -remoteWrite.maxHourlySeries int The maximum number of unique series vmagent can send to remote storage systems during the last hour. Excess series are logged and dropped. This can be useful for limiting series cardinality. See also -remoteWrite.maxDailySeries + -remoteWrite.oauth2.clientID array + Optional OAuth2 clientID to use for -remoteWrite.url. If multiple args are set, then they are applied independently for the corresponding -remoteWrite.url + Supports an array of values separated by comma or specified via multiple flags. + -remoteWrite.oauth2.clientSecret array + Optional OAuth2 clientSecret to use for -remoteWrite.url. If multiple args are set, then they are applied independently for the corresponding -remoteWrite.url + Supports an array of values separated by comma or specified via multiple flags. + -remoteWrite.oauth2.clientSecretFile array + Optional OAuth2 clientSecretFile to use for -remoteWrite.url. If multiple args are set, then they are applied independently for the corresponding -remoteWrite.url + Supports an array of values separated by comma or specified via multiple flags. + -remoteWrite.oauth2.scopes array + Optional OAuth2 scopes to use for -remoteWrite.url. Scopes must be delimited by ';'. If multiple args are set, then they are applied independently for the corresponding -remoteWrite.url + Supports an array of values separated by comma or specified via multiple flags. + -remoteWrite.oauth2.tokenUrl array + Optional OAuth2 tokenURL to use for -remoteWrite.url. If multiple args are set, then they are applied independently for the corresponding -remoteWrite.url + Supports an array of values separated by comma or specified via multiple flags. -remoteWrite.proxyURL array Optional proxy URL for writing data to -remoteWrite.url. Supported proxies: http, https, socks5. Example: -remoteWrite.proxyURL=socks5://proxy:1234 Supports an array of values separated by comma or specified via multiple flags. diff --git a/app/vmagent/remotewrite/client.go b/app/vmagent/remotewrite/client.go index 6cb87769d..4a6a78cb9 100644 --- a/app/vmagent/remotewrite/client.go +++ b/app/vmagent/remotewrite/client.go @@ -2,8 +2,6 @@ package remotewrite import ( "bytes" - "crypto/tls" - "encoding/base64" "fmt" "io/ioutil" "net/http" @@ -42,25 +40,30 @@ var ( "If multiple args are set, then they are applied independently for the corresponding -remoteWrite.url") basicAuthPassword = flagutil.NewArray("remoteWrite.basicAuth.password", "Optional basic auth password to use for -remoteWrite.url. "+ "If multiple args are set, then they are applied independently for the corresponding -remoteWrite.url") + basicAuthPasswordFile = flagutil.NewArray("remoteWrite.basicAuth.passwordFile", "Optional path to basic auth password to use for -remoteWrite.url. "+ + "The file is re-read every second. "+ + "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") + bearerTokenFile = flagutil.NewArray("remoteWrite.bearerTokenFile", "Optional path to bearer token file to use for -remoteWrite.url. "+ + "The token is re-read from the file every second. "+ + "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."+ + oauth2ClientID = 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."+ + oauth2ClientSecret = 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."+ + oauth2ClientSecretFile = 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."+ + oauth2TokenURL = flagutil.NewArray("remoteWrite.oauth2.tokenUrl", "Optional OAuth2 tokenURL 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."+ + oauth2Scopes = flagutil.NewArray("remoteWrite.oauth2.scopes", "Optional OAuth2 scopes to use for -remoteWrite.url. Scopes must be delimited by ';'. "+ "If multiple args are set, then they are applied independently for the corresponding -remoteWrite.url") ) type client struct { sanitizedURL string remoteWriteURL string - authHeader string fq *persistentqueue.FastQueue hc *http.Client @@ -81,11 +84,11 @@ type client struct { } func newClient(argIdx int, remoteWriteURL, sanitizedURL string, fq *persistentqueue.FastQueue, concurrency int) *client { - tlsCfg, err := getTLSConfig(argIdx) + authCfg, err := getAuthConfig(argIdx) if err != nil { - logger.Panicf("FATAL: cannot initialize TLS config: %s", err) + logger.Panicf("FATAL: cannot initialize auth config: %s", err) } - + tlsCfg := authCfg.NewTLSConfig() tr := &http.Transport{ Dial: statDial, TLSClientConfig: tlsCfg, @@ -106,33 +109,9 @@ func newClient(argIdx int, remoteWriteURL, sanitizedURL string, fq *persistentqu } tr.Proxy = http.ProxyURL(urlProxy) } - authHeader := "" - username := basicAuthUsername.GetOptionalArg(argIdx) - password := basicAuthPassword.GetOptionalArg(argIdx) - if len(username) > 0 || len(password) > 0 { - // See https://en.wikipedia.org/wiki/Basic_access_authentication - token := username + ":" + password - token64 := base64.StdEncoding.EncodeToString([]byte(token)) - authHeader = "Basic " + token64 - } - token := bearerToken.GetOptionalArg(argIdx) - if len(token) > 0 { - if authHeader != "" { - logger.Fatalf("`-remoteWrite.bearerToken`=%q cannot be set when `-remoteWrite.basicAuth.*` flags are set", token) - } - 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{ @@ -171,38 +150,44 @@ func (c *client) MustStop() { logger.Infof("stopped client for -remoteWrite.url=%q", c.sanitizedURL) } -func getTLSConfig(argIdx int) (*tls.Config, error) { - c := &promauth.TLSConfig{ +func getAuthConfig(argIdx int) (*promauth.Config, error) { + username := basicAuthUsername.GetOptionalArg(argIdx) + password := basicAuthPassword.GetOptionalArg(argIdx) + passwordFile := basicAuthPasswordFile.GetOptionalArg(argIdx) + var basicAuthCfg *promauth.BasicAuthConfig + if username != "" || password != "" || passwordFile != "" { + basicAuthCfg = &promauth.BasicAuthConfig{ + Username: username, + Password: password, + PasswordFile: passwordFile, + } + } + + token := bearerToken.GetOptionalArg(argIdx) + tokenFile := bearerTokenFile.GetOptionalArg(argIdx) + + var oauth2Cfg *promauth.OAuth2Config + clientSecret := oauth2ClientSecret.GetOptionalArg(argIdx) + clientSecretFile := oauth2ClientSecretFile.GetOptionalArg(argIdx) + if clientSecretFile != "" || clientSecret != "" { + oauth2Cfg = &promauth.OAuth2Config{ + ClientID: oauth2ClientID.GetOptionalArg(argIdx), + ClientSecret: clientSecret, + ClientSecretFile: clientSecretFile, + TokenURL: oauth2TokenURL.GetOptionalArg(argIdx), + Scopes: strings.Split(oauth2Scopes.GetOptionalArg(argIdx), ";"), + } + } + + tlsCfg := &promauth.TLSConfig{ CAFile: tlsCAFile.GetOptionalArg(argIdx), CertFile: tlsCertFile.GetOptionalArg(argIdx), KeyFile: tlsKeyFile.GetOptionalArg(argIdx), ServerName: tlsServerName.GetOptionalArg(argIdx), InsecureSkipVerify: tlsInsecureSkipVerify.GetOptionalArg(argIdx), } - if c.CAFile == "" && c.CertFile == "" && c.KeyFile == "" && c.ServerName == "" && !c.InsecureSkipVerify { - return nil, nil - } - cfg, err := promauth.NewConfig(".", nil, nil, "", "", nil, c) - if err != nil { - return nil, fmt.Errorf("cannot populate TLS config: %w", err) - } - tlsCfg := cfg.NewTLSConfig() - 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) + authCfg, err := promauth.NewConfig(".", nil, basicAuthCfg, token, tokenFile, oauth2Cfg, tlsCfg) if err != nil { return nil, fmt.Errorf("cannot populate OAuth2 config for remoteWrite idx: %d, err: %w", argIdx, err) } @@ -267,13 +252,8 @@ again: h.Set("Content-Type", "application/x-protobuf") h.Set("Content-Encoding", "snappy") h.Set("X-Prometheus-Remote-Write-Version", "0.1.0") - 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()) + if ah := c.authCfg.GetAuthHeader(); ah != "" { + req.Header.Set("Authorization", ah) } startTime := time.Now() diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 8755a44b1..d0e6ec2ad 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -20,6 +20,8 @@ sort: 15 * FEATURE: automatically detect memory and cpu limits for VictoriaMetrics components running under [cgroup v2](https://www.kernel.org/doc/html/latest/admin-guide/cgroup-v2.html) environments such as [HashiCorp Nomad](https://www.nomadproject.io/). See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1269). * FEATURE: vmauth: allow `-auth.config` reloading via `/-/reload` http endpoint. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1194). * FEATURE: add `timezone_offset(tz)` function. It returns offset in seconds for the given timezone `tz` relative to UTC. This can be useful when combining with datetime-related functions. For example, `day_of_week(time()+timezone_offset("America/Los_Angeles"))` would return weekdays for `America/Los_Angeles` time zone. Special `Local` time zone can be used for returning an offset for the time zone set on the host where VictoriaMetrics runs. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1306) and [MetricsQL docs](https://docs.victoriametrics.com/MetricsQL.html) for more details. +* FEATURE: vmagent: add support for OAuth2 authorization for scrape targets and service discovery in the same way as Prometheus does. See [these docs](https://prometheus.io/docs/prometheus/latest/configuration/configuration/#oauth2). +* FEATURE: vmagent: add support for OAuth2 authorization when writing data to `-remoteWrite.url`. See `-remoteWrite.oauth2.*` config params in `/path/to/vmagent -help` output. * BUGFIX: vmagent: do not retry scraping targets, which don't support HTTP. This should reduce CPU load and network usage at `vmagent` and at scrape target. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1289). * BUGFIX: vmagent: fix possible race when refreshing `role: endpoints` and `role: endpointslices` scrape targets in `kubernetes_sd_config`. Prevoiusly `pod` objects could be updated after the related `endpoints` object update. This could lead to missing scrape targets. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1240). diff --git a/docs/vmagent.md b/docs/vmagent.md index f3fc5e233..224172e2c 100644 --- a/docs/vmagent.md +++ b/docs/vmagent.md @@ -673,12 +673,18 @@ See the docs at https://docs.victoriametrics.com/vmagent.html . -remoteWrite.basicAuth.password array Optional basic auth password to use for -remoteWrite.url. If multiple args are set, then they are applied independently for the corresponding -remoteWrite.url Supports an array of values separated by comma or specified via multiple flags. + -remoteWrite.basicAuth.passwordFile array + Optional path to basic auth password to use for -remoteWrite.url. The file is re-read every second. If multiple args are set, then they are applied independently for the corresponding -remoteWrite.url + Supports an array of values separated by comma or specified via multiple flags. -remoteWrite.basicAuth.username array Optional basic auth username to use for -remoteWrite.url. If multiple args are set, then they are applied independently for the corresponding -remoteWrite.url Supports an array of values separated by comma or specified via multiple flags. -remoteWrite.bearerToken array Optional bearer auth token to use for -remoteWrite.url. If multiple args are set, then they are applied independently for the corresponding -remoteWrite.url Supports an array of values separated by comma or specified via multiple flags. + -remoteWrite.bearerTokenFile array + Optional path to bearer token file to use for -remoteWrite.url. The token is re-read from the file every second. If multiple args are set, then they are applied independently for the corresponding -remoteWrite.url + Supports an array of values separated by comma or specified via multiple flags. -remoteWrite.flushInterval duration Interval for flushing the data to remote storage. This option takes effect only when less than 10K data points per second are pushed to -remoteWrite.url (default 1s) -remoteWrite.label array @@ -694,6 +700,21 @@ See the docs at https://docs.victoriametrics.com/vmagent.html . Supports the following optional suffixes for size values: KB, MB, GB, KiB, MiB, GiB (default 0) -remoteWrite.maxHourlySeries int The maximum number of unique series vmagent can send to remote storage systems during the last hour. Excess series are logged and dropped. This can be useful for limiting series cardinality. See also -remoteWrite.maxDailySeries + -remoteWrite.oauth2.clientID array + Optional OAuth2 clientID to use for -remoteWrite.url. If multiple args are set, then they are applied independently for the corresponding -remoteWrite.url + Supports an array of values separated by comma or specified via multiple flags. + -remoteWrite.oauth2.clientSecret array + Optional OAuth2 clientSecret to use for -remoteWrite.url. If multiple args are set, then they are applied independently for the corresponding -remoteWrite.url + Supports an array of values separated by comma or specified via multiple flags. + -remoteWrite.oauth2.clientSecretFile array + Optional OAuth2 clientSecretFile to use for -remoteWrite.url. If multiple args are set, then they are applied independently for the corresponding -remoteWrite.url + Supports an array of values separated by comma or specified via multiple flags. + -remoteWrite.oauth2.scopes array + Optional OAuth2 scopes to use for -remoteWrite.url. Scopes must be delimited by ';'. If multiple args are set, then they are applied independently for the corresponding -remoteWrite.url + Supports an array of values separated by comma or specified via multiple flags. + -remoteWrite.oauth2.tokenUrl array + Optional OAuth2 tokenURL to use for -remoteWrite.url. If multiple args are set, then they are applied independently for the corresponding -remoteWrite.url + Supports an array of values separated by comma or specified via multiple flags. -remoteWrite.proxyURL array Optional proxy URL for writing data to -remoteWrite.url. Supported proxies: http, https, socks5. Example: -remoteWrite.proxyURL=socks5://proxy:1234 Supports an array of values separated by comma or specified via multiple flags. diff --git a/go.sum b/go.sum index 9b20abbb8..7ed799ddf 100644 --- a/go.sum +++ b/go.sum @@ -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:KefL3i2JdNgsZwKRlHkkV+sYs4ejJ+aGF6glBUoKKio= +github.com/influxdata/influxdb v1.9.0 h1:9z/aRmTpWT1rIm4EN+qTJTZqgEdLGZ4xRMgvA276UEA= 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= @@ -1031,8 +1031,6 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= 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= diff --git a/lib/promauth/config.go b/lib/promauth/config.go index 02812c405..f555c77e6 100644 --- a/lib/promauth/config.go +++ b/lib/promauth/config.go @@ -8,6 +8,7 @@ import ( "encoding/base64" "fmt" "io/ioutil" + "net/url" "sync" "github.com/VictoriaMetrics/VictoriaMetrics/lib/fasttime" @@ -64,65 +65,93 @@ type ProxyClientConfig struct { // 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 + ClientID string `yaml:"client_id"` + ClientSecret string `yaml:"client_secret"` + ClientSecretFile string `yaml:"client_secret_file"` + Scopes []string `yaml:"scopes"` + TokenURL string `yaml:"token_url"` + EndpointParams map[string]string `yaml:"endpoint_params"` } -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()) +// String returns string representation of o. +func (o *OAuth2Config) String() string { + return fmt.Sprintf("clientID=%q, clientSecret=%q, clientSecretFile=%q, Scopes=%q, tokenURL=%q, endpointParams=%q", + o.ClientID, o.ClientSecret, o.ClientSecretFile, o.Scopes, o.TokenURL, o.EndpointParams) } -// validate checks given configs. func (o *OAuth2Config) validate() error { - if o.TokenURL == "" { - return fmt.Errorf("token url cannot be empty") + if o.ClientID == "" { + return fmt.Errorf("client_id 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 fmt.Errorf("ClientSecret and ClientSecretFile cannot be set simultaneously") + } + if o.TokenURL == "" { + return fmt.Errorf("token_url cannot be empty") } 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) - } +type oauth2ConfigInternal struct { + mu sync.Mutex + cfg *clientcredentials.Config + clientSecretFile string + tokenSource oauth2.TokenSource +} - return t.Type() + " " + t.AccessToken, nil +func newOAuth2ConfigInternal(baseDir string, o *OAuth2Config) (*oauth2ConfigInternal, error) { + if err := o.validate(); err != nil { + return nil, err + } + oi := &oauth2ConfigInternal{ + cfg: &clientcredentials.Config{ + ClientID: o.ClientID, + ClientSecret: o.ClientSecret, + TokenURL: o.TokenURL, + Scopes: o.Scopes, + EndpointParams: urlValuesFromMap(o.EndpointParams), + }, + } + if o.ClientSecretFile != "" { + oi.clientSecretFile = getFilepath(baseDir, o.ClientSecretFile) + secret, err := readPasswordFromFile(oi.clientSecretFile) + if err != nil { + return nil, fmt.Errorf("cannot read OAuth2 secret from %q: %w", oi.clientSecretFile, err) + } + oi.cfg.ClientSecret = secret + } + oi.tokenSource = oi.cfg.TokenSource(context.Background()) + return oi, nil +} + +func urlValuesFromMap(m map[string]string) url.Values { + result := make(url.Values, len(m)) + for k, v := range m { + result[k] = []string{v} + } + return result +} + +func (oi *oauth2ConfigInternal) getTokenSource() (oauth2.TokenSource, error) { + oi.mu.Lock() + defer oi.mu.Unlock() + + if oi.clientSecretFile == "" { + return oi.tokenSource, nil + } + newSecret, err := readPasswordFromFile(oi.clientSecretFile) + if err != nil { + return nil, fmt.Errorf("cannot read OAuth2 secret from %q: %w", oi.clientSecretFile, err) + } + if newSecret == oi.cfg.ClientSecret { + return oi.tokenSource, nil + } + oi.cfg.ClientSecret = newSecret + oi.tokenSource = oi.cfg.TokenSource(context.Background()) + return oi.tokenSource, nil } // Config is auth config. @@ -207,7 +236,7 @@ func (pcc *ProxyClientConfig) NewConfig(baseDir string) (*Config, error) { } // NewConfig creates auth config from the given args. -func NewConfig(baseDir string, az *Authorization, basicAuth *BasicAuthConfig, bearerToken, bearerTokenFile string, oauth *OAuth2Config, tlsConfig *TLSConfig) (*Config, error) { +func NewConfig(baseDir string, az *Authorization, basicAuth *BasicAuthConfig, bearerToken, bearerTokenFile string, o *OAuth2Config, tlsConfig *TLSConfig) (*Config, error) { var getAuthHeader func() string authDigest := "" if az != nil { @@ -297,29 +326,28 @@ func NewConfig(baseDir string, az *Authorization, basicAuth *BasicAuthConfig, be } authDigest = fmt.Sprintf("bearer(token=%q)", bearerToken) } - if oauth != nil { + if o != nil { if getAuthHeader != nil { return nil, fmt.Errorf("cannot simultaneously use `authorization`, `basic_auth, `bearer_token` and `ouath2`") } - if err := oauth.validate(); err != nil { + oi, err := newOAuth2ConfigInternal(baseDir, o) + if 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() + ts, err := oi.getTokenSource() if err != nil { - logger.Errorf("cannot get OAuth2 header: %s", err) + logger.Errorf("cannot get OAuth2 tokenSource: %s", err) return "" } - return h + t, err := ts.Token() + if err != nil { + logger.Errorf("cannot get OAuth2 token: %s", err) + return "" + } + return t.Type() + " " + t.AccessToken } + authDigest = fmt.Sprintf("oauth2(%s)", o.String()) } var tlsRootCA *x509.CertPool var tlsCertificate *tls.Certificate diff --git a/lib/promauth/config_test.go b/lib/promauth/config_test.go index 0d438fd3f..12bd9f427 100644 --- a/lib/promauth/config_test.go +++ b/lib/promauth/config_test.go @@ -17,9 +17,10 @@ func TestNewConfig(t *testing.T) { tlsConfig *TLSConfig } tests := []struct { - name string - args args - wantErr bool + name string + args args + wantErr bool + expectHeader string }{ { name: "OAuth2 config", @@ -30,6 +31,7 @@ func TestNewConfig(t *testing.T) { TokenURL: "http://localhost:8511", }, }, + expectHeader: "Bearer some-token", }, { name: "OAuth2 config with file", @@ -40,8 +42,8 @@ func TestNewConfig(t *testing.T) { TokenURL: "http://localhost:8511", }, }, + expectHeader: "Bearer some-token", }, - { name: "OAuth2 want err", args: args{ @@ -62,6 +64,7 @@ func TestNewConfig(t *testing.T) { Password: "password", }, }, + expectHeader: "Basic dXNlcjpwYXNzd29yZA==", }, { name: "basic Auth config with file", @@ -71,21 +74,24 @@ func TestNewConfig(t *testing.T) { PasswordFile: "testdata/test_secretfile.txt", }, }, + expectHeader: "Basic dXNlcjpzZWNyZXQtY29udGVudA==", }, { name: "want Authorization", args: args{ az: &Authorization{ - Type: "Bearer ", + Type: "Bearer", Credentials: "Value", }, }, + expectHeader: "Bearer Value", }, { name: "token file", args: args{ bearerTokenFile: "testdata/test_secretfile.txt", }, + expectHeader: "Bearer secret-content", }, { name: "token with tls", @@ -95,6 +101,7 @@ func TestNewConfig(t *testing.T) { InsecureSkipVerify: true, }, }, + expectHeader: "Bearer some-token", }, } for _, tt := range tests { @@ -103,7 +110,7 @@ func TestNewConfig(t *testing.T) { 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 "}`)) + w.Write([]byte(`{"access_token":"some-token","token_type": "Bearer"}`)) }) mock := httptest.NewServer(r) @@ -115,8 +122,9 @@ func TestNewConfig(t *testing.T) { return } if got != nil { - if ah := got.GetAuthHeader(); ah == "" { - t.Fatalf("unexpected empty auth header") + ah := got.GetAuthHeader() + if ah != tt.expectHeader { + t.Fatalf("unexpected auth header; got %q; want %q", ah, tt.expectHeader) } }