diff options
Diffstat (limited to 'vendor/github.com/hashicorp/vault/api/client.go')
-rw-r--r-- | vendor/github.com/hashicorp/vault/api/client.go | 423 |
1 files changed, 330 insertions, 93 deletions
diff --git a/vendor/github.com/hashicorp/vault/api/client.go b/vendor/github.com/hashicorp/vault/api/client.go index 0204cec..8f0d3f8 100644 --- a/vendor/github.com/hashicorp/vault/api/client.go +++ b/vendor/github.com/hashicorp/vault/api/client.go @@ -1,6 +1,7 @@ package api import ( + "context" "crypto/tls" "fmt" "net" @@ -12,13 +13,15 @@ import ( "strings" "sync" "time" + "unicode" - "golang.org/x/net/http2" - + "github.com/hashicorp/errwrap" "github.com/hashicorp/go-cleanhttp" + retryablehttp "github.com/hashicorp/go-retryablehttp" "github.com/hashicorp/go-rootcerts" "github.com/hashicorp/vault/helper/parseutil" - "github.com/sethgrid/pester" + "golang.org/x/net/http2" + "golang.org/x/time/rate" ) const EnvVaultAddress = "VAULT_ADDR" @@ -32,6 +35,8 @@ const EnvVaultTLSServerName = "VAULT_TLS_SERVER_NAME" const EnvVaultWrapTTL = "VAULT_WRAP_TTL" const EnvVaultMaxRetries = "VAULT_MAX_RETRIES" const EnvVaultToken = "VAULT_TOKEN" +const EnvVaultMFA = "VAULT_MFA" +const EnvRateLimit = "VAULT_RATE_LIMIT" // WrappingLookupFunc is a function that, given an HTTP verb and a path, // returns an optional string duration to be used for response wrapping (e.g. @@ -42,24 +47,42 @@ type WrappingLookupFunc func(operation, path string) string // Config is used to configure the creation of the client. type Config struct { + modifyLock sync.RWMutex + // Address is the address of the Vault server. This should be a complete // URL such as "http://vault.example.com". If you need a custom SSL // cert or want to enable insecure mode, you need to specify a custom // HttpClient. Address string - // HttpClient is the HTTP client to use, which will currently always have the - // same values as http.DefaultClient. This is used to control redirect behavior. + // HttpClient is the HTTP client to use. Vault sets sane defaults for the + // http.Client and its associated http.Transport created in DefaultConfig. + // If you must modify Vault's defaults, it is suggested that you start with + // that client and modify as needed rather than start with an empty client + // (or http.DefaultClient). HttpClient *http.Client - redirectSetup sync.Once - - // MaxRetries controls the maximum number of times to retry when a 5xx error - // occurs. Set to 0 or less to disable retrying. Defaults to 0. + // MaxRetries controls the maximum number of times to retry when a 5xx + // error occurs. Set to 0 to disable retrying. Defaults to 2 (for a total + // of three tries). MaxRetries int // Timeout is for setting custom timeout parameter in the HttpClient Timeout time.Duration + + // If there is an error when creating the configuration, this will be the + // error + Error error + + // The Backoff function to use; a default is used if not provided + Backoff retryablehttp.Backoff + + // Limiter is the rate limiter used by the client. + // If this pointer is nil, then there will be no limit set. + // In contrast, if this pointer is set, even to an empty struct, + // then that limiter will be used. Note that an empty Limiter + // is equivalent blocking all events. + Limiter *rate.Limiter } // TLSConfig contains the parameters needed to configure TLS on the HTTP client @@ -92,60 +115,94 @@ type TLSConfig struct { // // The default Address is https://127.0.0.1:8200, but this can be overridden by // setting the `VAULT_ADDR` environment variable. +// +// If an error is encountered, this will return nil. func DefaultConfig() *Config { config := &Config{ Address: "https://127.0.0.1:8200", HttpClient: cleanhttp.DefaultClient(), } config.HttpClient.Timeout = time.Second * 60 + transport := config.HttpClient.Transport.(*http.Transport) transport.TLSHandshakeTimeout = 10 * time.Second transport.TLSClientConfig = &tls.Config{ MinVersion: tls.VersionTLS12, } + if err := http2.ConfigureTransport(transport); err != nil { + config.Error = err + return config + } - if v := os.Getenv(EnvVaultAddress); v != "" { - config.Address = v + if err := config.ReadEnvironment(); err != nil { + config.Error = err + return config + } + + // Ensure redirects are not automatically followed + // Note that this is sane for the API client as it has its own + // redirect handling logic (and thus also for command/meta), + // but in e.g. http_test actual redirect handling is necessary + config.HttpClient.CheckRedirect = func(req *http.Request, via []*http.Request) error { + // Returning this value causes the Go net library to not close the + // response body and to nil out the error. Otherwise retry clients may + // try three times on every redirect because it sees an error from this + // function (to prevent redirects) passing through to it. + return http.ErrUseLastResponse } + config.Backoff = retryablehttp.LinearJitterBackoff + config.MaxRetries = 2 + return config } -// ConfigureTLS takes a set of TLS configurations and applies those to the the HTTP client. +// ConfigureTLS takes a set of TLS configurations and applies those to the the +// HTTP client. func (c *Config) ConfigureTLS(t *TLSConfig) error { if c.HttpClient == nil { c.HttpClient = DefaultConfig().HttpClient } + clientTLSConfig := c.HttpClient.Transport.(*http.Transport).TLSClientConfig var clientCert tls.Certificate foundClientCert := false - if t.CACert != "" || t.CAPath != "" || t.ClientCert != "" || t.ClientKey != "" || t.Insecure { - if t.ClientCert != "" && t.ClientKey != "" { - var err error - clientCert, err = tls.LoadX509KeyPair(t.ClientCert, t.ClientKey) - if err != nil { - return err - } - foundClientCert = true - } else if t.ClientCert != "" || t.ClientKey != "" { - return fmt.Errorf("Both client cert and client key must be provided") + + switch { + case t.ClientCert != "" && t.ClientKey != "": + var err error + clientCert, err = tls.LoadX509KeyPair(t.ClientCert, t.ClientKey) + if err != nil { + return err } + foundClientCert = true + case t.ClientCert != "" || t.ClientKey != "": + return fmt.Errorf("both client cert and client key must be provided") } - clientTLSConfig := c.HttpClient.Transport.(*http.Transport).TLSClientConfig - rootConfig := &rootcerts.Config{ - CAFile: t.CACert, - CAPath: t.CAPath, - } - if err := rootcerts.ConfigureTLS(clientTLSConfig, rootConfig); err != nil { - return err + if t.CACert != "" || t.CAPath != "" { + rootConfig := &rootcerts.Config{ + CAFile: t.CACert, + CAPath: t.CAPath, + } + if err := rootcerts.ConfigureTLS(clientTLSConfig, rootConfig); err != nil { + return err + } } - clientTLSConfig.InsecureSkipVerify = t.Insecure + if t.Insecure { + clientTLSConfig.InsecureSkipVerify = true + } if foundClientCert { - clientTLSConfig.Certificates = []tls.Certificate{clientCert} + // We use this function to ignore the server's preferential list of + // CAs, otherwise any CA used for the cert auth backend must be in the + // server's CA pool + clientTLSConfig.GetClientCertificate = func(*tls.CertificateRequestInfo) (*tls.Certificate, error) { + return &clientCert, nil + } } + if t.TLSServerName != "" { clientTLSConfig.ServerName = t.TLSServerName } @@ -153,9 +210,8 @@ func (c *Config) ConfigureTLS(t *TLSConfig) error { return nil } -// ReadEnvironment reads configuration information from the -// environment. If there is an error, no configuration value -// is updated. +// ReadEnvironment reads configuration information from the environment. If +// there is an error, no configuration value is updated. func (c *Config) ReadEnvironment() error { var envAddress string var envCACert string @@ -166,6 +222,7 @@ func (c *Config) ReadEnvironment() error { var envInsecure bool var envTLSServerName string var envMaxRetries *uint64 + var limit *rate.Limiter // Parse the environment variables if v := os.Getenv(EnvVaultAddress); v != "" { @@ -190,10 +247,17 @@ func (c *Config) ReadEnvironment() error { if v := os.Getenv(EnvVaultClientKey); v != "" { envClientKey = v } + if v := os.Getenv(EnvRateLimit); v != "" { + rateLimit, burstLimit, err := parseRateLimit(v) + if err != nil { + return err + } + limit = rate.NewLimiter(rate.Limit(rateLimit), burstLimit) + } if t := os.Getenv(EnvVaultClientTimeout); t != "" { clientTimeout, err := parseutil.ParseDurationSecond(t) if err != nil { - return fmt.Errorf("Could not parse %s", EnvVaultClientTimeout) + return fmt.Errorf("could not parse %q", EnvVaultClientTimeout) } envClientTimeout = clientTimeout } @@ -201,7 +265,7 @@ func (c *Config) ReadEnvironment() error { var err error envInsecure, err = strconv.ParseBool(v) if err != nil { - return fmt.Errorf("Could not parse VAULT_SKIP_VERIFY") + return fmt.Errorf("could not parse VAULT_SKIP_VERIFY") } } if v := os.Getenv(EnvVaultTLSServerName); v != "" { @@ -217,6 +281,12 @@ func (c *Config) ReadEnvironment() error { TLSServerName: envTLSServerName, Insecure: envInsecure, } + + c.modifyLock.Lock() + defer c.modifyLock.Unlock() + + c.Limiter = limit + if err := c.ConfigureTLS(t); err != nil { return err } @@ -226,7 +296,7 @@ func (c *Config) ReadEnvironment() error { } if envMaxRetries != nil { - c.MaxRetries = int(*envMaxRetries) + 1 + c.MaxRetries = int(*envMaxRetries) } if envClientTimeout != 0 { @@ -236,70 +306,76 @@ func (c *Config) ReadEnvironment() error { return nil } -// Client is the client to the Vault API. Create a client with -// NewClient. +func parseRateLimit(val string) (rate float64, burst int, err error) { + + _, err = fmt.Sscanf(val, "%f:%d", &rate, &burst) + if err != nil { + rate, err = strconv.ParseFloat(val, 64) + if err != nil { + err = fmt.Errorf("%v was provided but incorrectly formatted", EnvRateLimit) + } + burst = int(rate) + } + + return rate, burst, err + +} + +// Client is the client to the Vault API. Create a client with NewClient. type Client struct { + modifyLock sync.RWMutex addr *url.URL config *Config token string headers http.Header wrappingLookupFunc WrappingLookupFunc + mfaCreds []string + policyOverride bool } // NewClient returns a new client for the given configuration. // +// If the configuration is nil, Vault will use configuration from +// DefaultConfig(), which is the recommended starting configuration. +// // If the environment variable `VAULT_TOKEN` is present, the token will be // automatically added to the client. Otherwise, you must manually call // `SetToken()`. func NewClient(c *Config) (*Client, error) { + def := DefaultConfig() + if def == nil { + return nil, fmt.Errorf("could not create/read default configuration") + } + if def.Error != nil { + return nil, errwrap.Wrapf("error encountered setting up default configuration: {{err}}", def.Error) + } + if c == nil { - c = DefaultConfig() - if err := c.ReadEnvironment(); err != nil { - return nil, fmt.Errorf("error reading environment: %v", err) - } + c = def } + c.modifyLock.Lock() + defer c.modifyLock.Unlock() + u, err := url.Parse(c.Address) if err != nil { return nil, err } if c.HttpClient == nil { - c.HttpClient = DefaultConfig().HttpClient + c.HttpClient = def.HttpClient } if c.HttpClient.Transport == nil { - c.HttpClient.Transport = cleanhttp.DefaultTransport() + c.HttpClient.Transport = def.HttpClient.Transport } - if tp, ok := c.HttpClient.Transport.(*http.Transport); ok { - if err := http2.ConfigureTransport(tp); err != nil { - return nil, err - } - } - - redirFunc := func() { - // Ensure redirects are not automatically followed - // Note that this is sane for the API client as it has its own - // redirect handling logic (and thus also for command/meta), - // but in e.g. http_test actual redirect handling is necessary - c.HttpClient.CheckRedirect = func(req *http.Request, via []*http.Request) error { - // Returning this value causes the Go net library to not close the - // response body and to nil out the error. Otherwise pester tries - // three times on every redirect because it sees an error from this - // function (to prevent redirects) passing through to it. - return http.ErrUseLastResponse - } - } - - c.redirectSetup.Do(redirFunc) - client := &Client{ addr: u, config: c, } if token := os.Getenv(EnvVaultToken); token != "" { - client.SetToken(token) + client.token = token } return client, nil @@ -309,72 +385,181 @@ func NewClient(c *Config) (*Client, error) { // "<Scheme>://<Host>:<Port>". Setting this on a client will override the // value of VAULT_ADDR environment variable. func (c *Client) SetAddress(addr string) error { - var err error - if c.addr, err = url.Parse(addr); err != nil { - return fmt.Errorf("failed to set address: %v", err) + c.modifyLock.Lock() + defer c.modifyLock.Unlock() + + parsedAddr, err := url.Parse(addr) + if err != nil { + return errwrap.Wrapf("failed to set address: {{err}}", err) } + c.addr = parsedAddr return nil } // Address returns the Vault URL the client is configured to connect to func (c *Client) Address() string { + c.modifyLock.RLock() + defer c.modifyLock.RUnlock() + return c.addr.String() } +// SetLimiter will set the rate limiter for this client. +// This method is thread-safe. +// rateLimit and burst are specified according to https://godoc.org/golang.org/x/time/rate#NewLimiter +func (c *Client) SetLimiter(rateLimit float64, burst int) { + c.modifyLock.RLock() + c.config.modifyLock.Lock() + defer c.config.modifyLock.Unlock() + c.modifyLock.RUnlock() + + c.config.Limiter = rate.NewLimiter(rate.Limit(rateLimit), burst) +} + // SetMaxRetries sets the number of retries that will be used in the case of certain errors func (c *Client) SetMaxRetries(retries int) { + c.modifyLock.RLock() + c.config.modifyLock.Lock() + defer c.config.modifyLock.Unlock() + c.modifyLock.RUnlock() + c.config.MaxRetries = retries } // SetClientTimeout sets the client request timeout func (c *Client) SetClientTimeout(timeout time.Duration) { + c.modifyLock.RLock() + c.config.modifyLock.Lock() + defer c.config.modifyLock.Unlock() + c.modifyLock.RUnlock() + c.config.Timeout = timeout } +// CurrentWrappingLookupFunc sets a lookup function that returns desired wrap TTLs +// for a given operation and path +func (c *Client) CurrentWrappingLookupFunc() WrappingLookupFunc { + c.modifyLock.RLock() + defer c.modifyLock.RUnlock() + + return c.wrappingLookupFunc +} + // SetWrappingLookupFunc sets a lookup function that returns desired wrap TTLs // for a given operation and path func (c *Client) SetWrappingLookupFunc(lookupFunc WrappingLookupFunc) { + c.modifyLock.Lock() + defer c.modifyLock.Unlock() + c.wrappingLookupFunc = lookupFunc } +// SetMFACreds sets the MFA credentials supplied either via the environment +// variable or via the command line. +func (c *Client) SetMFACreds(creds []string) { + c.modifyLock.Lock() + defer c.modifyLock.Unlock() + + c.mfaCreds = creds +} + // Token returns the access token being used by this client. It will // return the empty string if there is no token set. func (c *Client) Token() string { + c.modifyLock.RLock() + defer c.modifyLock.RUnlock() + return c.token } // SetToken sets the token directly. This won't perform any auth // verification, it simply sets the token properly for future requests. func (c *Client) SetToken(v string) { + c.modifyLock.Lock() + defer c.modifyLock.Unlock() + c.token = v } // ClearToken deletes the token if it is set or does nothing otherwise. func (c *Client) ClearToken() { + c.modifyLock.Lock() + defer c.modifyLock.Unlock() + c.token = "" } // SetHeaders sets the headers to be used for future requests. func (c *Client) SetHeaders(headers http.Header) { + c.modifyLock.Lock() + defer c.modifyLock.Unlock() + c.headers = headers } -// Clone creates a copy of this client. +// SetBackoff sets the backoff function to be used for future requests. +func (c *Client) SetBackoff(backoff retryablehttp.Backoff) { + c.modifyLock.RLock() + c.config.modifyLock.Lock() + defer c.config.modifyLock.Unlock() + c.modifyLock.RUnlock() + + c.config.Backoff = backoff +} + +// Clone creates a new client with the same configuration. Note that the same +// underlying http.Client is used; modifying the client from more than one +// goroutine at once may not be safe, so modify the client as needed and then +// clone. func (c *Client) Clone() (*Client, error) { - return NewClient(c.config) + c.modifyLock.RLock() + c.config.modifyLock.RLock() + config := c.config + c.modifyLock.RUnlock() + + newConfig := &Config{ + Address: config.Address, + HttpClient: config.HttpClient, + MaxRetries: config.MaxRetries, + Timeout: config.Timeout, + Backoff: config.Backoff, + Limiter: config.Limiter, + } + config.modifyLock.RUnlock() + + return NewClient(newConfig) +} + +// SetPolicyOverride sets whether requests should be sent with the policy +// override flag to request overriding soft-mandatory Sentinel policies (both +// RGPs and EGPs) +func (c *Client) SetPolicyOverride(override bool) { + c.modifyLock.Lock() + defer c.modifyLock.Unlock() + + c.policyOverride = override } // NewRequest creates a new raw request object to query the Vault server // configured for this client. This is an advanced method and generally // doesn't need to be called externally. func (c *Client) NewRequest(method, requestPath string) *Request { + c.modifyLock.RLock() + addr := c.addr + token := c.token + mfaCreds := c.mfaCreds + wrappingLookupFunc := c.wrappingLookupFunc + headers := c.headers + policyOverride := c.policyOverride + c.modifyLock.RUnlock() + // if SRV records exist (see https://tools.ietf.org/html/draft-andrews-http-srv-02), lookup the SRV // record and take the highest match; this is not designed for high-availability, just discovery - var host string = c.addr.Host - if c.addr.Port() == "" { + var host string = addr.Host + if addr.Port() == "" { // Internet Draft specifies that the SRV record is ignored if a port is given - _, addrs, err := net.LookupSRV("http", "tcp", c.addr.Hostname()) + _, addrs, err := net.LookupSRV("http", "tcp", addr.Hostname()) if err == nil && len(addrs) > 0 { host = fmt.Sprintf("%s:%d", addrs[0].Target, addrs[0].Port) } @@ -383,12 +568,12 @@ func (c *Client) NewRequest(method, requestPath string) *Request { req := &Request{ Method: method, URL: &url.URL{ - User: c.addr.User, - Scheme: c.addr.Scheme, + User: addr.User, + Scheme: addr.Scheme, Host: host, - Path: path.Join(c.addr.Path, requestPath), + Path: path.Join(addr.Path, requestPath), }, - ClientToken: c.token, + ClientToken: token, Params: make(map[string][]string), } @@ -401,18 +586,21 @@ func (c *Client) NewRequest(method, requestPath string) *Request { default: lookupPath = requestPath } - if c.wrappingLookupFunc != nil { - req.WrapTTL = c.wrappingLookupFunc(method, lookupPath) + + req.MFAHeaderVals = mfaCreds + + if wrappingLookupFunc != nil { + req.WrapTTL = wrappingLookupFunc(method, lookupPath) } else { req.WrapTTL = DefaultWrappingLookupFunc(method, lookupPath) } - if c.config.Timeout != 0 { - c.config.HttpClient.Timeout = c.config.Timeout - } - if c.headers != nil { - req.Headers = c.headers + + if headers != nil { + req.Headers = headers } + req.PolicyOverride = policyOverride + return req } @@ -420,26 +608,75 @@ func (c *Client) NewRequest(method, requestPath string) *Request { // a Vault server not configured with this client. This is an advanced operation // that generally won't need to be called externally. func (c *Client) RawRequest(r *Request) (*Response, error) { + c.modifyLock.RLock() + token := c.token + + c.config.modifyLock.RLock() + limiter := c.config.Limiter + maxRetries := c.config.MaxRetries + backoff := c.config.Backoff + httpClient := c.config.HttpClient + timeout := c.config.Timeout + c.config.modifyLock.RUnlock() + + c.modifyLock.RUnlock() + + if limiter != nil { + limiter.Wait(context.Background()) + } + + // Sanity check the token before potentially erroring from the API + idx := strings.IndexFunc(token, func(c rune) bool { + return !unicode.IsPrint(c) + }) + if idx != -1 { + return nil, fmt.Errorf("configured Vault token contains non-printable characters and cannot be used") + } + redirectCount := 0 START: - req, err := r.ToHTTP() + req, err := r.toRetryableHTTP() if err != nil { return nil, err } + if req == nil { + return nil, fmt.Errorf("nil request created") + } - client := pester.NewExtendedClient(c.config.HttpClient) - client.Backoff = pester.LinearJitterBackoff - client.MaxRetries = c.config.MaxRetries + // Set the timeout, if any + var cancelFunc context.CancelFunc + if timeout != 0 { + var ctx context.Context + ctx, cancelFunc = context.WithTimeout(context.Background(), timeout) + req.Request = req.Request.WithContext(ctx) + } + + if backoff == nil { + backoff = retryablehttp.LinearJitterBackoff + } + + client := &retryablehttp.Client{ + HTTPClient: httpClient, + RetryWaitMin: 1000 * time.Millisecond, + RetryWaitMax: 1500 * time.Millisecond, + RetryMax: maxRetries, + CheckRetry: retryablehttp.DefaultRetryPolicy, + Backoff: backoff, + ErrorHandler: retryablehttp.PassthroughErrorHandler, + } var result *Response resp, err := client.Do(req) + if cancelFunc != nil { + cancelFunc() + } if resp != nil { result = &Response{Response: resp} } if err != nil { if strings.Contains(err.Error(), "tls: oversized") { - err = fmt.Errorf( - "%s\n\n"+ + err = errwrap.Wrapf( + "{{err}}\n\n"+ "This error usually means that the server is running with TLS disabled\n"+ "but the client is configured to use TLS. Please either enable TLS\n"+ "on the server or run the client with -address set to an address\n"+ |