Merge pull request #4100 from hashicorp/b-vault-no-auth

Improve handling of Vault errors
This commit is contained in:
Alex Dadgar
2018-04-03 17:23:43 -07:00
committed by GitHub
28 changed files with 1336 additions and 805 deletions

View File

@@ -82,6 +82,7 @@ BUG FIXES:
* client: Improve auto-detection of network interface when interface name has a
space in it on Windows [[GH-3855](https://github.com/hashicorp/nomad/issues/3855)]
* client/vault: Recognize renewing non-renewable Vault lease as fatal [[GH-3727](https://github.com/hashicorp/nomad/issues/3727)]
* client/vault: Improved error handling of network errors with Vault [[GH-4100](https://github.com/hashicorp/nomad/issues/4100)]
* config: Revert minimum CPU limit back to 20 from 100 [[GH-3706](https://github.com/hashicorp/nomad/issues/3706)]
* config: Always add core scheduler to enabled schedulers and add invalid
EnabledScheduler detection [[GH-3978](https://github.com/hashicorp/nomad/issues/3978)]

View File

@@ -1939,10 +1939,27 @@ func (c *Client) deriveToken(alloc *structs.Allocation, taskNames []string, vcli
// Unwrap the vault token
unwrapResp, err := vclient.Logical().Unwrap(wrappedToken)
if err != nil {
return nil, fmt.Errorf("failed to unwrap the token for task %q: %v", taskName, err)
if structs.VaultUnrecoverableError.MatchString(err.Error()) {
return nil, err
}
// The error is recoverable
return nil, structs.NewRecoverableError(
fmt.Errorf("failed to unwrap the token for task %q: %v", taskName, err), true)
}
if unwrapResp == nil || unwrapResp.Auth == nil || unwrapResp.Auth.ClientToken == "" {
return nil, fmt.Errorf("failed to unwrap the token for task %q", taskName)
// Validate the response
var validationErr error
if unwrapResp == nil {
validationErr = fmt.Errorf("Vault returned nil secret when unwrapping")
} else if unwrapResp.Auth == nil {
validationErr = fmt.Errorf("Vault returned unwrap secret with nil Auth. Secret warnings: %v", unwrapResp.Warnings)
} else if unwrapResp.Auth.ClientToken == "" {
validationErr = fmt.Errorf("Vault returned unwrap secret with empty Auth.ClientToken. Secret warnings: %v", unwrapResp.Warnings)
}
if validationErr != nil {
c.logger.Printf("[WARN] client.vault: failed to unwrap token: %v", err)
return nil, structs.NewRecoverableError(validationErr, true)
}
// Append the unwrapped token to the return value

View File

@@ -1398,10 +1398,6 @@ func (n *Node) DeriveVaultToken(args *structs.DeriveVaultTokenRequest,
tokens := make(map[string]string, len(results))
for task, secret := range results {
w := secret.WrapInfo
if w == nil {
return fmt.Errorf("Vault returned Secret without WrapInfo")
}
tokens[task] = w.Token
accessor := &structs.VaultAccessor{
Accessor: w.WrappedAccessor,

View File

@@ -5141,6 +5141,12 @@ func (d *EphemeralDisk) Copy() *EphemeralDisk {
return ld
}
var (
// VaultUnrecoverableError matches unrecoverable errors returned by a Vault
// server
VaultUnrecoverableError = regexp.MustCompile(`Code:\s+40(0|3|4)`)
)
const (
// VaultChangeModeNoop takes no action when a new token is retrieved.
VaultChangeModeNoop = "noop"

View File

@@ -6,7 +6,6 @@ import (
"fmt"
"log"
"math/rand"
"regexp"
"sync"
"sync/atomic"
"time"
@@ -70,9 +69,6 @@ const (
)
var (
// vaultUnrecoverableError matches unrecoverable errors
vaultUnrecoverableError = regexp.MustCompile(`Code:\s+40(0|3|4)`)
// vaultCapabilitiesCapability is the expected capability of Nomad's Vault
// token on the the path. The token must have at least one of the
// capabilities.
@@ -695,7 +691,7 @@ func (v *vaultClient) validateCapabilities(role string, root bool) error {
_, _, err := v.hasCapability(vaultCapabilitiesLookupPath, vaultCapabilitiesCapability)
if err != nil {
// Check if there is a permission denied
if vaultUnrecoverableError.MatchString(err.Error()) {
if structs.VaultUnrecoverableError.MatchString(err.Error()) {
// Since we can't read permissions, we just log a warning that we
// can't tell if the Vault token will work
msg := fmt.Sprintf("Can not lookup token capabilities. "+
@@ -894,7 +890,7 @@ func (v *vaultClient) CreateToken(ctx context.Context, a *structs.Allocation, ta
// Determine whether it is unrecoverable
if err != nil {
if vaultUnrecoverableError.MatchString(err.Error()) {
if structs.VaultUnrecoverableError.MatchString(err.Error()) {
return secret, err
}
@@ -902,6 +898,21 @@ func (v *vaultClient) CreateToken(ctx context.Context, a *structs.Allocation, ta
return nil, structs.NewRecoverableError(err, true)
}
// Validate the response
var validationErr error
if secret == nil {
validationErr = fmt.Errorf("Vault returned nil Secret")
} else if secret.WrapInfo == nil {
validationErr = fmt.Errorf("Vault returned Secret with nil WrapInfo. Secret warnings: %v", secret.Warnings)
} else if secret.WrapInfo.WrappedAccessor == "" {
validationErr = fmt.Errorf("Vault returned WrapInfo without WrappedAccessor. Secret warnings: %v", secret.Warnings)
}
if validationErr != nil {
v.logger.Printf("[WARN] vault: failed to CreateToken: %v", err)
return nil, structs.NewRecoverableError(validationErr, true)
}
// Got a valid response
return secret, nil
}

View File

@@ -1,611 +0,0 @@
FORMAT: 1A
# vault
The Vault API gives you full access to the Vault project.
If you're browsing this API specifiction in GitHub or in raw
format, please excuse some of the odd formatting. This document
is in api-blueprint format that is read by viewers such as
Apiary.
## Sealed vs. Unsealed
Whenever an individual Vault server is started, it is started
in the _sealed_ state. In this state, it knows where its data
is located, but the data is encrypted and Vault doesn't have the
encryption keys to access it. Before Vault can operate, it must
be _unsealed_.
**Note:** Sealing/unsealing has no relationship to _authentication_
which is separate and still required once the Vault is unsealed.
Instead of being sealed with a single key, we utilize
[Shamir's Secret Sharing](http://en.wikipedia.org/wiki/Shamir%27s_Secret_Sharing)
to shard a key into _n_ parts such that _t_ parts are required
to reconstruct the original key, where `t <= n`. This means that
Vault itself doesn't know the original key, and no single person
has the original key (unless `n = 1`, or `t` parts are given to
a single person).
Unsealing is done via an unauthenticated
[unseal API](#reference/seal/unseal/unseal). This API takes a single
master shard and progresses the unsealing process. Once all shards
are given, the Vault is either unsealed or resets the unsealing
process if the key was invalid.
The entire seal/unseal state is server-wide. This allows multiple
distinct operators to use the unseal API (or more likely the
`vault unseal` command) from separate computers/networks and never
have to transmit their key in order to unseal the vault in a
distributed fashion.
## Transport
The API is expected to be accessed over a TLS connection at
all times, with a valid certificate that is verified by a well
behaved client.
## Authentication
Once the Vault is unsealed, every other operation requires
authentication. There are multiple methods for authentication
that can be enabled (see
[authentication](#reference/authentication)).
Authentication is done with the login endpoint. The login endpoint
returns an access token that is set as the `X-Vault-Token` header.
## Help
To retrieve the help for any API within Vault, including mounted
backends, credential providers, etc. then append `?help=1` to any
URL. If you have valid permission to access the path, then the help text
will be returned with the following structure:
{
"help": "help text"
}
## Error Response
A common JSON structure is always returned to return errors:
{
"errors": [
"message",
"another message"
]
}
This structure will be sent down for any non-20x HTTP status.
## HTTP Status Codes
The following HTTP status codes are used throughout the API.
- `200` - Success with data.
- `204` - Success, no data returned.
- `400` - Invalid request, missing or invalid data.
- `403` - Forbidden, your authentication details are either
incorrect or you don't have access to this feature.
- `404` - Invalid path. This can both mean that the path truly
doesn't exist or that you don't have permission to view a
specific path. We use 404 in some cases to avoid state leakage.
- `429` - Rate limit exceeded. Try again after waiting some period
of time.
- `500` - Internal server error. An internal error has occurred,
try again later. If the error persists, report a bug.
- `503` - Vault is down for maintenance or is currently sealed.
Try again later.
# Group Initialization
## Initialization [/sys/init]
### Initialization Status [GET]
Returns the status of whether the vault is initialized or not. The
vault doesn't have to be unsealed for this operation.
+ Response 200 (application/json)
{
"initialized": true
}
### Initialize [POST]
Initialize the vault. This is an unauthenticated request to initially
setup a new vault. Although this is unauthenticated, it is still safe:
data cannot be in vault prior to initialization, and any future
authentication will fail if you didn't initialize it yourself.
Additionally, once initialized, a vault cannot be reinitialized.
This API is the only time Vault will ever be aware of your keys, and
the only time the keys will ever be returned in one unit. Care should
be taken to ensure that the output of this request is never logged,
and that the keys are properly distributed.
The response also contains the initial root token that can be used
as authentication in order to initially configure Vault once it is
unsealed. Just as with the unseal keys, this is the only time Vault is
ever aware of this token.
+ Request (application/json)
{
"secret_shares": 5,
"secret_threshold": 3,
}
+ Response 200 (application/json)
{
"keys": ["one", "two", "three"],
"root_token": "foo"
}
# Group Seal/Unseal
## Seal Status [/sys/seal-status]
### Seal Status [GET]
Returns the status of whether the vault is currently
sealed or not, as well as the progress of unsealing.
The response has the following attributes:
- sealed (boolean) - If true, the vault is sealed. Otherwise,
it is unsealed.
- t (int) - The "t" value for the master key, or the number
of shards needed total to unseal the vault.
- n (int) - The "n" value for the master key, or the total
number of shards of the key distributed.
- progress (int) - The number of master key shards that have
been entered so far towards unsealing the vault.
+ Response 200 (application/json)
{
"sealed": true,
"t": 3,
"n": 5,
"progress": 1
}
## Seal [/sys/seal]
### Seal [PUT]
Seal the vault.
Sealing the vault locks Vault from any future operations on any
secrets or system configuration until the vault is once again
unsealed. Internally, sealing throws away the keys to access the
encrypted vault data, so Vault is unable to access the data without
unsealing to get the encryption keys.
+ Response 204
## Unseal [/sys/unseal]
### Unseal [PUT]
Unseal the vault.
Unseal the vault by entering a portion of the master key. The
response object will tell you if the unseal is complete or
only partial.
If the vault is already unsealed, this does nothing. It is
not an error, the return value just says the vault is unsealed.
Due to the architecture of Vault, we cannot validate whether
any portion of the unseal key given is valid until all keys
are inputted, therefore unsealing an already unsealed vault
is still a success even if the input key is invalid.
+ Request (application/json)
{
"key": "value"
}
+ Response 200 (application/json)
{
"sealed": true,
"t": 3,
"n": 5,
"progress": 1
}
# Group Authentication
## List Auth Methods [/sys/auth]
### List all auth methods [GET]
Lists all available authentication methods.
This returns the name of the authentication method as well as
a human-friendly long-form help text for the method that can be
shown to the user as documentation.
+ Response 200 (application/json)
{
"token": {
"type": "token",
"description": "Token authentication"
},
"oauth": {
"type": "oauth",
"description": "OAuth authentication"
}
}
## Single Auth Method [/sys/auth/{id}]
+ Parameters
+ id (required, string) ... The ID of the auth method.
### Enable an auth method [PUT]
Enables an authentication method.
The body of the request depends on the authentication method
being used. Please reference the documentation for the specific
authentication method you're enabling in order to determine what
parameters you must give it.
If an authentication method is already enabled, then this can be
used to change the configuration, including even the type of
the configuration.
+ Request (application/json)
{
"type": "type",
"key": "value",
"key2": "value2"
}
+ Response 204
### Disable an auth method [DELETE]
Disables an authentication method. Previously authenticated sessions
are immediately invalidated.
+ Response 204
# Group Policies
Policies are named permission sets that identities returned by
credential stores are bound to. This separates _authentication_
from _authorization_.
## Policies [/sys/policy]
### List all Policies [GET]
List all the policies.
+ Response 200 (application/json)
{
"policies": ["root"]
}
## Single Policy [/sys/policy/{id}]
+ Parameters
+ id (required, string) ... The name of the policy
### Upsert [PUT]
Create or update a policy with the given ID.
+ Request (application/json)
{
"rules": "HCL"
}
+ Response 204
### Delete [DELETE]
Delete a policy with the given ID. Any identities bound to this
policy will immediately become "deny all" despite already being
authenticated.
+ Response 204
# Group Mounts
Logical backends are mounted at _mount points_, similar to
filesystems. This allows you to mount the "aws" logical backend
at the "aws-us-east" path, so all access is at `/aws-us-east/keys/foo`
for example. This enables multiple logical backends to be enabled.
## Mounts [/sys/mounts]
### List all mounts [GET]
Lists all the active mount points.
+ Response 200 (application/json)
{
"aws": {
"type": "aws",
"description": "AWS"
},
"pg": {
"type": "postgresql",
"description": "PostgreSQL dynamic users"
}
}
## Single Mount [/sys/mounts/{path}]
### New Mount [POST]
Mount a logical backend to a new path.
Configuration for this new backend is done via the normal
read/write mechanism once it is mounted.
+ Request (application/json)
{
"type": "aws",
"description": "EU AWS tokens"
}
+ Response 204
### Unmount [DELETE]
Unmount a mount point.
+ Response 204
## Remount [/sys/remount]
### Remount [POST]
Move an already-mounted backend to a new path.
+ Request (application/json)
{
"from": "aws",
"to": "aws-east"
}
+ Response 204
# Group Audit Backends
Audit backends are responsible for shuttling the audit logs that
Vault generates to a durable system for future querying. By default,
audit logs are not stored anywhere.
## Audit Backends [/sys/audit]
### List Enabled Audit Backends [GET]
List all the enabled audit backends
+ Response 200 (application/json)
{
"file": {
"type": "file",
"description": "Send audit logs to a file",
"options": {}
}
}
## Single Audit Backend [/sys/audit/{path}]
+ Parameters
+ path (required, string) ... The path where the audit backend is mounted
### Enable [PUT]
Enable an audit backend.
+ Request (application/json)
{
"type": "file",
"description": "send to a file",
"options": {
"path": "/var/log/vault.audit.log"
}
}
+ Response 204
### Disable [DELETE]
Disable an audit backend.
+ Request (application/json)
+ Response 204
# Group Secrets
## Generic [/{mount}/{path}]
This group documents the general format of reading and writing
to Vault. The exact structure of the keyspace is defined by the
logical backends in use, so documentation related to
a specific backend should be referenced for details on what keys
and routes are expected.
The path for examples are `/prefix/path`, but in practice
these will be defined by the backends that are mounted. For
example, reading an AWS key might be at the `/aws/root` path.
These paths are defined by the logical backends.
+ Parameters
+ mount (required, string) ... The mount point for the
logical backend. Example: `aws`.
+ path (optional, string) ... The path within the backend
to read or write data.
### Read [GET]
Read data from vault.
The data read from the vault can either be a secret or
arbitrary configuration data. The type of data returned
depends on the path, and is defined by the logical backend.
If the return value is a secret, then the return structure
is a mixture of arbitrary key/value along with the following
fields which are guaranteed to exist:
- `lease_id` (string) - A unique ID used for renewal and
revocation.
- `renewable` (bool) - If true, then this key can be renewed.
If a key can't be renewed, then a new key must be requested
after the lease duration period.
- `lease_duration` (int) - The time in seconds that a secret is
valid for before it must be renewed.
- `lease_duration_max` (int) - The maximum amount of time in
seconds that a secret is valid for. This will always be
greater than or equal to `lease_duration`. The difference
between this and `lease_duration` is an overlap window
where multiple keys may be valid.
If the return value is not a secret, then the return structure
is an arbitrary JSON object.
+ Response 200 (application/json)
{
"lease_id": "UUID",
"lease_duration": 3600,
"key": "value"
}
### Write [PUT]
Write data to vault.
The behavior and arguments to the write are defined by
the logical backend.
+ Request (application/json)
{
"key": "value"
}
+ Response 204
# Group Lease Management
## Renew Key [/sys/renew/{id}]
+ Parameters
+ id (required, string) ... The `lease_id` of the secret
to renew.
### Renew [PUT]
+ Response 200 (application/json)
{
"lease_id": "...",
"lease_duration": 3600,
"access_key": "foo",
"secret_key": "bar"
}
## Revoke Key [/sys/revoke/{id}]
+ Parameters
+ id (required, string) ... The `lease_id` of the secret
to revoke.
### Revoke [PUT]
+ Response 204
# Group Backend: AWS
## Root Key [/aws/root]
### Set the Key [PUT]
Set the root key that the logical backend will use to create
new secrets, IAM policies, etc.
+ Request (application/json)
{
"access_key": "key",
"secret_key": "key",
"region": "us-east-1"
}
+ Response 204
## Policies [/aws/policies]
### List Policies [GET]
List all the policies that can be used to create keys.
+ Response 200 (application/json)
[{
"name": "root",
"description": "Root access"
}, {
"name": "web-deploy",
"description": "Enough permissions to deploy the web app."
}]
## Single Policy [/aws/policies/{name}]
+ Parameters
+ name (required, string) ... Name of the policy.
### Read [GET]
Read a policy.
+ Response 200 (application/json)
{
"policy": "base64-encoded policy"
}
### Upsert [PUT]
Create or update a policy.
+ Request (application/json)
{
"policy": "base64-encoded policy"
}
+ Response 204
### Delete [DELETE]
Delete the policy with the given name.
+ Response 204
## Generate Access Keys [/aws/keys/{policy}]
### Create [GET]
This generates a new keypair for the given policy.
+ Parameters
+ policy (required, string) ... The policy under which to create
the key pair.
+ Response 200 (application/json)
{
"lease_id": "...",
"lease_duration": 3600,
"access_key": "foo",
"secret_key": "bar"
}

View File

@@ -3,6 +3,7 @@ package api
import (
"crypto/tls"
"fmt"
"net"
"net/http"
"net/url"
"os"
@@ -11,13 +12,14 @@ import (
"strings"
"sync"
"time"
"unicode"
"golang.org/x/net/http2"
"github.com/hashicorp/errwrap"
"github.com/hashicorp/go-cleanhttp"
"github.com/hashicorp/go-rootcerts"
"github.com/hashicorp/vault/helper/parseutil"
"github.com/sethgrid/pester"
"golang.org/x/net/http2"
)
const EnvVaultAddress = "VAULT_ADDR"
@@ -31,6 +33,7 @@ const EnvVaultTLSServerName = "VAULT_TLS_SERVER_NAME"
const EnvVaultWrapTTL = "VAULT_WRAP_TTL"
const EnvVaultMaxRetries = "VAULT_MAX_RETRIES"
const EnvVaultToken = "VAULT_TOKEN"
const EnvVaultMFA = "VAULT_MFA"
// 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.
@@ -41,24 +44,31 @@ 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 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
}
// TLSConfig contains the parameters needed to configure TLS on the HTTP client
@@ -91,60 +101,91 @@ 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 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
}
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")
}
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 := c.HttpClient.Transport.(*http.Transport).TLSClientConfig
rootConfig := &rootcerts.Config{
CAFile: t.CACert,
CAPath: t.CAPath,
if t.Insecure {
clientTLSConfig.InsecureSkipVerify = true
}
if err := rootcerts.ConfigureTLS(clientTLSConfig, rootConfig); err != nil {
return err
}
clientTLSConfig.InsecureSkipVerify = t.Insecure
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
}
@@ -152,9 +193,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
@@ -216,6 +256,10 @@ func (c *Config) ReadEnvironment() error {
TLSServerName: envTLSServerName,
Insecure: envInsecure,
}
c.modifyLock.Lock()
defer c.modifyLock.Unlock()
if err := c.ConfigureTLS(t); err != nil {
return err
}
@@ -235,27 +279,41 @@ func (c *Config) ReadEnvironment() error {
return nil
}
// Client is the client to the Vault API. Create a client with
// NewClient.
// 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) {
if c == nil {
c = DefaultConfig()
if err := c.ReadEnvironment(); err != nil {
return nil, fmt.Errorf("error reading environment: %v", err)
}
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 = def
}
c.modifyLock.Lock()
defer c.modifyLock.Unlock()
u, err := url.Parse(c.Address)
if err != nil {
@@ -263,37 +321,19 @@ func NewClient(c *Config) (*Client, error) {
}
if c.HttpClient == nil {
c.HttpClient = DefaultConfig().HttpClient
c.HttpClient = def.HttpClient
}
tp := c.HttpClient.Transport.(*http.Transport)
if err := http2.ConfigureTransport(tp); err != nil {
return nil, err
if c.HttpClient.Transport == nil {
c.HttpClient.Transport = def.HttpClient.Transport
}
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
@@ -303,6 +343,9 @@ 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 {
c.modifyLock.Lock()
defer c.modifyLock.Unlock()
var err error
if c.addr, err = url.Parse(addr); err != nil {
return fmt.Errorf("failed to set address: %v", err)
@@ -313,57 +356,139 @@ func (c *Client) SetAddress(addr string) error {
// 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()
}
// 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
}
// 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 = ""
}
// Clone creates a copy of this client.
// 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 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,
}
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()
defer 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() == "" {
// Internet Draft specifies that the SRV record is ignored if a port is given
_, addrs, err := net.LookupSRV("http", "tcp", c.addr.Hostname())
if err == nil && len(addrs) > 0 {
host = fmt.Sprintf("%s:%d", addrs[0].Target, addrs[0].Port)
}
}
req := &Request{
Method: method,
URL: &url.URL{
User: c.addr.User,
Scheme: c.addr.Scheme,
Host: c.addr.Host,
Host: host,
Path: path.Join(c.addr.Path, requestPath),
},
ClientToken: c.token,
@@ -379,6 +504,9 @@ func (c *Client) NewRequest(method, requestPath string) *Request {
default:
lookupPath = requestPath
}
req.MFAHeaderVals = c.mfaCreds
if c.wrappingLookupFunc != nil {
req.WrapTTL = c.wrappingLookupFunc(method, lookupPath)
} else {
@@ -387,6 +515,11 @@ func (c *Client) NewRequest(method, requestPath string) *Request {
if c.config.Timeout != 0 {
c.config.HttpClient.Timeout = c.config.Timeout
}
if c.headers != nil {
req.Headers = c.headers
}
req.PolicyOverride = c.policyOverride
return req
}
@@ -395,6 +528,20 @@ 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()
c.config.modifyLock.RLock()
defer c.config.modifyLock.RUnlock()
token := c.token
c.modifyLock.RUnlock()
// 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()

View File

@@ -138,10 +138,11 @@ func (c *Logical) Unwrap(wrappingToken string) (*Secret, error) {
if resp != nil {
defer resp.Body.Close()
}
if err != nil {
if resp != nil && resp.StatusCode != 404 {
return nil, err
}
// Return all errors except those that are from a 404 as we handle the not
// found error as a special case.
if err != nil && (resp == nil || resp.StatusCode != 404) {
return nil, err
}
if resp == nil {
return nil, nil
@@ -178,7 +179,7 @@ func (c *Logical) Unwrap(wrappingToken string) (*Secret, error) {
wrappedSecret := new(Secret)
buf := bytes.NewBufferString(secret.Data["response"].(string))
if err := jsonutil.DecodeJSONFromReader(buf, wrappedSecret); err != nil {
return nil, fmt.Errorf("error unmarshaling wrapped secret: %s", err)
return nil, fmt.Errorf("error unmarshalling wrapped secret: %s", err)
}
return wrappedSecret, nil

View File

@@ -13,9 +13,6 @@ var (
ErrRenewerNotRenewable = errors.New("secret is not renewable")
ErrRenewerNoSecretData = errors.New("returned empty secret data")
// DefaultRenewerGrace is the default grace period
DefaultRenewerGrace = 15 * time.Second
// DefaultRenewerRenewBuffer is the default size of the buffer for renew
// messages on the channel.
DefaultRenewerRenewBuffer = 5
@@ -50,12 +47,13 @@ var (
type Renewer struct {
l sync.Mutex
client *Client
secret *Secret
grace time.Duration
random *rand.Rand
doneCh chan error
renewCh chan *RenewOutput
client *Client
secret *Secret
grace time.Duration
random *rand.Rand
increment int
doneCh chan error
renewCh chan *RenewOutput
stopped bool
stopCh chan struct{}
@@ -79,6 +77,11 @@ type RenewerInput struct {
// RenewBuffer is the size of the buffered channel where renew messages are
// dispatched.
RenewBuffer int
// The new TTL, in seconds, that should be set on the lease. The TTL set
// here may or may not be honored by the vault server, based on Vault
// configuration or any associated max TTL values.
Increment int
}
// RenewOutput is the metadata returned to the client (if it's listening) to
@@ -105,9 +108,6 @@ func (c *Client) NewRenewer(i *RenewerInput) (*Renewer, error) {
}
grace := i.Grace
if grace == 0 {
grace = DefaultRenewerGrace
}
random := i.Rand
if random == nil {
@@ -120,12 +120,13 @@ func (c *Client) NewRenewer(i *RenewerInput) (*Renewer, error) {
}
return &Renewer{
client: c,
secret: secret,
grace: grace,
random: random,
doneCh: make(chan error, 1),
renewCh: make(chan *RenewOutput, renewBuffer),
client: c,
secret: secret,
grace: grace,
increment: i.Increment,
random: random,
doneCh: make(chan error, 1),
renewCh: make(chan *RenewOutput, renewBuffer),
stopped: false,
stopCh: make(chan struct{}),
@@ -155,8 +156,8 @@ func (r *Renewer) Stop() {
}
// Renew starts a background process for renewing this secret. When the secret
// is has auth data, this attempts to renew the auth (token). When the secret
// has a lease, this attempts to renew the lease.
// has auth data, this attempts to renew the auth (token). When the secret has
// a lease, this attempts to renew the lease.
func (r *Renewer) Renew() {
var result error
if r.secret.Auth != nil {
@@ -177,6 +178,9 @@ func (r *Renewer) renewAuth() error {
return ErrRenewerNotRenewable
}
priorDuration := time.Duration(r.secret.Auth.LeaseDuration) * time.Second
r.calculateGrace(priorDuration)
client, token := r.client, r.secret.Auth.ClientToken
for {
@@ -188,7 +192,7 @@ func (r *Renewer) renewAuth() error {
}
// Renew the auth.
renewal, err := client.Auth().Token().RenewTokenAsSelf(token, 0)
renewal, err := client.Auth().Token().RenewTokenAsSelf(token, r.increment)
if err != nil {
return err
}
@@ -209,13 +213,28 @@ func (r *Renewer) renewAuth() error {
return ErrRenewerNotRenewable
}
// Grab the lease duration and sleep duration - note that we grab the auth
// lease duration, not the secret lease duration.
// Grab the lease duration
leaseDuration := time.Duration(renewal.Auth.LeaseDuration) * time.Second
sleepDuration := r.sleepDuration(leaseDuration)
// If we are within grace, return now.
if leaseDuration <= r.grace || sleepDuration <= r.grace {
// We keep evaluating a new grace period so long as the lease is
// extending. Once it stops extending, we've hit the max and need to
// rely on the grace duration.
if leaseDuration > priorDuration {
r.calculateGrace(leaseDuration)
}
priorDuration = leaseDuration
// The sleep duration is set to 2/3 of the current lease duration plus
// 1/3 of the current grace period, which adds jitter.
sleepDuration := time.Duration(float64(leaseDuration.Nanoseconds())*2/3 + float64(r.grace.Nanoseconds())/3)
// If we are within grace, return now; or, if the amount of time we
// would sleep would land us in the grace period. This helps with short
// tokens; for example, you don't want a current lease duration of 4
// seconds, a grace period of 3 seconds, and end up sleeping for more
// than three of those seconds and having a very small budget of time
// to renew.
if leaseDuration <= r.grace || leaseDuration-sleepDuration <= r.grace {
return nil
}
@@ -234,6 +253,9 @@ func (r *Renewer) renewLease() error {
return ErrRenewerNotRenewable
}
priorDuration := time.Duration(r.secret.LeaseDuration) * time.Second
r.calculateGrace(priorDuration)
client, leaseID := r.client, r.secret.LeaseID
for {
@@ -245,7 +267,7 @@ func (r *Renewer) renewLease() error {
}
// Renew the lease.
renewal, err := client.Sys().Renew(leaseID, 0)
renewal, err := client.Sys().Renew(leaseID, r.increment)
if err != nil {
return err
}
@@ -266,12 +288,28 @@ func (r *Renewer) renewLease() error {
return ErrRenewerNotRenewable
}
// Grab the lease duration and sleep duration
// Grab the lease duration
leaseDuration := time.Duration(renewal.LeaseDuration) * time.Second
sleepDuration := r.sleepDuration(leaseDuration)
// If we are within grace, return now.
if leaseDuration <= r.grace || sleepDuration <= r.grace {
// We keep evaluating a new grace period so long as the lease is
// extending. Once it stops extending, we've hit the max and need to
// rely on the grace duration.
if leaseDuration > priorDuration {
r.calculateGrace(leaseDuration)
}
priorDuration = leaseDuration
// The sleep duration is set to 2/3 of the current lease duration plus
// 1/3 of the current grace period, which adds jitter.
sleepDuration := time.Duration(float64(leaseDuration.Nanoseconds())*2/3 + float64(r.grace.Nanoseconds())/3)
// If we are within grace, return now; or, if the amount of time we
// would sleep would land us in the grace period. This helps with short
// tokens; for example, you don't want a current lease duration of 4
// seconds, a grace period of 3 seconds, and end up sleeping for more
// than three of those seconds and having a very small budget of time
// to renew.
if leaseDuration <= r.grace || leaseDuration-sleepDuration <= r.grace {
return nil
}
@@ -300,3 +338,20 @@ func (r *Renewer) sleepDuration(base time.Duration) time.Duration {
return time.Duration(sleep)
}
// calculateGrace calculates the grace period based on a reasonable set of
// assumptions given the total lease time; it also adds some jitter to not have
// clients be in sync.
func (r *Renewer) calculateGrace(leaseDuration time.Duration) {
if leaseDuration == 0 {
r.grace = 0
return
}
leaseNanos := float64(leaseDuration.Nanoseconds())
jitterMax := 0.1 * leaseNanos
// For a given lease duration, we want to allow 80-90% of that to elapse,
// so the remaining amount is the grace period
r.grace = time.Duration(jitterMax) + time.Duration(uint64(r.random.Int63())%uint64(jitterMax))
}

View File

@@ -11,15 +11,21 @@ import (
// Request is a raw request configuration structure used to initiate
// API requests to the Vault server.
type Request struct {
Method string
URL *url.URL
Params url.Values
Headers http.Header
ClientToken string
WrapTTL string
Obj interface{}
Body io.Reader
BodySize int64
Method string
URL *url.URL
Params url.Values
Headers http.Header
ClientToken string
MFAHeaderVals []string
WrapTTL string
Obj interface{}
Body io.Reader
BodySize int64
// Whether to request overriding soft-mandatory Sentinel policies (RGPs and
// EGPs). If set, the override flag will take effect for all policies
// evaluated during the request.
PolicyOverride bool
}
// SetJSONBody is used to set a request body that is a JSON-encoded value.
@@ -77,5 +83,15 @@ func (r *Request) ToHTTP() (*http.Request, error) {
req.Header.Set("X-Vault-Wrap-TTL", r.WrapTTL)
}
if len(r.MFAHeaderVals) != 0 {
for _, mfaHeaderVal := range r.MFAHeaderVals {
req.Header.Add("X-Vault-MFA", mfaHeaderVal)
}
}
if r.PolicyOverride {
req.Header.Set("X-Vault-Policy-Override", "true")
}
return req, nil
}

View File

@@ -1,10 +1,12 @@
package api
import (
"fmt"
"io"
"time"
"github.com/hashicorp/vault/helper/jsonutil"
"github.com/hashicorp/vault/helper/parseutil"
)
// Secret is the structure returned for every secret within Vault.
@@ -35,13 +37,197 @@ type Secret struct {
WrapInfo *SecretWrapInfo `json:"wrap_info,omitempty"`
}
// TokenID returns the standardized token ID (token) for the given secret.
func (s *Secret) TokenID() (string, error) {
if s == nil {
return "", nil
}
if s.Auth != nil && len(s.Auth.ClientToken) > 0 {
return s.Auth.ClientToken, nil
}
if s.Data == nil || s.Data["id"] == nil {
return "", nil
}
id, ok := s.Data["id"].(string)
if !ok {
return "", fmt.Errorf("token found but in the wrong format")
}
return id, nil
}
// TokenAccessor returns the standardized token accessor for the given secret.
// If the secret is nil or does not contain an accessor, this returns the empty
// string.
func (s *Secret) TokenAccessor() (string, error) {
if s == nil {
return "", nil
}
if s.Auth != nil && len(s.Auth.Accessor) > 0 {
return s.Auth.Accessor, nil
}
if s.Data == nil || s.Data["accessor"] == nil {
return "", nil
}
accessor, ok := s.Data["accessor"].(string)
if !ok {
return "", fmt.Errorf("token found but in the wrong format")
}
return accessor, nil
}
// TokenRemainingUses returns the standardized remaining uses for the given
// secret. If the secret is nil or does not contain the "num_uses", this
// returns -1. On error, this will return -1 and a non-nil error.
func (s *Secret) TokenRemainingUses() (int, error) {
if s == nil || s.Data == nil || s.Data["num_uses"] == nil {
return -1, nil
}
uses, err := parseutil.ParseInt(s.Data["num_uses"])
if err != nil {
return 0, err
}
return int(uses), nil
}
// TokenPolicies returns the standardized list of policies for the given secret.
// If the secret is nil or does not contain any policies, this returns nil.
func (s *Secret) TokenPolicies() ([]string, error) {
if s == nil {
return nil, nil
}
if s.Auth != nil && len(s.Auth.Policies) > 0 {
return s.Auth.Policies, nil
}
if s.Data == nil || s.Data["policies"] == nil {
return nil, nil
}
sList, ok := s.Data["policies"].([]string)
if ok {
return sList, nil
}
list, ok := s.Data["policies"].([]interface{})
if !ok {
return nil, fmt.Errorf("unable to convert token policies to expected format")
}
policies := make([]string, len(list))
for i := range list {
p, ok := list[i].(string)
if !ok {
return nil, fmt.Errorf("unable to convert policy %v to string", list[i])
}
policies[i] = p
}
return policies, nil
}
// TokenMetadata returns the map of metadata associated with this token, if any
// exists. If the secret is nil or does not contain the "metadata" key, this
// returns nil.
func (s *Secret) TokenMetadata() (map[string]string, error) {
if s == nil {
return nil, nil
}
if s.Auth != nil && len(s.Auth.Metadata) > 0 {
return s.Auth.Metadata, nil
}
if s.Data == nil || (s.Data["metadata"] == nil && s.Data["meta"] == nil) {
return nil, nil
}
data, ok := s.Data["metadata"].(map[string]interface{})
if !ok {
data, ok = s.Data["meta"].(map[string]interface{})
if !ok {
return nil, fmt.Errorf("unable to convert metadata field to expected format")
}
}
metadata := make(map[string]string, len(data))
for k, v := range data {
typed, ok := v.(string)
if !ok {
return nil, fmt.Errorf("unable to convert metadata value %v to string", v)
}
metadata[k] = typed
}
return metadata, nil
}
// TokenIsRenewable returns the standardized token renewability for the given
// secret. If the secret is nil or does not contain the "renewable" key, this
// returns false.
func (s *Secret) TokenIsRenewable() (bool, error) {
if s == nil {
return false, nil
}
if s.Auth != nil && s.Auth.Renewable {
return s.Auth.Renewable, nil
}
if s.Data == nil || s.Data["renewable"] == nil {
return false, nil
}
renewable, err := parseutil.ParseBool(s.Data["renewable"])
if err != nil {
return false, fmt.Errorf("could not convert renewable value to a boolean: %v", err)
}
return renewable, nil
}
// TokenTTL returns the standardized remaining token TTL for the given secret.
// If the secret is nil or does not contain a TTL, this returns 0.
func (s *Secret) TokenTTL() (time.Duration, error) {
if s == nil {
return 0, nil
}
if s.Auth != nil && s.Auth.LeaseDuration > 0 {
return time.Duration(s.Auth.LeaseDuration) * time.Second, nil
}
if s.Data == nil || s.Data["ttl"] == nil {
return 0, nil
}
ttl, err := parseutil.ParseDurationSecond(s.Data["ttl"])
if err != nil {
return 0, err
}
return ttl, nil
}
// SecretWrapInfo contains wrapping information if we have it. If what is
// contained is an authentication token, the accessor for the token will be
// available in WrappedAccessor.
type SecretWrapInfo struct {
Token string `json:"token"`
Accessor string `json:"accessor"`
TTL int `json:"ttl"`
CreationTime time.Time `json:"creation_time"`
CreationPath string `json:"creation_path"`
WrappedAccessor string `json:"wrapped_accessor"`
}

View File

@@ -36,3 +36,20 @@ func (c *SSH) Credential(role string, data map[string]interface{}) (*Secret, err
return ParseSecret(resp.Body)
}
// SignKey signs the given public key and returns a signed public key to pass
// along with the SSH request.
func (c *SSH) SignKey(role string, data map[string]interface{}) (*Secret, error) {
r := c.c.NewRequest("PUT", fmt.Sprintf("/v1/%s/sign/%s", c.MountPoint, role))
if err := r.SetJSONBody(data); err != nil {
return nil, err
}
resp, err := c.c.RawRequest(r)
if err != nil {
return nil, err
}
defer resp.Body.Close()
return ParseSecret(resp.Body)
}

View File

@@ -78,26 +78,45 @@ func (c *Sys) DisableAuth(path string) error {
}
// Structures for the requests/resposne are all down here. They aren't
// individually documentd because the map almost directly to the raw HTTP API
// individually documented because the map almost directly to the raw HTTP API
// documentation. Please refer to that documentation for more details.
type EnableAuthOptions struct {
Type string `json:"type" structs:"type"`
Description string `json:"description" structs:"description"`
Local bool `json:"local" structs:"local"`
PluginName string `json:"plugin_name,omitempty" structs:"plugin_name,omitempty" mapstructure:"plugin_name"`
Type string `json:"type" structs:"type"`
Description string `json:"description" structs:"description"`
Config AuthConfigInput `json:"config" structs:"config"`
Local bool `json:"local" structs:"local"`
PluginName string `json:"plugin_name,omitempty" structs:"plugin_name,omitempty"`
SealWrap bool `json:"seal_wrap" structs:"seal_wrap" mapstructure:"seal_wrap"`
Options map[string]string `json:"options" structs:"options" mapstructure:"options"`
}
type AuthConfigInput struct {
DefaultLeaseTTL string `json:"default_lease_ttl" structs:"default_lease_ttl" mapstructure:"default_lease_ttl"`
MaxLeaseTTL string `json:"max_lease_ttl" structs:"max_lease_ttl" mapstructure:"max_lease_ttl"`
PluginName string `json:"plugin_name,omitempty" structs:"plugin_name,omitempty" mapstructure:"plugin_name"`
AuditNonHMACRequestKeys []string `json:"audit_non_hmac_request_keys,omitempty" structs:"audit_non_hmac_request_keys" mapstructure:"audit_non_hmac_request_keys"`
AuditNonHMACResponseKeys []string `json:"audit_non_hmac_response_keys,omitempty" structs:"audit_non_hmac_response_keys" mapstructure:"audit_non_hmac_response_keys"`
ListingVisibility string `json:"listing_visibility,omitempty" structs:"listing_visibility" mapstructure:"listing_visibility"`
PassthroughRequestHeaders []string `json:"passthrough_request_headers,omitempty" structs:"passthrough_request_headers" mapstructure:"passthrough_request_headers"`
}
type AuthMount struct {
Type string `json:"type" structs:"type" mapstructure:"type"`
Description string `json:"description" structs:"description" mapstructure:"description"`
Accessor string `json:"accessor" structs:"accessor" mapstructure:"accessor"`
Config AuthConfigOutput `json:"config" structs:"config" mapstructure:"config"`
Local bool `json:"local" structs:"local" mapstructure:"local"`
Type string `json:"type" structs:"type" mapstructure:"type"`
Description string `json:"description" structs:"description" mapstructure:"description"`
Accessor string `json:"accessor" structs:"accessor" mapstructure:"accessor"`
Config AuthConfigOutput `json:"config" structs:"config" mapstructure:"config"`
Local bool `json:"local" structs:"local" mapstructure:"local"`
SealWrap bool `json:"seal_wrap" structs:"seal_wrap" mapstructure:"seal_wrap"`
Options map[string]string `json:"options" structs:"options" mapstructure:"options"`
}
type AuthConfigOutput struct {
DefaultLeaseTTL int `json:"default_lease_ttl" structs:"default_lease_ttl" mapstructure:"default_lease_ttl"`
MaxLeaseTTL int `json:"max_lease_ttl" structs:"max_lease_ttl" mapstructure:"max_lease_ttl"`
PluginName string `json:"plugin_name,omitempty" structs:"plugin_name,omitempty" mapstructure:"plugin_name"`
DefaultLeaseTTL int `json:"default_lease_ttl" structs:"default_lease_ttl" mapstructure:"default_lease_ttl"`
MaxLeaseTTL int `json:"max_lease_ttl" structs:"max_lease_ttl" mapstructure:"max_lease_ttl"`
PluginName string `json:"plugin_name,omitempty" structs:"plugin_name,omitempty" mapstructure:"plugin_name"`
AuditNonHMACRequestKeys []string `json:"audit_non_hmac_request_keys,omitempty" structs:"audit_non_hmac_request_keys" mapstructure:"audit_non_hmac_request_keys"`
AuditNonHMACResponseKeys []string `json:"audit_non_hmac_response_keys,omitempty" structs:"audit_non_hmac_response_keys" mapstructure:"audit_non_hmac_response_keys"`
ListingVisibility string `json:"listing_visibility,omitempty" structs:"listing_visibility" mapstructure:"listing_visibility"`
PassthroughRequestHeaders []string `json:"passthrough_request_headers,omitempty" structs:"passthrough_request_headers" mapstructure:"passthrough_request_headers"`
}

View File

@@ -1,7 +1,15 @@
package api
func (c *Sys) GenerateRootStatus() (*GenerateRootStatusResponse, error) {
r := c.c.NewRequest("GET", "/v1/sys/generate-root/attempt")
return c.generateRootStatusCommon("/v1/sys/generate-root/attempt")
}
func (c *Sys) GenerateDROperationTokenStatus() (*GenerateRootStatusResponse, error) {
return c.generateRootStatusCommon("/v1/sys/replication/dr/secondary/generate-operation-token/attempt")
}
func (c *Sys) generateRootStatusCommon(path string) (*GenerateRootStatusResponse, error) {
r := c.c.NewRequest("GET", path)
resp, err := c.c.RawRequest(r)
if err != nil {
return nil, err
@@ -14,12 +22,20 @@ func (c *Sys) GenerateRootStatus() (*GenerateRootStatusResponse, error) {
}
func (c *Sys) GenerateRootInit(otp, pgpKey string) (*GenerateRootStatusResponse, error) {
return c.generateRootInitCommon("/v1/sys/generate-root/attempt", otp, pgpKey)
}
func (c *Sys) GenerateDROperationTokenInit(otp, pgpKey string) (*GenerateRootStatusResponse, error) {
return c.generateRootInitCommon("/v1/sys/replication/dr/secondary/generate-operation-token/attempt", otp, pgpKey)
}
func (c *Sys) generateRootInitCommon(path, otp, pgpKey string) (*GenerateRootStatusResponse, error) {
body := map[string]interface{}{
"otp": otp,
"pgp_key": pgpKey,
}
r := c.c.NewRequest("PUT", "/v1/sys/generate-root/attempt")
r := c.c.NewRequest("PUT", path)
if err := r.SetJSONBody(body); err != nil {
return nil, err
}
@@ -36,7 +52,15 @@ func (c *Sys) GenerateRootInit(otp, pgpKey string) (*GenerateRootStatusResponse,
}
func (c *Sys) GenerateRootCancel() error {
r := c.c.NewRequest("DELETE", "/v1/sys/generate-root/attempt")
return c.generateRootCancelCommon("/v1/sys/generate-root/attempt")
}
func (c *Sys) GenerateDROperationTokenCancel() error {
return c.generateRootCancelCommon("/v1/sys/replication/dr/secondary/generate-operation-token/attempt")
}
func (c *Sys) generateRootCancelCommon(path string) error {
r := c.c.NewRequest("DELETE", path)
resp, err := c.c.RawRequest(r)
if err == nil {
defer resp.Body.Close()
@@ -45,12 +69,20 @@ func (c *Sys) GenerateRootCancel() error {
}
func (c *Sys) GenerateRootUpdate(shard, nonce string) (*GenerateRootStatusResponse, error) {
return c.generateRootUpdateCommon("/v1/sys/generate-root/update", shard, nonce)
}
func (c *Sys) GenerateDROperationTokenUpdate(shard, nonce string) (*GenerateRootStatusResponse, error) {
return c.generateRootUpdateCommon("/v1/sys/replication/dr/secondary/generate-operation-token/update", shard, nonce)
}
func (c *Sys) generateRootUpdateCommon(path, shard, nonce string) (*GenerateRootStatusResponse, error) {
body := map[string]interface{}{
"key": shard,
"nonce": nonce,
}
r := c.c.NewRequest("PUT", "/v1/sys/generate-root/update")
r := c.c.NewRequest("PUT", path)
if err := r.SetJSONBody(body); err != nil {
return nil, err
}
@@ -67,11 +99,12 @@ func (c *Sys) GenerateRootUpdate(shard, nonce string) (*GenerateRootStatusRespon
}
type GenerateRootStatusResponse struct {
Nonce string
Started bool
Progress int
Required int
Complete bool
Nonce string `json:"nonce"`
Started bool `json:"started"`
Progress int `json:"progress"`
Required int `json:"required"`
Complete bool `json:"complete"`
EncodedToken string `json:"encoded_token"`
EncodedRootToken string `json:"encoded_root_token"`
PGPFingerprint string `json:"pgp_fingerprint"`
}

View File

@@ -2,6 +2,13 @@ package api
func (c *Sys) Health() (*HealthResponse, error) {
r := c.c.NewRequest("GET", "/v1/sys/health")
// If the code is 400 or above it will automatically turn into an error,
// but the sys/health API defaults to returning 5xx when not sealed or
// inited, so we force this code to be something else so we parse correctly
r.Params.Add("uninitcode", "299")
r.Params.Add("sealedcode", "299")
r.Params.Add("standbycode", "299")
r.Params.Add("drsecondarycode", "299")
resp, err := c.c.RawRequest(r)
if err != nil {
return nil, err
@@ -14,11 +21,13 @@ func (c *Sys) Health() (*HealthResponse, error) {
}
type HealthResponse struct {
Initialized bool `json:"initialized"`
Sealed bool `json:"sealed"`
Standby bool `json:"standby"`
ServerTimeUTC int64 `json:"server_time_utc"`
Version string `json:"version"`
ClusterName string `json:"cluster_name,omitempty"`
ClusterID string `json:"cluster_id,omitempty"`
Initialized bool `json:"initialized"`
Sealed bool `json:"sealed"`
Standby bool `json:"standby"`
ReplicationPerformanceMode string `json:"replication_performance_mode"`
ReplicationDRMode string `json:"replication_dr_mode"`
ServerTimeUTC int64 `json:"server_time_utc"`
Version string `json:"version"`
ClusterName string `json:"cluster_name,omitempty"`
ClusterID string `json:"cluster_id,omitempty"`
}

View File

@@ -120,17 +120,25 @@ func (c *Sys) MountConfig(path string) (*MountConfigOutput, error) {
}
type MountInput struct {
Type string `json:"type" structs:"type"`
Description string `json:"description" structs:"description"`
Config MountConfigInput `json:"config" structs:"config"`
Local bool `json:"local" structs:"local"`
Type string `json:"type" structs:"type"`
Description string `json:"description" structs:"description"`
Config MountConfigInput `json:"config" structs:"config"`
Options map[string]string `json:"options" structs:"options"`
Local bool `json:"local" structs:"local"`
PluginName string `json:"plugin_name,omitempty" structs:"plugin_name"`
SealWrap bool `json:"seal_wrap" structs:"seal_wrap" mapstructure:"seal_wrap"`
}
type MountConfigInput struct {
DefaultLeaseTTL string `json:"default_lease_ttl" structs:"default_lease_ttl" mapstructure:"default_lease_ttl"`
MaxLeaseTTL string `json:"max_lease_ttl" structs:"max_lease_ttl" mapstructure:"max_lease_ttl"`
ForceNoCache bool `json:"force_no_cache" structs:"force_no_cache" mapstructure:"force_no_cache"`
PluginName string `json:"plugin_name,omitempty" structs:"plugin_name,omitempty" mapstructure:"plugin_name"`
Options map[string]string `json:"options" structs:"options" mapstructure:"options"`
DefaultLeaseTTL string `json:"default_lease_ttl" structs:"default_lease_ttl" mapstructure:"default_lease_ttl"`
MaxLeaseTTL string `json:"max_lease_ttl" structs:"max_lease_ttl" mapstructure:"max_lease_ttl"`
ForceNoCache bool `json:"force_no_cache" structs:"force_no_cache" mapstructure:"force_no_cache"`
PluginName string `json:"plugin_name,omitempty" structs:"plugin_name,omitempty" mapstructure:"plugin_name"`
AuditNonHMACRequestKeys []string `json:"audit_non_hmac_request_keys,omitempty" structs:"audit_non_hmac_request_keys" mapstructure:"audit_non_hmac_request_keys"`
AuditNonHMACResponseKeys []string `json:"audit_non_hmac_response_keys,omitempty" structs:"audit_non_hmac_response_keys" mapstructure:"audit_non_hmac_response_keys"`
ListingVisibility string `json:"listing_visibility,omitempty" structs:"listing_visibility" mapstructure:"listing_visibility"`
PassthroughRequestHeaders []string `json:"passthrough_request_headers,omitempty" structs:"passthrough_request_headers" mapstructure:"passthrough_request_headers"`
}
type MountOutput struct {
@@ -138,12 +146,18 @@ type MountOutput struct {
Description string `json:"description" structs:"description"`
Accessor string `json:"accessor" structs:"accessor"`
Config MountConfigOutput `json:"config" structs:"config"`
Options map[string]string `json:"options" structs:"options"`
Local bool `json:"local" structs:"local"`
SealWrap bool `json:"seal_wrap" structs:"seal_wrap" mapstructure:"seal_wrap"`
}
type MountConfigOutput struct {
DefaultLeaseTTL int `json:"default_lease_ttl" structs:"default_lease_ttl" mapstructure:"default_lease_ttl"`
MaxLeaseTTL int `json:"max_lease_ttl" structs:"max_lease_ttl" mapstructure:"max_lease_ttl"`
ForceNoCache bool `json:"force_no_cache" structs:"force_no_cache" mapstructure:"force_no_cache"`
PluginName string `json:"plugin_name,omitempty" structs:"plugin_name,omitempty" mapstructure:"plugin_name"`
DefaultLeaseTTL int `json:"default_lease_ttl" structs:"default_lease_ttl" mapstructure:"default_lease_ttl"`
MaxLeaseTTL int `json:"max_lease_ttl" structs:"max_lease_ttl" mapstructure:"max_lease_ttl"`
ForceNoCache bool `json:"force_no_cache" structs:"force_no_cache" mapstructure:"force_no_cache"`
PluginName string `json:"plugin_name,omitempty" structs:"plugin_name,omitempty" mapstructure:"plugin_name"`
AuditNonHMACRequestKeys []string `json:"audit_non_hmac_request_keys,omitempty" structs:"audit_non_hmac_request_keys" mapstructure:"audit_non_hmac_request_keys"`
AuditNonHMACResponseKeys []string `json:"audit_non_hmac_response_keys,omitempty" structs:"audit_non_hmac_response_keys" mapstructure:"audit_non_hmac_response_keys"`
ListingVisibility string `json:"listing_visibility,omitempty" structs:"listing_visibility" mapstructure:"listing_visibility"`
PassthroughRequestHeaders []string `json:"passthrough_request_headers,omitempty" structs:"passthrough_request_headers" mapstructure:"passthrough_request_headers"`
}

117
vendor/github.com/hashicorp/vault/api/sys_plugins.go generated vendored Normal file
View File

@@ -0,0 +1,117 @@
package api
import (
"fmt"
"net/http"
)
// ListPluginsInput is used as input to the ListPlugins function.
type ListPluginsInput struct{}
// ListPluginsResponse is the response from the ListPlugins call.
type ListPluginsResponse struct {
// Names is the list of names of the plugins.
Names []string
}
// ListPlugins lists all plugins in the catalog and returns their names as a
// list of strings.
func (c *Sys) ListPlugins(i *ListPluginsInput) (*ListPluginsResponse, error) {
path := "/v1/sys/plugins/catalog"
req := c.c.NewRequest("LIST", path)
resp, err := c.c.RawRequest(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
var result struct {
Data struct {
Keys []string `json:"keys"`
} `json:"data"`
}
if err := resp.DecodeJSON(&result); err != nil {
return nil, err
}
return &ListPluginsResponse{Names: result.Data.Keys}, nil
}
// GetPluginInput is used as input to the GetPlugin function.
type GetPluginInput struct {
Name string `json:"-"`
}
// GetPluginResponse is the response from the GetPlugin call.
type GetPluginResponse struct {
Args []string `json:"args"`
Builtin bool `json:"builtin"`
Command string `json:"command"`
Name string `json:"name"`
SHA256 string `json:"sha256"`
}
func (c *Sys) GetPlugin(i *GetPluginInput) (*GetPluginResponse, error) {
path := fmt.Sprintf("/v1/sys/plugins/catalog/%s", i.Name)
req := c.c.NewRequest(http.MethodGet, path)
resp, err := c.c.RawRequest(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
var result GetPluginResponse
err = resp.DecodeJSON(&result)
if err != nil {
return nil, err
}
return &result, err
}
// RegisterPluginInput is used as input to the RegisterPlugin function.
type RegisterPluginInput struct {
// Name is the name of the plugin. Required.
Name string `json:"-"`
// Args is the list of args to spawn the process with.
Args []string `json:"args,omitempty"`
// Command is the command to run.
Command string `json:"command,omitempty"`
// SHA256 is the shasum of the plugin.
SHA256 string `json:"sha256,omitempty"`
}
// RegisterPlugin registers the plugin with the given information.
func (c *Sys) RegisterPlugin(i *RegisterPluginInput) error {
path := fmt.Sprintf("/v1/sys/plugins/catalog/%s", i.Name)
req := c.c.NewRequest(http.MethodPut, path)
if err := req.SetJSONBody(i); err != nil {
return err
}
resp, err := c.c.RawRequest(req)
if err == nil {
defer resp.Body.Close()
}
return err
}
// DeregisterPluginInput is used as input to the DeregisterPlugin function.
type DeregisterPluginInput struct {
// Name is the name of the plugin. Required.
Name string `json:"-"`
}
// DeregisterPlugin removes the plugin with the given name from the plugin
// catalog.
func (c *Sys) DeregisterPlugin(i *DeregisterPluginInput) error {
path := fmt.Sprintf("/v1/sys/plugins/catalog/%s", i.Name)
req := c.c.NewRequest(http.MethodDelete, path)
resp, err := c.c.RawRequest(req)
if err == nil {
defer resp.Body.Close()
}
return err
}

View File

@@ -50,12 +50,14 @@ func (c *Sys) GetPolicy(name string) (string, error) {
return "", err
}
var ok bool
if _, ok = result["rules"]; !ok {
return "", fmt.Errorf("rules not found in response")
if rulesRaw, ok := result["rules"]; ok {
return rulesRaw.(string), nil
}
if policyRaw, ok := result["policy"]; ok {
return policyRaw.(string), nil
}
return result["rules"].(string), nil
return "", fmt.Errorf("no policy found in response")
}
func (c *Sys) PutPolicy(name, rules string) error {

View File

@@ -171,32 +171,33 @@ func (c *Sys) RekeyDeleteRecoveryBackup() error {
type RekeyInitRequest struct {
SecretShares int `json:"secret_shares"`
SecretThreshold int `json:"secret_threshold"`
StoredShares int `json:"stored_shares"`
PGPKeys []string `json:"pgp_keys"`
Backup bool
}
type RekeyStatusResponse struct {
Nonce string
Started bool
T int
N int
Progress int
Required int
Nonce string `json:"nonce"`
Started bool `json:"started"`
T int `json:"t"`
N int `json:"n"`
Progress int `json:"progress"`
Required int `json:"required"`
PGPFingerprints []string `json:"pgp_fingerprints"`
Backup bool
Backup bool `json:"backup"`
}
type RekeyUpdateResponse struct {
Nonce string
Complete bool
Keys []string
Nonce string `json:"nonce"`
Complete bool `json:"complete"`
Keys []string `json:"keys"`
KeysB64 []string `json:"keys_base64"`
PGPFingerprints []string `json:"pgp_fingerprints"`
Backup bool
Backup bool `json:"backup"`
}
type RekeyRetrieveResponse struct {
Nonce string
Keys map[string][]string
Nonce string `json:"nonce"`
Keys map[string][]string `json:"keys"`
KeysB64 map[string][]string `json:"keys_base64"`
}

View File

@@ -49,12 +49,14 @@ func sealStatusRequest(c *Sys, r *Request) (*SealStatusResponse, error) {
}
type SealStatusResponse struct {
Sealed bool `json:"sealed"`
T int `json:"t"`
N int `json:"n"`
Progress int `json:"progress"`
Nonce string `json:"nonce"`
Version string `json:"version"`
ClusterName string `json:"cluster_name,omitempty"`
ClusterID string `json:"cluster_id,omitempty"`
Type string `json:"type"`
Sealed bool `json:"sealed"`
T int `json:"t"`
N int `json:"n"`
Progress int `json:"progress"`
Nonce string `json:"nonce"`
Version string `json:"version"`
ClusterName string `json:"cluster_name,omitempty"`
ClusterID string `json:"cluster_id,omitempty"`
RecoverySeal bool `json:"recovery_seal"`
}

View File

@@ -33,7 +33,7 @@ const (
)
// SnappyReadCloser embeds the snappy reader which implements the io.Reader
// interface. The decompress procedure in this utility expectes an
// interface. The decompress procedure in this utility expects an
// io.ReadCloser. This type implements the io.Closer interface to retain the
// generic way of decompression.
type SnappyReadCloser struct {

View File

@@ -91,7 +91,7 @@ func DecodeJSONFromReader(r io.Reader, out interface{}) error {
dec := json.NewDecoder(r)
// While decoding JSON values, intepret the integer values as `json.Number`s instead of `float64`.
// While decoding JSON values, interpret the integer values as `json.Number`s instead of `float64`.
dec.UseNumber()
// Since 'out' is an interface representing a pointer, pass it to the decoder without an '&'

View File

@@ -7,6 +7,7 @@ import (
"strings"
"time"
"github.com/hashicorp/vault/helper/strutil"
"github.com/mitchellh/mapstructure"
)
@@ -19,6 +20,9 @@ func ParseDurationSecond(in interface{}) (time.Duration, error) {
switch in.(type) {
case string:
inp := in.(string)
if inp == "" {
return time.Duration(0), nil
}
var err error
// Look for a suffix otherwise its a plain second value
if strings.HasSuffix(inp, "s") || strings.HasSuffix(inp, "m") || strings.HasSuffix(inp, "h") {
@@ -53,6 +57,43 @@ func ParseDurationSecond(in interface{}) (time.Duration, error) {
return dur, nil
}
func ParseInt(in interface{}) (int64, error) {
var ret int64
jsonIn, ok := in.(json.Number)
if ok {
in = jsonIn.String()
}
switch in.(type) {
case string:
inp := in.(string)
if inp == "" {
return 0, nil
}
var err error
left, err := strconv.ParseInt(inp, 10, 64)
if err != nil {
return ret, err
}
ret = left
case int:
ret = int64(in.(int))
case int32:
ret = int64(in.(int32))
case int64:
ret = in.(int64)
case uint:
ret = int64(in.(uint))
case uint32:
ret = int64(in.(uint32))
case uint64:
ret = int64(in.(uint64))
default:
return 0, errors.New("could not parse value from input")
}
return ret, nil
}
func ParseBool(in interface{}) (bool, error) {
var result bool
if err := mapstructure.WeakDecode(in, &result); err != nil {
@@ -60,3 +101,20 @@ func ParseBool(in interface{}) (bool, error) {
}
return result, nil
}
func ParseCommaStringSlice(in interface{}) ([]string, error) {
var result []string
config := &mapstructure.DecoderConfig{
Result: &result,
WeaklyTypedInput: true,
DecodeHook: mapstructure.StringToSliceHookFunc(","),
}
decoder, err := mapstructure.NewDecoder(config)
if err != nil {
return nil, err
}
if err := decoder.Decode(in); err != nil {
return nil, err
}
return strutil.TrimStrings(result), nil
}

View File

@@ -0,0 +1,326 @@
package strutil
import (
"encoding/base64"
"encoding/json"
"fmt"
"sort"
"strings"
glob "github.com/ryanuber/go-glob"
)
// StrListContainsGlob looks for a string in a list of strings and allows
// globs.
func StrListContainsGlob(haystack []string, needle string) bool {
for _, item := range haystack {
if glob.Glob(item, needle) {
return true
}
}
return false
}
// StrListContains looks for a string in a list of strings.
func StrListContains(haystack []string, needle string) bool {
for _, item := range haystack {
if item == needle {
return true
}
}
return false
}
// StrListSubset checks if a given list is a subset
// of another set
func StrListSubset(super, sub []string) bool {
for _, item := range sub {
if !StrListContains(super, item) {
return false
}
}
return true
}
// Parses a comma separated list of strings into a slice of strings.
// The return slice will be sorted and will not contain duplicate or
// empty items.
func ParseDedupAndSortStrings(input string, sep string) []string {
input = strings.TrimSpace(input)
parsed := []string{}
if input == "" {
// Don't return nil
return parsed
}
return RemoveDuplicates(strings.Split(input, sep), false)
}
// Parses a comma separated list of strings into a slice of strings.
// The return slice will be sorted and will not contain duplicate or
// empty items. The values will be converted to lower case.
func ParseDedupLowercaseAndSortStrings(input string, sep string) []string {
input = strings.TrimSpace(input)
parsed := []string{}
if input == "" {
// Don't return nil
return parsed
}
return RemoveDuplicates(strings.Split(input, sep), true)
}
// Parses a comma separated list of `<key>=<value>` tuples into a
// map[string]string.
func ParseKeyValues(input string, out map[string]string, sep string) error {
if out == nil {
return fmt.Errorf("'out is nil")
}
keyValues := ParseDedupLowercaseAndSortStrings(input, sep)
if len(keyValues) == 0 {
return nil
}
for _, keyValue := range keyValues {
shards := strings.Split(keyValue, "=")
if len(shards) != 2 {
return fmt.Errorf("invalid <key,value> format")
}
key := strings.TrimSpace(shards[0])
value := strings.TrimSpace(shards[1])
if key == "" || value == "" {
return fmt.Errorf("invalid <key,value> pair: key:'%s' value:'%s'", key, value)
}
out[key] = value
}
return nil
}
// Parses arbitrary <key,value> tuples. The input can be one of
// the following:
// * JSON string
// * Base64 encoded JSON string
// * Comma separated list of `<key>=<value>` pairs
// * Base64 encoded string containing comma separated list of
// `<key>=<value>` pairs
//
// Input will be parsed into the output parameter, which should
// be a non-nil map[string]string.
func ParseArbitraryKeyValues(input string, out map[string]string, sep string) error {
input = strings.TrimSpace(input)
if input == "" {
return nil
}
if out == nil {
return fmt.Errorf("'out' is nil")
}
// Try to base64 decode the input. If successful, consider the decoded
// value as input.
inputBytes, err := base64.StdEncoding.DecodeString(input)
if err == nil {
input = string(inputBytes)
}
// Try to JSON unmarshal the input. If successful, consider that the
// metadata was supplied as JSON input.
err = json.Unmarshal([]byte(input), &out)
if err != nil {
// If JSON unmarshalling fails, consider that the input was
// supplied as a comma separated string of 'key=value' pairs.
if err = ParseKeyValues(input, out, sep); err != nil {
return fmt.Errorf("failed to parse the input: %v", err)
}
}
// Validate the parsed input
for key, value := range out {
if key != "" && value == "" {
return fmt.Errorf("invalid value for key '%s'", key)
}
}
return nil
}
// Parses a `sep`-separated list of strings into a
// []string.
//
// The output will always be a valid slice but may be of length zero.
func ParseStringSlice(input string, sep string) []string {
input = strings.TrimSpace(input)
if input == "" {
return []string{}
}
splitStr := strings.Split(input, sep)
ret := make([]string, len(splitStr))
for i, val := range splitStr {
ret[i] = val
}
return ret
}
// Parses arbitrary string slice. The input can be one of
// the following:
// * JSON string
// * Base64 encoded JSON string
// * `sep` separated list of values
// * Base64-encoded string containing a `sep` separated list of values
//
// Note that the separator is ignored if the input is found to already be in a
// structured format (e.g., JSON)
//
// The output will always be a valid slice but may be of length zero.
func ParseArbitraryStringSlice(input string, sep string) []string {
input = strings.TrimSpace(input)
if input == "" {
return []string{}
}
// Try to base64 decode the input. If successful, consider the decoded
// value as input.
inputBytes, err := base64.StdEncoding.DecodeString(input)
if err == nil {
input = string(inputBytes)
}
ret := []string{}
// Try to JSON unmarshal the input. If successful, consider that the
// metadata was supplied as JSON input.
err = json.Unmarshal([]byte(input), &ret)
if err != nil {
// If JSON unmarshalling fails, consider that the input was
// supplied as a separated string of values.
return ParseStringSlice(input, sep)
}
if ret == nil {
return []string{}
}
return ret
}
// TrimStrings takes a slice of strings and returns a slice of strings
// with trimmed spaces
func TrimStrings(items []string) []string {
ret := make([]string, len(items))
for i, item := range items {
ret[i] = strings.TrimSpace(item)
}
return ret
}
// Removes duplicate and empty elements from a slice of strings. This also may
// convert the items in the slice to lower case and returns a sorted slice.
func RemoveDuplicates(items []string, lowercase bool) []string {
itemsMap := map[string]bool{}
for _, item := range items {
item = strings.TrimSpace(item)
if lowercase {
item = strings.ToLower(item)
}
if item == "" {
continue
}
itemsMap[item] = true
}
items = make([]string, 0, len(itemsMap))
for item, _ := range itemsMap {
items = append(items, item)
}
sort.Strings(items)
return items
}
// EquivalentSlices checks whether the given string sets are equivalent, as in,
// they contain the same values.
func EquivalentSlices(a, b []string) bool {
if a == nil && b == nil {
return true
}
if a == nil || b == nil {
return false
}
// First we'll build maps to ensure unique values
mapA := map[string]bool{}
mapB := map[string]bool{}
for _, keyA := range a {
mapA[keyA] = true
}
for _, keyB := range b {
mapB[keyB] = true
}
// Now we'll build our checking slices
var sortedA, sortedB []string
for keyA, _ := range mapA {
sortedA = append(sortedA, keyA)
}
for keyB, _ := range mapB {
sortedB = append(sortedB, keyB)
}
sort.Strings(sortedA)
sort.Strings(sortedB)
// Finally, compare
if len(sortedA) != len(sortedB) {
return false
}
for i := range sortedA {
if sortedA[i] != sortedB[i] {
return false
}
}
return true
}
// StrListDelete removes the first occurrence of the given item from the slice
// of strings if the item exists.
func StrListDelete(s []string, d string) []string {
if s == nil {
return s
}
for index, element := range s {
if element == d {
return append(s[:index], s[index+1:]...)
}
}
return s
}
func GlobbedStringsMatch(item, val string) bool {
if len(item) < 2 {
return val == item
}
hasPrefix := strings.HasPrefix(item, "*")
hasSuffix := strings.HasSuffix(item, "*")
if hasPrefix && hasSuffix {
return strings.Contains(val, item[1:len(item)-1])
} else if hasPrefix {
return strings.HasSuffix(val, item[1:])
} else if hasSuffix {
return strings.HasPrefix(val, item[:len(item)-1])
}
return val == item
}
// AppendIfMissing adds a string to a slice if the given string is not present
func AppendIfMissing(slice []string, i string) []string {
if StrListContains(slice, i) {
return slice
}
return append(slice, i)
}

21
vendor/github.com/ryanuber/go-glob/LICENSE generated vendored Normal file
View File

@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2014 Ryan Uber
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

29
vendor/github.com/ryanuber/go-glob/README.md generated vendored Normal file
View File

@@ -0,0 +1,29 @@
# String globbing in golang [![Build Status](https://travis-ci.org/ryanuber/go-glob.svg)](https://travis-ci.org/ryanuber/go-glob)
`go-glob` is a single-function library implementing basic string glob support.
Globs are an extremely user-friendly way of supporting string matching without
requiring knowledge of regular expressions or Go's particular regex engine. Most
people understand that if you put a `*` character somewhere in a string, it is
treated as a wildcard. Surprisingly, this functionality isn't found in Go's
standard library, except for `path.Match`, which is intended to be used while
comparing paths (not arbitrary strings), and contains specialized logic for this
use case. A better solution might be a POSIX basic (non-ERE) regular expression
engine for Go, which doesn't exist currently.
Example
=======
```
package main
import "github.com/ryanuber/go-glob"
func main() {
glob.Glob("*World!", "Hello, World!") // true
glob.Glob("Hello,*", "Hello, World!") // true
glob.Glob("*ello,*", "Hello, World!") // true
glob.Glob("World!", "Hello, World!") // false
glob.Glob("/home/*", "/home/ryanuber/.bashrc") // true
}
```

56
vendor/github.com/ryanuber/go-glob/glob.go generated vendored Normal file
View File

@@ -0,0 +1,56 @@
package glob
import "strings"
// The character which is treated like a glob
const GLOB = "*"
// Glob will test a string pattern, potentially containing globs, against a
// subject string. The result is a simple true/false, determining whether or
// not the glob pattern matched the subject text.
func Glob(pattern, subj string) bool {
// Empty pattern can only match empty subject
if pattern == "" {
return subj == pattern
}
// If the pattern _is_ a glob, it matches everything
if pattern == GLOB {
return true
}
parts := strings.Split(pattern, GLOB)
if len(parts) == 1 {
// No globs in pattern, so test for equality
return subj == pattern
}
leadingGlob := strings.HasPrefix(pattern, GLOB)
trailingGlob := strings.HasSuffix(pattern, GLOB)
end := len(parts) - 1
// Go over the leading parts and ensure they match.
for i := 0; i < end; i++ {
idx := strings.Index(subj, parts[i])
switch i {
case 0:
// Check the first section. Requires special handling.
if !leadingGlob && idx != 0 {
return false
}
default:
// Check that the middle parts match.
if idx < 0 {
return false
}
}
// Trim evaluated text from subj as we loop over the pattern.
subj = subj[idx+len(parts[i]):]
}
// Reached the last section. Requires special handling.
return trailingGlob || strings.HasSuffix(subj, parts[end])
}

10
vendor/vendor.json vendored
View File

@@ -169,10 +169,11 @@
{"path":"github.com/hashicorp/serf/coordinate","checksumSHA1":"/oss17GO4hXGM7QnUdI3VzcAHzA=","revision":"bbeddf0b3ab3072a60525afbd6b6f47d33839eee","revisionTime":"2017-07-14T18:26:01Z"},
{"path":"github.com/hashicorp/serf/serf","checksumSHA1":"pvLOzocYsZtxuJ9pREHRTxYnoa4=","revision":"bbeddf0b3ab3072a60525afbd6b6f47d33839eee","revisionTime":"2017-07-14T18:26:01Z"},
{"path":"github.com/hashicorp/vault","checksumSHA1":"eGzvBRMFD6ZB3A6uO750np7Om/E=","revision":"182ba68a9589d4cef95234134aaa498a686e3de3","revisionTime":"2016-08-21T23:40:57Z"},
{"path":"github.com/hashicorp/vault/api","checksumSHA1":"hLIXn9iQhPcjY+/G64j3mIlLlK8=","revision":"0c3e14f047aede0a70256e1e8b321610910b246e","revisionTime":"2017-08-01T15:50:41Z"},
{"path":"github.com/hashicorp/vault/helper/compressutil","checksumSHA1":"au+CDkddC4sVFV15UaPiI7FvSw0=","revision":"1fd46cbcb10569bd205c3f662e7a4f16f1e69056","revisionTime":"2017-08-11T01:28:18Z"},
{"path":"github.com/hashicorp/vault/helper/jsonutil","checksumSHA1":"yUiSTPf0QUuL2r/81sjuytqBoeQ=","revision":"0c3e14f047aede0a70256e1e8b321610910b246e","revisionTime":"2017-08-01T15:50:41Z"},
{"path":"github.com/hashicorp/vault/helper/parseutil","checksumSHA1":"GGveKvOwScWGZAAnupzpyw+0Jko=","revision":"1fd46cbcb10569bd205c3f662e7a4f16f1e69056","revisionTime":"2017-08-11T01:28:18Z"},
{"path":"github.com/hashicorp/vault/api","checksumSHA1":"mKN4rEIWyflT6aqJyjgu9m1tPXI=","revision":"3ddd3bd20cec0588788547aecd15e91461b9d546","revisionTime":"2018-04-03T21:11:47Z"},
{"path":"github.com/hashicorp/vault/helper/compressutil","checksumSHA1":"jHVLe8KMdEpb/ZALp0zu+tenADo=","revision":"3ddd3bd20cec0588788547aecd15e91461b9d546","revisionTime":"2018-04-03T21:11:47Z"},
{"path":"github.com/hashicorp/vault/helper/jsonutil","checksumSHA1":"TEViSweHazfDVJ/4Y+luMnNMiqY=","revision":"3ddd3bd20cec0588788547aecd15e91461b9d546","revisionTime":"2018-04-03T21:11:47Z"},
{"path":"github.com/hashicorp/vault/helper/parseutil","checksumSHA1":"6OrIfQ/Lr5hNyZ9oU/JQvfd2Bto=","revision":"3ddd3bd20cec0588788547aecd15e91461b9d546","revisionTime":"2018-04-03T21:11:47Z"},
{"path":"github.com/hashicorp/vault/helper/strutil","checksumSHA1":"rXiSGn0TsznSSCvVlt7fvXKMF1M=","revision":"3ddd3bd20cec0588788547aecd15e91461b9d546","revisionTime":"2018-04-03T21:11:47Z"},
{"path":"github.com/hashicorp/yamux","checksumSHA1":"NnWv17i1tpvBNJtpdRRWpE6j4LY=","revision":"2658be15c5f05e76244154714161f17e3e77de2e","revisionTime":"2018-03-14T20:07:45Z"},
{"path":"github.com/hpcloud/tail/util","checksumSHA1":"0xM336Lb25URO/1W1/CtGoRygVU=","revision":"37f4271387456dd1bf82ab1ad9229f060cc45386","revisionTime":"2017-08-14T16:06:53Z"},
{"path":"github.com/hpcloud/tail/watch","checksumSHA1":"TP4OAv5JMtzj2TB6OQBKqauaKDc=","revision":"37f4271387456dd1bf82ab1ad9229f060cc45386","revisionTime":"2017-08-14T16:06:53Z"},
@@ -258,6 +259,7 @@
{"path":"github.com/rkt/rkt/networking/netinfo","checksumSHA1":"4QqLbh9MmajcN6gCx8Er1voiQys=","revision":"5e83d91aafef5f7a38fef62c045e8b57eeeb8bce","revisionTime":"2017-09-20T12:17:54Z"},
{"path":"github.com/rs/cors","checksumSHA1":"I778b2sbNN/yjwKSdb3y7hz2yUQ=","revision":"eabcc6af4bbe5ad3a949d36450326a2b0b9894b8","revisionTime":"2017-08-01T07:32:01Z"},
{"path":"github.com/ryanuber/columnize","checksumSHA1":"M57Rrfc8Z966p+IBtQ91QOcUtcg=","comment":"v2.0.1-8-g983d3a5","revision":"abc90934186a77966e2beeac62ed966aac0561d5","revisionTime":"2017-07-03T20:58:27Z"},
{"path":"github.com/ryanuber/go-glob","checksumSHA1":"6JP37UqrI0H80Gpk0Y2P+KXgn5M=","revision":"256dc444b735e061061cf46c809487313d5b0065","revisionTime":"2017-01-28T01:21:29Z"},
{"path":"github.com/sean-/seed","checksumSHA1":"tnMZLo/kR9Kqx6GtmWwowtTLlA8=","revision":"e2103e2c35297fb7e17febb81e49b312087a2372","revisionTime":"2017-03-13T16:33:22Z"},
{"path":"github.com/sethgrid/pester","checksumSHA1":"8Lm8nsMCFz4+gr9EvQLqK8+w+Ks=","revision":"8053687f99650573b28fb75cddf3f295082704d7","revisionTime":"2016-04-29T17:20:22Z"},
{"path":"github.com/shirou/gopsutil/cpu","checksumSHA1":"k+PmW/6PFt0FVFTTnfMbWwrm9hU=","revision":"5776ff9c7c5d063d574ef53d740f75c68b448e53","revisionTime":"2018-02-27T22:58:47Z","tree":true},