2020-04-13 11:59:05 +02:00
|
|
|
package promauth
|
|
|
|
|
|
|
|
import (
|
2020-05-03 11:41:13 +02:00
|
|
|
"bytes"
|
2021-05-22 15:20:18 +02:00
|
|
|
"context"
|
2020-04-13 11:59:05 +02:00
|
|
|
"crypto/tls"
|
|
|
|
"crypto/x509"
|
|
|
|
"encoding/base64"
|
|
|
|
"fmt"
|
|
|
|
"io/ioutil"
|
2021-05-14 19:00:05 +02:00
|
|
|
"sync"
|
|
|
|
|
|
|
|
"github.com/VictoriaMetrics/VictoriaMetrics/lib/fasttime"
|
|
|
|
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
|
2021-05-22 15:20:18 +02:00
|
|
|
"golang.org/x/oauth2"
|
|
|
|
"golang.org/x/oauth2/clientcredentials"
|
2020-04-13 11:59:05 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
// TLSConfig represents TLS config.
|
|
|
|
//
|
|
|
|
// See https://prometheus.io/docs/prometheus/latest/configuration/configuration/#tls_config
|
|
|
|
type TLSConfig struct {
|
2020-11-13 15:17:03 +01:00
|
|
|
CAFile string `yaml:"ca_file,omitempty"`
|
|
|
|
CertFile string `yaml:"cert_file,omitempty"`
|
|
|
|
KeyFile string `yaml:"key_file,omitempty"`
|
|
|
|
ServerName string `yaml:"server_name,omitempty"`
|
|
|
|
InsecureSkipVerify bool `yaml:"insecure_skip_verify,omitempty"`
|
2020-04-13 11:59:05 +02:00
|
|
|
}
|
|
|
|
|
2021-04-02 20:17:43 +02:00
|
|
|
// Authorization represents generic authorization config.
|
|
|
|
//
|
|
|
|
// See https://prometheus.io/docs/prometheus/latest/configuration/configuration/
|
|
|
|
type Authorization struct {
|
|
|
|
Type string `yaml:"type,omitempty"`
|
|
|
|
Credentials string `yaml:"credentials,omitempty"`
|
|
|
|
CredentialsFile string `yaml:"credentials_file,omitempty"`
|
|
|
|
}
|
|
|
|
|
2020-04-13 11:59:05 +02:00
|
|
|
// BasicAuthConfig represents basic auth config.
|
|
|
|
type BasicAuthConfig struct {
|
|
|
|
Username string `yaml:"username"`
|
2020-11-13 15:17:03 +01:00
|
|
|
Password string `yaml:"password,omitempty"`
|
|
|
|
PasswordFile string `yaml:"password_file,omitempty"`
|
2020-04-13 11:59:05 +02:00
|
|
|
}
|
|
|
|
|
2021-04-02 20:17:43 +02:00
|
|
|
// HTTPClientConfig represents http client config.
|
|
|
|
type HTTPClientConfig struct {
|
|
|
|
Authorization *Authorization `yaml:"authorization,omitempty"`
|
|
|
|
BasicAuth *BasicAuthConfig `yaml:"basic_auth,omitempty"`
|
|
|
|
BearerToken string `yaml:"bearer_token,omitempty"`
|
|
|
|
BearerTokenFile string `yaml:"bearer_token_file,omitempty"`
|
2021-05-22 15:20:18 +02:00
|
|
|
OAuth2 *OAuth2Config `yaml:"oauth2,omitempty"`
|
2021-04-02 20:17:43 +02:00
|
|
|
TLSConfig *TLSConfig `yaml:"tls_config,omitempty"`
|
|
|
|
}
|
|
|
|
|
2021-04-03 23:40:08 +02:00
|
|
|
// ProxyClientConfig represents proxy client config.
|
|
|
|
type ProxyClientConfig struct {
|
|
|
|
Authorization *Authorization `yaml:"proxy_authorization,omitempty"`
|
|
|
|
BasicAuth *BasicAuthConfig `yaml:"proxy_basic_auth,omitempty"`
|
|
|
|
BearerToken string `yaml:"proxy_bearer_token,omitempty"`
|
|
|
|
BearerTokenFile string `yaml:"proxy_bearer_token_file,omitempty"`
|
|
|
|
TLSConfig *TLSConfig `yaml:"proxy_tls_config,omitempty"`
|
|
|
|
}
|
|
|
|
|
2021-05-22 15:20:18 +02:00
|
|
|
// 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
|
|
|
|
}
|
|
|
|
|
2020-04-13 11:59:05 +02:00
|
|
|
// Config is auth config.
|
|
|
|
type Config struct {
|
|
|
|
// Optional TLS config
|
|
|
|
TLSRootCA *x509.CertPool
|
|
|
|
TLSCertificate *tls.Certificate
|
|
|
|
TLSServerName string
|
|
|
|
TLSInsecureSkipVerify bool
|
2021-05-14 19:00:05 +02:00
|
|
|
|
|
|
|
getAuthHeader func() string
|
|
|
|
authHeaderLock sync.Mutex
|
|
|
|
authHeader string
|
|
|
|
authHeaderDeadline uint64
|
|
|
|
|
|
|
|
authDigest string
|
|
|
|
}
|
|
|
|
|
|
|
|
// GetAuthHeader returns optional `Authorization: ...` http header.
|
|
|
|
func (ac *Config) GetAuthHeader() string {
|
|
|
|
f := ac.getAuthHeader
|
|
|
|
if f == nil {
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
ac.authHeaderLock.Lock()
|
|
|
|
defer ac.authHeaderLock.Unlock()
|
|
|
|
if fasttime.UnixTimestamp() > ac.authHeaderDeadline {
|
|
|
|
ac.authHeader = f()
|
|
|
|
// Cache the authHeader for a second.
|
|
|
|
ac.authHeaderDeadline = fasttime.UnixTimestamp() + 1
|
|
|
|
}
|
|
|
|
return ac.authHeader
|
2020-04-13 11:59:05 +02:00
|
|
|
}
|
|
|
|
|
2021-05-14 19:00:05 +02:00
|
|
|
// String returns human-readable representation for ac.
|
2020-05-03 11:41:13 +02:00
|
|
|
func (ac *Config) String() string {
|
2021-05-14 19:00:05 +02:00
|
|
|
return fmt.Sprintf("AuthDigest=%s, TLSRootCA=%s, TLSCertificate=%s, TLSServerName=%s, TLSInsecureSkipVerify=%v",
|
|
|
|
ac.authDigest, ac.tlsRootCAString(), ac.tlsCertificateString(), ac.TLSServerName, ac.TLSInsecureSkipVerify)
|
2020-05-03 11:41:13 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
func (ac *Config) tlsRootCAString() string {
|
|
|
|
if ac.TLSRootCA == nil {
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
data := ac.TLSRootCA.Subjects()
|
|
|
|
return string(bytes.Join(data, []byte("\n")))
|
|
|
|
}
|
|
|
|
|
|
|
|
func (ac *Config) tlsCertificateString() string {
|
|
|
|
if ac.TLSCertificate == nil {
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
return string(bytes.Join(ac.TLSCertificate.Certificate, []byte("\n")))
|
|
|
|
}
|
|
|
|
|
2020-04-13 11:59:05 +02:00
|
|
|
// NewTLSConfig returns new TLS config for the given ac.
|
|
|
|
func (ac *Config) NewTLSConfig() *tls.Config {
|
|
|
|
tlsCfg := &tls.Config{
|
|
|
|
ClientSessionCache: tls.NewLRUClientSessionCache(0),
|
|
|
|
}
|
2021-03-09 17:54:09 +01:00
|
|
|
if ac == nil {
|
|
|
|
return tlsCfg
|
|
|
|
}
|
2020-04-13 11:59:05 +02:00
|
|
|
if ac.TLSCertificate != nil {
|
2020-05-12 16:20:55 +02:00
|
|
|
// Do not set tlsCfg.GetClientCertificate, since tlsCfg.Certificates should work OK.
|
|
|
|
tlsCfg.Certificates = []tls.Certificate{*ac.TLSCertificate}
|
2020-04-13 11:59:05 +02:00
|
|
|
}
|
2021-03-09 17:54:09 +01:00
|
|
|
tlsCfg.RootCAs = ac.TLSRootCA
|
2020-04-13 11:59:05 +02:00
|
|
|
tlsCfg.ServerName = ac.TLSServerName
|
|
|
|
tlsCfg.InsecureSkipVerify = ac.TLSInsecureSkipVerify
|
|
|
|
return tlsCfg
|
|
|
|
}
|
|
|
|
|
2021-04-02 20:17:43 +02:00
|
|
|
// NewConfig creates auth config for the given hcc.
|
|
|
|
func (hcc *HTTPClientConfig) NewConfig(baseDir string) (*Config, error) {
|
2021-05-22 15:20:18 +02:00
|
|
|
return NewConfig(baseDir, hcc.Authorization, hcc.BasicAuth, hcc.BearerToken, hcc.BearerTokenFile, hcc.OAuth2, hcc.TLSConfig)
|
2021-04-02 20:17:43 +02:00
|
|
|
}
|
|
|
|
|
2021-04-03 23:40:08 +02:00
|
|
|
// NewConfig creates auth config for the given pcc.
|
|
|
|
func (pcc *ProxyClientConfig) NewConfig(baseDir string) (*Config, error) {
|
2021-05-22 15:20:18 +02:00
|
|
|
return NewConfig(baseDir, pcc.Authorization, pcc.BasicAuth, pcc.BearerToken, pcc.BearerTokenFile, nil, pcc.TLSConfig)
|
2021-04-03 23:40:08 +02:00
|
|
|
}
|
|
|
|
|
2020-04-13 11:59:05 +02:00
|
|
|
// NewConfig creates auth config from the given args.
|
2021-05-22 15:20:18 +02:00
|
|
|
func NewConfig(baseDir string, az *Authorization, basicAuth *BasicAuthConfig, bearerToken, bearerTokenFile string, oauth *OAuth2Config, tlsConfig *TLSConfig) (*Config, error) {
|
2021-05-14 19:00:05 +02:00
|
|
|
var getAuthHeader func() string
|
|
|
|
authDigest := ""
|
2021-04-02 20:17:43 +02:00
|
|
|
if az != nil {
|
|
|
|
azType := "Bearer"
|
|
|
|
if az.Type != "" {
|
|
|
|
azType = az.Type
|
|
|
|
}
|
|
|
|
if az.CredentialsFile != "" {
|
|
|
|
if az.Credentials != "" {
|
|
|
|
return nil, fmt.Errorf("both `credentials`=%q and `credentials_file`=%q are set", az.Credentials, az.CredentialsFile)
|
|
|
|
}
|
2021-05-14 19:00:05 +02:00
|
|
|
filePath := getFilepath(baseDir, az.CredentialsFile)
|
|
|
|
getAuthHeader = func() string {
|
|
|
|
token, err := readPasswordFromFile(filePath)
|
|
|
|
if err != nil {
|
|
|
|
logger.Errorf("cannot read credentials from `credentials_file`=%q: %s", az.CredentialsFile, err)
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
return azType + " " + token
|
|
|
|
}
|
|
|
|
authDigest = fmt.Sprintf("custom(type=%q, credsFile=%q)", az.Type, filePath)
|
|
|
|
} else {
|
|
|
|
getAuthHeader = func() string {
|
|
|
|
return azType + " " + az.Credentials
|
2021-04-02 20:17:43 +02:00
|
|
|
}
|
2021-05-14 19:00:05 +02:00
|
|
|
authDigest = fmt.Sprintf("custom(type=%q, creds=%q)", az.Type, az.Credentials)
|
2021-04-02 20:17:43 +02:00
|
|
|
}
|
|
|
|
}
|
2020-04-13 11:59:05 +02:00
|
|
|
if basicAuth != nil {
|
2021-05-14 19:00:05 +02:00
|
|
|
if getAuthHeader != nil {
|
2021-04-02 20:17:43 +02:00
|
|
|
return nil, fmt.Errorf("cannot use both `authorization` and `basic_auth`")
|
|
|
|
}
|
2020-04-13 11:59:05 +02:00
|
|
|
if basicAuth.Username == "" {
|
|
|
|
return nil, fmt.Errorf("missing `username` in `basic_auth` section")
|
|
|
|
}
|
|
|
|
if basicAuth.PasswordFile != "" {
|
|
|
|
if basicAuth.Password != "" {
|
|
|
|
return nil, fmt.Errorf("both `password`=%q and `password_file`=%q are set in `basic_auth` section", basicAuth.Password, basicAuth.PasswordFile)
|
|
|
|
}
|
2021-05-14 19:00:05 +02:00
|
|
|
filePath := getFilepath(baseDir, basicAuth.PasswordFile)
|
|
|
|
getAuthHeader = func() string {
|
|
|
|
password, err := readPasswordFromFile(filePath)
|
|
|
|
if err != nil {
|
|
|
|
logger.Errorf("cannot read password from `password_file`=%q set in `basic_auth` section: %s", basicAuth.PasswordFile, err)
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
// See https://en.wikipedia.org/wiki/Basic_access_authentication
|
|
|
|
token := basicAuth.Username + ":" + password
|
|
|
|
token64 := base64.StdEncoding.EncodeToString([]byte(token))
|
|
|
|
return "Basic " + token64
|
|
|
|
}
|
|
|
|
authDigest = fmt.Sprintf("basic(username=%q, passwordFile=%q)", basicAuth.Username, filePath)
|
|
|
|
} else {
|
|
|
|
getAuthHeader = func() string {
|
|
|
|
// See https://en.wikipedia.org/wiki/Basic_access_authentication
|
|
|
|
token := basicAuth.Username + ":" + basicAuth.Password
|
|
|
|
token64 := base64.StdEncoding.EncodeToString([]byte(token))
|
|
|
|
return "Basic " + token64
|
2020-04-13 11:59:05 +02:00
|
|
|
}
|
2021-05-14 19:00:05 +02:00
|
|
|
authDigest = fmt.Sprintf("basic(username=%q, password=%q)", basicAuth.Username, basicAuth.Password)
|
2020-04-13 11:59:05 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
if bearerTokenFile != "" {
|
2021-05-14 19:00:05 +02:00
|
|
|
if getAuthHeader != nil {
|
2021-04-02 20:17:43 +02:00
|
|
|
return nil, fmt.Errorf("cannot simultaneously use `authorization`, `basic_auth` and `bearer_token_file`")
|
|
|
|
}
|
2020-04-13 11:59:05 +02:00
|
|
|
if bearerToken != "" {
|
|
|
|
return nil, fmt.Errorf("both `bearer_token`=%q and `bearer_token_file`=%q are set", bearerToken, bearerTokenFile)
|
|
|
|
}
|
2021-05-14 19:00:05 +02:00
|
|
|
filePath := getFilepath(baseDir, bearerTokenFile)
|
|
|
|
getAuthHeader = func() string {
|
|
|
|
token, err := readPasswordFromFile(filePath)
|
|
|
|
if err != nil {
|
|
|
|
logger.Errorf("cannot read bearer token from `bearer_token_file`=%q: %s", bearerTokenFile, err)
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
return "Bearer " + token
|
2020-04-13 11:59:05 +02:00
|
|
|
}
|
2021-05-14 19:00:05 +02:00
|
|
|
authDigest = fmt.Sprintf("bearer(tokenFile=%q)", filePath)
|
2020-04-13 11:59:05 +02:00
|
|
|
}
|
|
|
|
if bearerToken != "" {
|
2021-05-14 19:00:05 +02:00
|
|
|
if getAuthHeader != nil {
|
2021-04-02 20:17:43 +02:00
|
|
|
return nil, fmt.Errorf("cannot simultaneously use `authorization`, `basic_auth` and `bearer_token`")
|
2020-04-13 11:59:05 +02:00
|
|
|
}
|
2021-05-14 19:00:05 +02:00
|
|
|
getAuthHeader = func() string {
|
|
|
|
return "Bearer " + bearerToken
|
|
|
|
}
|
|
|
|
authDigest = fmt.Sprintf("bearer(token=%q)", bearerToken)
|
2020-04-13 11:59:05 +02:00
|
|
|
}
|
2021-05-22 15:20:18 +02:00
|
|
|
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
|
|
|
|
}
|
|
|
|
}
|
2020-04-13 11:59:05 +02:00
|
|
|
var tlsRootCA *x509.CertPool
|
|
|
|
var tlsCertificate *tls.Certificate
|
|
|
|
tlsServerName := ""
|
|
|
|
tlsInsecureSkipVerify := false
|
|
|
|
if tlsConfig != nil {
|
|
|
|
tlsServerName = tlsConfig.ServerName
|
|
|
|
tlsInsecureSkipVerify = tlsConfig.InsecureSkipVerify
|
|
|
|
if tlsConfig.CertFile != "" || tlsConfig.KeyFile != "" {
|
|
|
|
certPath := getFilepath(baseDir, tlsConfig.CertFile)
|
|
|
|
keyPath := getFilepath(baseDir, tlsConfig.KeyFile)
|
|
|
|
cert, err := tls.LoadX509KeyPair(certPath, keyPath)
|
|
|
|
if err != nil {
|
2020-06-30 21:58:18 +02:00
|
|
|
return nil, fmt.Errorf("cannot load TLS certificate from `cert_file`=%q, `key_file`=%q: %w", tlsConfig.CertFile, tlsConfig.KeyFile, err)
|
2020-04-13 11:59:05 +02:00
|
|
|
}
|
|
|
|
tlsCertificate = &cert
|
|
|
|
}
|
|
|
|
if tlsConfig.CAFile != "" {
|
|
|
|
path := getFilepath(baseDir, tlsConfig.CAFile)
|
|
|
|
data, err := ioutil.ReadFile(path)
|
|
|
|
if err != nil {
|
2020-06-30 21:58:18 +02:00
|
|
|
return nil, fmt.Errorf("cannot read `ca_file` %q: %w", tlsConfig.CAFile, err)
|
2020-04-13 11:59:05 +02:00
|
|
|
}
|
|
|
|
tlsRootCA = x509.NewCertPool()
|
|
|
|
if !tlsRootCA.AppendCertsFromPEM(data) {
|
|
|
|
return nil, fmt.Errorf("cannot parse data from `ca_file` %q", tlsConfig.CAFile)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
ac := &Config{
|
|
|
|
TLSRootCA: tlsRootCA,
|
|
|
|
TLSCertificate: tlsCertificate,
|
|
|
|
TLSServerName: tlsServerName,
|
|
|
|
TLSInsecureSkipVerify: tlsInsecureSkipVerify,
|
2021-05-14 19:00:05 +02:00
|
|
|
|
|
|
|
getAuthHeader: getAuthHeader,
|
|
|
|
authDigest: authDigest,
|
2020-04-13 11:59:05 +02:00
|
|
|
}
|
|
|
|
return ac, nil
|
|
|
|
}
|